CRUD application with NestJS and MongoDB using Mongoose

Niranjan Gawali
Globant
Published in
9 min readApr 12, 2022

Introduction:

NestJS and MongoDB are superior combinations to implement a scalable backend. In this blog, We will learn how to create a CRUD application with NestJS and MongoDB using Mongoose as an ORM.

The prerequisite to implementing this CRUD application,

  • We need the MongoDB database, either it should be installed locally or the cloud MongoDB database can also be used. In this example, I have a MongoDB server running on my local machine. If required you can download MongoDB Community Edition from this link.
  • NestJS CLI should already be installed, if not please install using the following command.
npm i @nestjs/cli

After installation is done, we verify if it's installed or not by using the below command it should return the installed NestJS CLI.

nest --version

Installation:

To start with, create a new NestJS project and install MongoDB dependencies by executing the following commands.

nest new nestjs-mongodb-crud
npm install --save @nestjs/mongoose mongoose

NestJS MongoDB Configuration:

Once project creation is done, We will add the MongoDB connection in the imports of the app.module.ts file. here forRoot() method accepts the same configuration object as mongoose.connect() from the Mongoose package.

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

If username and password are present then we also need to mention that in the connection URL.

@Module({
imports: [MongooseModule.forRoot('mongodb://<username>:<password>@localhost:27017',{dbName: 'studentdb'}),
],
})
export class AppModule {}

Create Mongoose Schema:

After the database connection is added, the next part is to create a mongoose model. Create a new folder schema and create the new schema file student.schema.ts in it. Then add the required student properties to the file. After adding the properties file will look as below.

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);

In the above code, we have used two NestJS Decorators:

1> Schema: This decorator fixes the class as the schema definition. here whatever name we give this class will appear as the name of the collection. This will map our Student class to the MongoDB Student collection

2> Prop: Basically, this decorator defines a property within the document. For example, in the above schema, we have a total of 5 properties like name, roleNumber, class, gender and marks. Using Typescript’s metadata and class reflection, the types for these properties are automatically inferred.

