CRUD (or Create, Read, Update, Delete) applications are quite common in the world of software development. In this post, we learn how to build a NestJS MongoDB CRUD application.

Basically, NestJS supports two methods for integrating with MongoDB database. We can use TypeORM module which has a connector for MongoDB. Alternatively, we can use Mongoose. Incidentally, Mongoose happens to be the most popular modeling tool for MongoDB

In case you are completely new to NestJS, I would recommend to start with NestJS Basics and follow the series of NestJS posts. If you are already aware of NestJS framework, you can continue right on with this post.

1 – Install NestJS Mongoose Package

The first step to create a NestJS MongoDB CRUD application using Mongoose is to install NestJS Mongoose package in our NestJS project. You can use the below command for the same.

npm install --save @nestjs/mongoose mongoose

Basically, we are installing the standard mongoose package and the NestJS mongoose wrapper for making things work together seamlessly.

2 – NestJS MongoDB Connection Configuration

For this example, I have a MongoDB server running on my local machine. You can download MongoDB Community Edition from this link.

We can then configure the connection in the app.module.ts file.

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

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

Basically, here we use the forRoot() method to establish the connection in the imports section. This method accepts the same configuration object as mongoose.connect().

As you can see, we are connecting to the local MongoDB instance. Also, we are connecting to the demo database. At the time of application startup NestJS will automatically create this database if it does not exist already.

3 – NestJS MongoDB Model Injection

Next step is to create the appropriate models.

Basically, in Mongoose everything is derived from a schema. Each schema maps to a MongoDB collection. In other words, schema defines how the collection should look like.

Schemas define models. These models are responsible for creating, updating and reading documents from the collection.

We can create schemas using NestJS decorators or with Mongoose manually. However, using decorators to create schema greatly reduces boilerplate code. In other words, it improves code readability because of the declarative approach.

Let us define one sample schema for our example.

import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Document } from "mongoose";

export type BookDocument = Book & Document;

@Schema()
export class Book {

    @Prop()
    name: string;

    @Prop()
    author: string;

    @Prop()
    publishYear: number;
}

export const BookSchema = SchemaFactory.createForClass(Book);

The @Schema decorator fixes the class as a schema definition. Basically, this decorator maps our Book class to an underlying Book collection in the demo database. The collection name will have an s at the end. In other words, this means that the collection for the Book class will be named books. The decorator itself accepts a single argument – the schema options object. You can check out the various options available at the below official link.

Next, we have the @Prop() decorator. Basically, this decorator defines a property within the document. For example, in the above schema, we have the name property, the author property and the publishYear. The types for this properties are automatically inferred using Typescript metadata and class reflection.

Once the schema is defined, 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.

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Book, BookSchema } from './schemas/book.schema';

