Logo
Published on

A Guide to Building an API Server with Nextjs 14, Mongoose, and Cypress

Authors
  • Name
    Twitter

Creating an API server using Next.js 14 and Mongoose involves several key steps. Next.js, a popular React framework, simplifies the process of building server-side functionality, including APIs. Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It manages relationships between data, provides schema validation, and is used to translate between objects in code and the representation of those objects in MongoDB.

Prerequisites

  • Node.js: Make sure Node.js is installed on your machine.
  • MongoDB: Have a MongoDB database set up, either locally or hosted (e.g., MongoDB Atlas).
  • Next.js Project: Start with a Next.js project. If you haven’t created one, you can do so using npx create-next-app@14 your-project-name.

Step 1: Set Up Your Next.js Project

  • Initialize Your Project: If you haven’t already, create a new Next.js project:
npx create-next-app@14 your-project-name  
cd your-project-name

Install Dependencies: Install Mongoose to interact with your MongoDB database:

npm install mongoose

Step 2: Configure Mongoose

  • Create a Mongoose Connection: In your project, create a file to handle connecting to MongoDB, such as util/connect-mongo.ts.
import mongoose from 'mongoose';  
  
const MONGO_URI = process.env.MONGO_URI;  
const cached: { connection?: typeof mongoose; promise?: Promise<typeof mongoose> } = {};  
async function connectMongo() {  
    if (!MONGO_URI) {  
        throw new Error('Please define the MONGO_URI environment variable inside .env.local');  
    }  
    if (cached.connection) {  
        return cached.connection;  
    }  
    if (!cached.promise) {  
        const opts = {  
            bufferCommands: false,  
        };  
        cached.promise = mongoose.connect(MONGO_URI, opts);  
    }  
    try {  
        cached.connection = await cached.promise;  
    } catch (e) {  
        cached.promise = undefined;  
        throw e;  
    }  
    return cached.connection;  
}  
export default connectMongo;
  • Create a Mongoose Schema and Model: For example, if you’re creating a user model, create a file like models/product.ts.
import { model, models, Schema } from 'mongoose';  
  
export interface IProduct {  
    name: string;  
    description: string;  
    price: number;  
}  
const ProductSchema = new Schema<IProduct>(  
    {  
        name: String,  
        description: String,  
        price: Number,  
    },  
    {  
        timestamps: true,  
        toJSON: {  
            versionKey: false,  
            virtuals: true,  
            transform: (_, ret) => {  
                delete ret._id;  
            },  
        },  
    },  
);  
const Product = models.Product || model('Product', ProductSchema);  
export default Product;

Step 3: Create API Routes

  • Define API Endpoints: In the app/api directory, create files for each of your API routes. For example, app/api/products/route.ts
import { HttpStatusCode } from 'axios';  
import connectMongo from '@/util/connect-mongo';  
import Product from '@/models/product';  
import { CreateProductDto } from '@/dto/create-product.dto';  
import { NextRequest, NextResponse } from 'next/server';  
  
export async function POST(req: NextRequest) {  
    try {  
        await connectMongo();  
        const body: CreateProductDto = await req.json();  
        if (body.name) {  
            const product = await Product.create(body);  
            return NextResponse.json(  
                { product, message: 'Your product has been created' },  
                { status: HttpStatusCode.Created },  
            );  
        }  
        return NextResponse.json({ message: 'Product name is missing' }, { status: HttpStatusCode.BadRequest });  
    } catch (error) {  
        return NextResponse.json({ message: error }, { status: HttpStatusCode.BadRequest });  
    }  
}  
export async function GET() {  
    try {  
        await connectMongo();  
        const products = await Product.find();  
        return NextResponse.json({ data: products });  
    } catch (error) {  
        return NextResponse.json({ error });  
    }  
}

In this, the handlers above are for routes:

(POST) /api/products  
(GET) /api/products

Note: In this example, I have created a DTO file with the following code in the folder dto/create-product.dto.ts:

export type CreateProductDto = {  
    name: string;  
    description: string;  
    price: number;  
};

For handling api route for a specific product, we will need to create a route.ts in folder api/product/[id]/route.ts

import { NextRequest, NextResponse } from 'next/server';  
import connectMongo from '@/keepCount/util/connect-mongo';  
import Product from '@/keepCount/models/product';  
import { HttpStatusCode } from 'axios';  
import { UpdateProductDto } from '@/keepCount/dto/update-product.dto';  
  
export async function GET(_: NextRequest, { params }: { params: { id: string } }) {  
    try {  
        await connectMongo();  
        const product = await Product.findById(params.id);  
        if (product) {  
            return NextResponse.json({ product });  
        }  
        return NextResponse.json({ message: `Product ${params.id} not found` }, { status: HttpStatusCode.NotFound });  
    } catch (error) {  
        return NextResponse.json({ message: error }, { status: HttpStatusCode.BadRequest });  
    }  
}  
  
export async function PUT(req: NextRequest, { params }: { params: { id: string } }) {  
    try {  
        await connectMongo();  
        const product = await Product.findById(params.id);  
        if (product) {  
            const body: UpdateProductDto = await req.json();  
            if (body.name) {  
                product.name = body.name;  
            }  
            if (body.price) {  
                product.name = body.price;  
            }  
            if (body.description) {  
                product.name = body.description;  
            }  
            product.save();  
            return NextResponse.json({ product });  
        }  
        return NextResponse.json({ message: `Product ${params.id} not found` }, { status: HttpStatusCode.NotFound });  
    } catch (error) {  
        return NextResponse.json({ message: error }, { status: HttpStatusCode.BadRequest });  
    }  
}  
  
