Teguh Arief

NestJS CRUD with MongoDB using Mongoose

Building a NestJS CRUD application with MongoDB and Mongoose for a superior backend.

Teguh Arief

Published on: June 1, 2024

Share:

Building a robust backend often involves combining powerful tools, and the NestJS and MongoDB duo stands out as a superior choice. This guide will walk you through creating a NestJS CRUD (Create, Read, Update, Delete) application using MongoDB with Mongoose.

MongoDB leverages Mongoose as its Object Data Modeling (ODM) library, often referred to as an Object Relational Mapping (ORM) in a broader sense. In NestJS, other database integrations, such as those with relational databases, might use TypeORM as a "bridge" between object-oriented programs and relational databases.

Before diving into this CRUD application, ensure your MongoDB server is running on your local machine. If not, you can download and install it using the following commands (for macOS with Homebrew):

For MongoDB Shell Download, you can use MongoDB Compass as an alternative GUI tool.

brew tap mongodb/brew
brew install mongodb-community
brew services start mongodb-community
brew services start mongodb/brew/mongodb-community

Additionally, the NestJS CLI should be installed. If you haven't already, install it for this project:

npx @nestjs/cli new nestjs-crud-with-mongodb-using-mongoose

Getting Started with NestJS CRUD

Begin by navigating into your new NestJS project directory and installing the necessary MongoDB and Mongoose dependencies:

cd nestjs-crud-with-mongodb-using-mongoose
npm install @nestjs/mongoose mongoose class-transformer class-validator @nestjs/mapped-types

Module Configuration for MongoDB

Next, you'll configure your module to integrate with Mongoose. This involves specifying the presence of your schema within the application's context. First, generate a new module for your data:

npx @nestjs/cli generate module student

Then, register your models within the current scope using the forFeature() method in your student.module.ts file. It should look like this:

import { Module } from '@nestjs/common';
import { StudentController } from './student.controller';
import { StudentService } from './student.service';
import { MongooseModule } from '@nestjs/mongoose';
import { StudentSchema } from './schema/student.schema';

@Module({
  imports: [
    MongooseModule.forFeature([{ name: 'Student', schema: StudentSchema }]),
  ],
  controllers: [StudentController],
  providers: [StudentService],
})
export class StudentModule {}

Creating Your Mongoose Schema

Now, define your Mongoose model by creating a new schema file, student.schema.ts, within a `schema` folder inside your `student` module. Add the required student properties. After adding the properties, your file will resemble the following:

This code utilizes two key NestJS Decorators for schema definition:

  1. @Schema(): This decorator marks the class as a schema definition. The name given to this class, StudentSchema, will serve as the name of your MongoDB collection, and the Student class will map directly to this collection.
  2. @Prop(): This decorator defines a property within your document. In this schema, we have five properties: `name`, `roleNumber`, `class`, `gender`, and `marks`. NestJS automatically infers the types for these properties through TypeScript's metadata and class reflection.
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";

@Schema()
export class Student {

    @Prop()
    name: string;

    @Prop()
    roleNumber: number;

    @Prop()
    class: number;

    @Prop()
    gender: string;

    @Prop()
    marks: number;
}

export const StudentSchema = SchemaFactory.createForClass(Student);

Defining an Interface for Your Data

To define the structure of your data objects, create an interface for the student schema. Create a new `interface` folder and the student.interface.ts file within your `student` module. This interface will inherit properties from the Mongoose Document class. All properties are set as `readonly` to prevent accidental modification.

import { Document } from 'mongoose';

export interface IStudent extends Document{
    readonly name: string;

    readonly roleNumber: number;

    readonly class: number;

    readonly gender: string;

    readonly marks: number;
}

Implementing the Student Service

Now, create the student.service.ts file. This service class will act as the intermediary between your request handlers and the database. Generate it using the NestJS CLI:

npx @nestjs/cli generate service student