@Module({
  imports: [MongooseModule.forRoot('mongodb://localhost/demo'),
            MongooseModule.forFeature([{name: Book.name, schema: BookSchema}])],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

As you can see, we added another entry in the imports array. We use the forFeature() method to register the models in the current scope.

4 – Creating the BookService

Next step in our CRUD application is to create a service class. This service class acts as a bridge between the request handlers and the database.

import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { Book, BookDocument } from "src/schemas/book.schema";

@Injectable()
export class BookService {

    constructor(@InjectModel(Book.name) private bookModel: Model<BookDocument>) {}
    
    async create(book: Book): Promise<Book> {
        const newBook = new this.bookModel(book);
        return newBook.save();
    }

    async readAll(): Promise<Book[]> {
        return await this.bookModel.find().exec();
    }

    async readById(id): Promise<Book> {
        return await this.bookModel.findById(id).exec();
    }

    async update(id, book: Book): Promise<Book> {
        return await this.bookModel.findByIdAndUpdate(id, book, {new: true})
    }

    async delete(id): Promise<any> {
        return await this.bookModel.findByIdAndRemove(id);
    }
}

We annotate the BookService class with @Injectable() decorator. Basically, this means we can inject it into other classes using the principles of dependency injection. You can read more about it in this detailed post about NestJS Providers.

In the constructor of the class we basically inject the BookModel. Here, we use the @InjectModel() decorator. Note that this is only possible after we have registered the schema in the app module configuration.

In the service class, we basically implement the methods to create, read, update and delete a book document from the underlying books collection. We use the standard methods available with the BookModel object to perform these basic operations. For demo purposes, I have kept the logic as simple as possible.

Lastly, we make the BookService available in the context by adding it in the app module.

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Book, BookSchema } from './schemas/book.schema';
import { BookService } from './services/book.service';

@Module({
  imports: [MongooseModule.forRoot('mongodb://localhost/demo'),
            MongooseModule.forFeature([{name: Book.name, schema: BookSchema}])],
  controllers: [AppController],
  providers: [AppService, BookService],
})
export class AppModule {}

Basically, we add the BookService in the providers array.

5 – Creating the BookController

The last piece of the puzzle is to create appropriate request handlers to perform the CRUD operations. Basically, we have to create a controller. You can read more about them in this detailed post on NestJS Controllers.

However, for this example, we have a basic controller as below:

import { Body, Controller, Delete, Get, HttpStatus, Param, Post, Put, Res } from "@nestjs/common";
import { Book } from "src/schemas/book.schema";
import { BookService } from "src/services/book.service";

@Controller('books')
export class BookController {
    constructor(private readonly bookService: BookService){}

    @Post()
    async createBook(@Res() response, @Body() book: Book) {
        const newBook = await this.bookService.create(book);
        return response.status(HttpStatus.CREATED).json({
            newBook
        })
    }

    @Get()
    async fetchAll(@Res() response) {
        const books = await this.bookService.readAll();
        return response.status(HttpStatus.OK).json({
            books
        })
    }

    @Get('/:id')
    async findById(@Res() response, @Param('id') id) {
        const book = await this.bookService.readById(id);
        return response.status(HttpStatus.OK).json({
            book
        })
    }

    @Put('/:id')
    async update(@Res() response, @Param('id') id, @Body() book: Book) {
        const updatedBook = await this.bookService.update(id, book);
        return response.status(HttpStatus.OK).json({
            updatedBook
        })
    }

    @Delete('/:id')
    async delete(@Res() response, @Param('id') id) {
        const deletedBook = await this.bookService.delete(id);
        return response.status(HttpStatus.OK).json({
            deletedBook
        })
    }
}

As you can see above, we have injected the BookService in the constructor. At runtime, NestJS will provide an instance of the BookService to the controller.

Next, we implement standard POST, GET, PUT and DELETE request handlers to perform the various operations. Within the request handler implementation, we simply call the appropriate service method.

Lastly, we need to register the BookController in the current context.

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { BookController } from './controllers/book.controller';
import { Book, BookSchema } from './schemas/book.schema';
import { BookService } from './services/book.service';

@Module({
  imports: [MongooseModule.forRoot('mongodb://localhost/demo'),
            MongooseModule.forFeature([{name: Book.name, schema: BookSchema}])],
  controllers: [AppController, BookController],
  providers: [AppService, BookService],
})
export class AppModule {}

Here, we add the BookController in the controllers array.

If we now start the application, we will be able to access the endpoints at http://localhost:3000 and perform the CRUD operations as required.

Conclusion

With this, we have successfully created a NestJS MongoDB CRUD application using Mongoose as the connecting glue between our application and the database.

We looked at how to create schemas and how to use it in our application service layer to interact with the database.

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


4 Comments

Mohammad · January 9, 2022 at 10:26 am

Great job.

    Saurabh Dashora · January 10, 2022 at 2:28 am

    Thanks for the kind feedback!

Felipe · April 19, 2022 at 5:27 pm

I’m using NestJs v8. When i use the @Res response, it throws me an error
“Error: This is caused by either a bug in Node.js or incorrect usage of Node.js internals.”.
am i missingsomething?

    Saurabh Dashora · April 20, 2022 at 1:15 am

    Hi Felipe, not sure what could be causing the issue. Can you share the Github repo for your code?

Leave a Reply

Your email address will not be published.