export async function DELETE(_: NextRequest, { params }: { params: { id: string } }) {  
    try {  
        await connectMongo();  
        const product = await Product.findById(params.id);  
        if (product) {  
            await Product.findByIdAndDelete(product._id);  
            return NextResponse.json({ message: `Product ${params.id} has been deleted` });  
        }  
        return NextResponse.json({ message: `Product ${params.id} not found` }, { status: HttpStatusCode.NotFound });  
    } catch (error) {  
        return NextResponse.json({ message: error }, { status: HttpStatusCode.BadRequest });  
    }  
}

In this handlers above are for routes:

(PUT) /api/products/{id}  
(GET) /api/products/{id}  
(DELETE) /api/products/{id}

Step 4: Environment Variables

  • Configure Environment Variables: Set up your .env.local file in the root of your Next.js project to store sensitive information like your MongoDB URI.
MONGO_URI=mongodb+srv://yourMongoDBURI  
# MONGO_URI=mongodb://localhost:@27017/your-database-name

Step 5: Testing Your API

  • Test Your Endpoints: Use tools like Postman, Thunder client a VC code plugin or your browser to test the API endpoints. Make sure they connect to the database and return the expected results.

  • Furthermore you can also use cypress :

npm install --save-dev cypress start-server-and-test
  • Add the Cypress open command to the package.json scripts field:
{  
  "scripts": {  
        "dev": "next dev",  
        "build": "next build",  
        "start": "next start",  
        "lint": "next lint",  
        "cypress:open": "cypress open",  
        "e2e": "start-server-and-test dev 3000 \"cypress run --e2e\"",  
        "e2e:headless": "start-server-and-test dev 3000 \"cypress run --e2e\""  
    },  
}
  • Then let update our cypress.config.ts:
import { defineConfig } from 'cypress';  
  
export default defineConfig({  
    e2e: {  
        setupNodeEvents() {},  
        baseUrl: 'http://localhost:3000',  
    },  
});
  • Now let create our e2e to test our Api Endpoints in the folder cypress/api/product.cy.ts:
import { CreateProductDto } from '@/keepCount/dto/create-product.dto';  
import { faker } from '@faker-js/faker';  
  
describe('Product Api (e2e)', () => {  
    it('Get Products', () => {  
        cy.request('GET', '/api/products').then((response) => {  
            expect(response.status).to.eq(200);  
        });  
    });  
  
    const createProductDTO: CreateProductDto = {  
        name: faker.word.words(),  
        description: faker.lorem.paragraph(),  
        price: Number(faker.commerce.price()),  
    };  
  
    let productId: string | undefined = undefined;  
  
    it('Post Products', () => {  
        cy.request('POST', '/api/products', createProductDTO).then((response) => {  
            expect(response.status).to.eq(201);  
            expect(response.body.product.id).to.be.ok;  
            expect(response.body.product.name).to.eq(createProductDTO.name);  
            expect(response.body.product.description).to.eq(createProductDTO.description);  
            expect(response.body.product.price).to.eq(createProductDTO.price);  
  
            productId = response.body.product.id;  
        });  
    });  
  
    it('Get Product', () => {  
        cy.request('GET', `/api/products/${productId}`).then((response) => {  
            expect(response.status).to.eq(200);  
            expect(response.body.product.id).to.be.ok;  
            expect(response.body.product.name).to.eq(createProductDTO.name);  
            expect(response.body.product.description).to.eq(createProductDTO.description);  
            expect(response.body.product.price).to.eq(createProductDTO.price);  
        });  
    });  
  
    const newName = faker.word.noun();  
  
    it('PUT Product', () => {  
        cy.request('PUT', `/api/products/${productId}`, { name: newName }).then((response) => {  
            expect(response.status).to.eq(200);  
            expect(response.body.product.id).to.be.ok;  
            expect(response.body.product.name).to.eq(newName);  
            expect(response.body.product.description).to.eq(createProductDTO.description);  
            expect(response.body.product.price).to.eq(createProductDTO.price);  
        });  
    });  
  
    it('DELETE Product', () => {  
        cy.request('DELETE', `/api/products/${productId}`).then((response) => {  
            expect(response.status).to.eq(200);  
        });  
    });  
  
    it('GET Product', () => {  
        cy.request({ url: `/api/products/${productId}`, method: 'DELETE', failOnStatusCode: false }).then(  
            (response) => {  
                expect(response.status).to.eq(404);  
            },  
        );  
    });  
});
  • Now to test it let run:
npm run e2e

Additional Tips

  • Security: Implement security best practices, especially if handling sensitive data.
  • Validation: Add validation to your API inputs.
  • Error Handling: Implement comprehensive error handling for robustness.
  • CORS: If your API will be accessed from different domains, configure CORS (Cross-Origin Resource Sharing) settings appropriately.

By following these steps, you’ll set up a basic API server using Next.js 14 and Mongoose, capable of performing CRUD operations with a MongoDB database.