After the schema creation is done, then we need to add it to the module-level configuration. In other words, we have to specify the presence of this schema in the context of the application. We need to add an entry in the imports array. We use the forFeature() method to register the models in the current scope. After modification app.module.ts will look as below.

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { StudentSchema } from './schema/student.schema';
@Module({
imports: [MongooseModule.forRoot('mongodb://localhost:27017/studentdb'),
MongooseModule.forFeature([{ name: 'Student', schema: StudentSchema }])],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

Create an Interface:

Once mongoose model creation is done now we can create an interface for the student schema, it defines how our data object structure will look like. Now create a new folder interface and create the student.interface.ts file in it. Here interface will inherit property from the mongoose Document class. All the properties are read-only so those can’t be modified.

import { Document } from 'mongoose';export interface IStudent extends Document{
readonly name: string;
readonly roleNumber: number;
readonly class: number;
readonly gender: string;
readonly marks: number;
}

Creating the DTO files:

Before proceeding with creating the DTO files some dependencies are needed to be installed. class-validator and class-transformer are required to implement DTO level validations. Install the following packages.

npm install class-validator --save
npm install class-transformer --save

So now we will proceed towards creating the DTO files these are useful for defining the object model. DTO file can also be used to define the swagger property. Now create a new folder dto and create-student.dto.ts file in it. Add all the properties along with the required validations. After adding the properties to the create-student.dto.ts file will look as follows.

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;
}

Before creating the DTO file to update students make sure the following package is present, it will allow us to use the existing DTO class properties

npm i @nestjs/mapped-types --save

Now we can create the update-student.dto.ts file in the dto folder. In this file UpdateStudentDto will extend the CreateStudentDto class using PartialType, it makes properties of CreateStudentDto optional, and it can be utilized in the UpdateStudentDto class as per the need.

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

For validations mentioned in the create-student.dto.ts file to work, we also need to register the validation pipe in the main.ts file. After the modification, the main file will look as below.

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();

Creating the Service:

Now the next step is to create a service class. This service class acts as a bridge between the request handlers and the database.
Create a new folder service and create the student.service.ts file in it using the following command.

nest generate service student

Once the service file generation is done then we will implement the methods to create, read, update and delete a student document from the underlying students collection. We will use standard methods available to the studentModel to implement the CRUD operations. After implementing the methods, the student service file will look as below.

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { CreateStudentDto } from 'src/dto/create-student.dto';
import { IStudent } from 'src/interface/student.interface';
import { Model } from "mongoose";
import { UpdateStudentDto } from 'src/dto/update-student.dto';
@Injectable()
export class StudentService {
constructor(@InjectModel('Student') private studentModel:Model<IStudent>) { }async createStudent(createStudentDto: CreateStudentDto): Promise<IStudent> {
const newStudent = await new this.studentModel(createStudentDto);
return newStudent.save();
}
async updateStudent(studentId: string, updateStudentDto: UpdateStudentDto): Promise<IStudent> {
const existingStudent = await this.studentModel.findByIdAndUpdate(studentId, updateStudentDto, { new: true });
if (!existingStudent) {
throw new NotFoundException(`Student #${studentId} 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(studentId: string): Promise<IStudent> {
const existingStudent = await this.studentModel.findById(studentId).exec();
if (!existingStudent) {
throw new NotFoundException(`Student #${studentId} not found`);
}
return existingStudent;
}
async deleteStudent(studentId: string): Promise<IStudent> {
const deletedStudent = await this.studentModel.findByIdAndDelete(studentId);
if (!deletedStudent) {
throw new NotFoundException(`Student #${studentId} not found`);
}
return deletedStudent;
}
}

The StudentService class is present with @Injectible() decorator. It means we can inject the service class into the other classes using the principles of dependency injection.
In the constructor the studenModel is injected into the service, @InjectModel decorator is used for the injection operation. This injection is only possible after the schema is registered in the app module configuration.
We also need to make StudentService available in the context by adding it to the app module. Basically, we add it to the providers array.

Creating the Controller:

Now the last step is to implement the controller to create appropriate request handlers to perform the CRUD operations. Create a new folder controller and create a student.controller.ts file in it by executing the following command.

nest generate controller student

Once the controller file is generated, in the constructor we will inject the StudentService class At the runtime, NestJS will provide an instance of the StudentService class to the controller to access the methods implemented in the service file.

We implement the standard POST, PUT, DELETE and GET request handlers and perform various operations in it using the StudentService instance to call the appropriate method.

After implementing the methods, the controller file will look as below.

import { Body, Controller, Delete, Get, HttpStatus, Param, Post, Put, Res } from '@nestjs/common';
import { CreateStudentDto } from 'src/dto/create-student.dto';
import { UpdateStudentDto } from 'src/dto/update-student.dto';
import { StudentService } from 'src/service/student/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(HttpStatus.BAD_REQUEST).json({
statusCode: 400,
message: 'Error: Student not created!',
error: 'Bad Request'
});
}
}
@Put('/:id')
async updateStudent(@Res() response,@Param('id') studentId: string,
@Body() updateStudentDto: UpdateStudentDto) {
try {
const existingStudent = await this.studentService.updateStudent(studentId, 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') studentId: string) {
try {
const existingStudent = await
this.studentService.getStudent(studentId);
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') studentId: string)
{
try {
const deletedStudent = await this.studentService.deleteStudent(studentId);
return response.status(HttpStatus.OK).json({
message: 'Student deleted successfully',
deletedStudent,});
}catch (err) {
return response.status(err.status).json(err.response);
}
}
}

At last, we need to make StudentController available in the context by adding it to the app module. Basically, we add it to the controllers array.

At final app.module.ts file will look as follows:

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { StudentController } from './controller/student/student.controller';
import { StudentSchema } from './schema/student.schema';
import { StudentService } from './service/student/student.service';
@Module({
imports:[MongooseModule.forRoot('mongodb://localhost:27017/studentdb'),
MongooseModule.forFeature([{ name: 'Student', schema: StudentSchema }])],
controllers: [AppController,StudentController],
providers: [AppService,StudentService],
})
export class AppModule {}

After all, files are created final code structure will be as follows.

NestJS Project Structure
Project Structure

Now we start the NestJS application by using the command npm run start, we will be able to access the CRUD endpoints at http://localhost:3000.

REST API client execution results:

Now once our project is running now we can verify the working of the endpoints with the help of the REST API client. For all GET, PUT, POST and DELETE endpoints we get the following success response.

1> Create a user:

Create a user
Create a user
curl --location --request POST 'http://localhost:3000/student/' \
--header 'Content-Type: application/json' \
--data-raw '{
"name":"Jack",
"roleNumber": 101,
"class": 10,
"gender": "Male",
"marks": 65
}'

2> Get all users:

Get all users
Get all users
curl --location --request GET 'http://localhost:3000/student/'

3> Update a user by id:

Update a user by id
Update a user by id
curl --location --request GET 'http://localhost:3000/student/624b5bce68ff4ddf506dfeba'

4> Get a user by id:

Get a user by id
Get a user by id
curl --location --request PUT 'http://localhost:3000/student/624b5bce68ff4ddf506dfeba' \
--header 'Content-Type: application/json' \
--data-raw '{
"class": 11,
"marks": 70
}'

5> Delete a user by id:

Delete a user by id
curl --location --request DELETE 'http://localhost:3000/student/624b5bce68ff4ddf506dfeba'

Conclusion:

By following the procedures documented above we have successfully created the CRUD application using NestJS and MongoDB. We have used mongoose as ORM to connect the application with the database. We have also checked the working of the created endpoints using the REST API client and verified the successful response. You can check the implemented code on this link.

If you have any comments or queries about this, please feel free to mention them in the comments section below.

--

--