Within this service, you'll implement methods for the CRUD operations: creating, reading, updating, and deleting student documents from your collection. These methods will utilize standard Mongoose operations available through the `studentModel`.

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { CreateStudentDto } from './dto/create-student.dto';
import { IStudent } from './interface/student.interface';
import { Model } from "mongoose";
import { UpdateStudentDto } from './dto/update-student.dto';

@Injectable()
export class StudentService {
    constructor(@InjectModel('Student') private studentModel: Model) { }

    async createStudent(createStudentDto: CreateStudentDto): Promise {
        const newStudent = await new this.studentModel(createStudentDto);
        return newStudent.save();
    }

    async updateStudent(id: string, updateStudentDto: UpdateStudentDto): Promise {
        const existingStudent = await this.studentModel.findByIdAndUpdate(id, updateStudentDto, { new: true });
        if (!existingStudent) {
            throw new NotFoundException('Student #${id} not found');
        }
        return existingStudent;
&    }

    async getAllStudents(): Promise<IStudent[]> {
        const studentData = await this.studentModel.find();
        if (!studentData || studentData.length == 0) {
            throw new NotFoundException('Students data not found!');
        }
        return studentData;
    }

    async getStudent(id: string): Promise {
        const existingStudent = await this.studentModel.findById(id).exec();
        if (!existingStudent) {
            throw new NotFoundException('Student #${id} not found');
        }
        return existingStudent;
    }

    async deleteStudent(id: string): Promise {
        const deletedStudent = await this.studentModel.findByIdAndDelete(id);
        if (!deletedStudent) {
            throw new NotFoundException('Student #${id} not found');
        }
        return deletedStudent;
    }
}

The StudentService class is decorated with @Injectable(), making it available for dependency injection into other classes. In the constructor, the `studentModel` is injected into the service using the @InjectModel decorator. This injection is only possible after the schema has been registered in the `app.module.ts` configuration. Ensure you add `StudentService` to the `providers` array in your `app.module.ts` to make it available in the application's context.

Creating the Student Controller

Now, implement the controller to handle incoming requests and perform CRUD operations. Create a student.controller.ts file in the `student` module by executing:

npx @nestjs/cli generate controller student

You'll inject the StudentService class into the controller's constructor. At runtime, NestJS will provide an instance of StudentService, allowing the controller to access the methods you implemented in the service file.

Implement the standard POST, PUT, DELETE, and GET request handlers. These handlers will call the appropriate methods from the StudentService instance to perform various operations.

import { Body, Controller, Delete, Get, HttpStatus, Param, Post, Put, Res } from '@nestjs/common';
import { CreateStudentDto } from './dto/create-student.dto';
import { UpdateStudentDto } from './dto/update-student.dto';
import { StudentService } from './student.service';

@Controller('student')
export class StudentController {
    constructor(private readonly studentService: StudentService) { }

    @Post()
    async createStudent(@Res() response, @Body() createStudentDto: CreateStudentDto) {
        try {
            const newStudent = await this.studentService.createStudent(createStudentDto);
            return response.status(HttpStatus.CREATED).json({
                message: 'Student has been created successfully',
                newStudent,
            });
        } catch (err) {
            return response.status(err.status).json(err.response);
        }
    }

    @Put(':id')
    async updateStudent(@Res() response,
        @Param('id') id: string,
        @Body() updateStudentDto: UpdateStudentDto) {
        try {
            const existingStudent = await this.studentService.updateStudent(id, updateStudentDto);
            return response.status(HttpStatus.OK).json({
                message: 'Student has been successfully updated',
                existingStudent,
            });
        } catch (err) {
            return response.status(err.status).json(err.response);
        }
    }

    @Get()
    async getStudents(@Res() response) {
        try {
            const studentData = await this.studentService.getAllStudents();
            return response.status(HttpStatus.OK).json({
                message: 'All students data found successfully',
                studentData,
            });
        } catch (err) {
            return response.status(err.status).json(err.response);
        }
    }

    @Get(':id')
    async getStudent(@Res() response, @Param('id') id: string) {
        try {
            const existingStudent = await this.studentService.getStudent(id);
            return response.status(HttpStatus.OK).json({
                message: 'Student found successfully',
                existingStudent,
            });
        } catch (err) {
            return response.status(err.status).json(err.response);
        }
    }

    @Delete(':id')
    async deleteStudent(@Res() response, @Param('id') id: string) {
        try {
            const deletedStudent = await this.studentService.deleteStudent(id);
            return response.status(HttpStatus.OK).json({
                message: 'Student deleted successfully',
                deletedStudent,
            });
        } catch (err) {
            return response.status(err.status).json(err.response);
        }
    }
}

Creating Data Transfer Objects (DTOs)

Now, create the create-student.dto.ts file within a `dto` folder in your `student` module. Add all the properties along with their required validations.

import { IsNotEmpty, IsNumber, IsString, MaxLength } from "class-validator";

export class CreateStudentDto {
    @IsString()
    @MaxLength(30)
    @IsNotEmpty()
    readonly name: string;

    @IsNumber()
    @IsNotEmpty()
    readonly roleNumber: number;

    @IsNumber()
    @IsNotEmpty()
    readonly class: number;

    @IsString()
    @MaxLength(30)
    @IsNotEmpty()
    readonly gender: string;

    @IsNumber()
    @IsNotEmpty()
    readonly marks: number;
}

Next, create the update-student.dto.ts file in the `dto` folder. The UpdateStudentDto class will extend the CreateStudentDto class using PartialType. This makes all properties of CreateStudentDto optional, allowing you to selectively update student information.

import { PartialType } from '@nestjs/mapped-types';
import { CreateStudentDto } from './create-student.dto';
 
export class UpdateStudentDto extends PartialType(CreateStudentDto) {}

For the validations defined in create-student.dto.ts to work, you also need to register the validation pipe in your main.ts file.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common'; // Import ValidationPipe

async function bootstrap() {
   const app = await NestFactory.create(AppModule);
   app.useGlobalPipes(new ValidationPipe()); // Register the validation pipe
   await app.listen(3000);
}
bootstrap();

NestJS MongoDB Configuration in AppModule

Finally, configure your MongoDB connection by adding it to the `imports` array of your app.module.ts file. The `forRoot()` method accepts the same configuration object as `mongoose.connect()` from the Mongoose package. Here, we'll connect without providing a username and password in the URL for simplicity in a local setup.

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { StudentModule } from './student/student.module';

@Module({
  imports: [
  MongooseModule.forRoot('mongodb://localhost:27017',{dbName: 'studentdb'}),
  StudentModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Running Your NestJS CRUD Project

Now, start your NestJS application using the command:

npm run start

Your NestJS CRUD application with MongoDB using Mongoose will be accessible at `http://localhost:3000`. Once the project is running, you can verify the functionality of all GET, PUT, POST, and DELETE endpoints using a REST API client (like Postman or Insomnia).

You can find the complete implemented code on this GitHub link.

This article, "NestJS CRUD with MongoDB using Mongoose," has been adapted from various sources to provide a comprehensive guide.

Related Posts

Setting up NestJS with PostgreSQL, TypeORM, JWT auth, and Docker.

NestJS with PostgreSQL, TypeORM, JWT auth, and Docker

Discover the complete steps to set up and run a NestJS application with PostgreSQL, TypeORM, JWT authentication, and Docker, all managed via Darwin Terminal.

Read More
Installing and configuring Firebase, Node.js, npm, and React on macOS.

Firebase, Node.js and React on MacOS

Learn how to install and set up Firebase, Node.js, npm, and React on macOS to build and deploy full-stack applications.

Read More
CI/CD pipeline for React.js deployment using Visual Studio Code, GitHub Actions, and cPanel FTP.

Streamline React.js Deployment: CI/CD with VS Code Integration

Learn to set up robust CI/CD for React.js with VS Code, GitHub Actions, and FTP on cPanel. This guide covers everything.

Read More