DEV Community

Manthan Ankolekar
Manthan Ankolekar

Posted on

Building a CRUD Application with NestJS and MongoDB

In this blog, we’ll explore how to build a CRUD (Create, Read, Update, Delete) application using NestJS, a progressive Node.js framework, along with MongoDB as the database. We’ll walk through key concepts, structure, and code snippets from the project files.

Why NestJS?

NestJS is a TypeScript-based framework for building scalable server-side applications. It leverages the full power of Node.js and incorporates elements of OOP (Object-Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming).

Setting Up the Project

Creating a New NestJS Project

To create a new NestJS project, you can use the NestJS CLI. If you haven’t installed the CLI yet, you can do so using the following command:

npm install -g @nestjs/cli
Enter fullscreen mode Exit fullscreen mode

Next, create a new NestJS project by running:

nest new crud-nestjs
Enter fullscreen mode Exit fullscreen mode

This command will generate a new NestJS project with the name crud-nestjs. Navigate to the project directory:

cd crud-nestjs
Enter fullscreen mode Exit fullscreen mode

Installing Dependencies

The first step is setting up your NestJS project. We’ll also include MongoDB through @nestjs/mongoose and mongoose for database interactions. Install the required dependencies:

npm install @nestjs/core @nestjs/common @nestjs/mongoose mongoose
Enter fullscreen mode Exit fullscreen mode

Next, install the necessary dev dependencies:

npm install --save-dev prettier @nestjs/schematics
Enter fullscreen mode Exit fullscreen mode

Your package.json should look something like this:

{
  "name": "crud-nestjs",
  "version": "0.0.1",
  "description": "",
  "author": "",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
  "dependencies": {
    "@nestjs/common": "^10.0.0",
    "@nestjs/config": "^3.2.0",
    "@nestjs/core": "^10.0.0",
    "@nestjs/mongoose": "^10.0.2",
    "@nestjs/platform-express": "^10.0.0",
    "mongoose": "^8.0.3",
    "reflect-metadata": "^0.1.13",
    "rxjs": "^7.8.1"
  },
  "devDependencies": {
    "@nestjs/cli": "^10.0.0",
    "@nestjs/schematics": "^10.0.0",
    "@nestjs/testing": "^10.0.0",
    "@types/express": "^4.17.17",
    "@types/jest": "^29.5.2",
    "@types/node": "^20.3.1",
    "@types/supertest": "^2.0.12",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "eslint": "^8.42.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-prettier": "^5.0.0",
    "jest": "^29.5.0",
    "prettier": "^3.0.0",
    "source-map-support": "^0.5.21",
    "supertest": "^6.3.3",
    "ts-jest": "^29.1.0",
    "ts-loader": "^9.4.3",
    "ts-node": "^10.9.1",
    "tsconfig-paths": "^4.2.0",
    "typescript": "^5.1.3"
  },
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node"
  }
}
Enter fullscreen mode Exit fullscreen mode

MongoDB Connection

We’ll use MongoDB as our database. Configure your environment variables to include the MongoDB credentials. Here’s an example .env file:

MONGODB_USER = "yourMongoUser"
MONGODB_PASSWORD = "yourMongoPassword"
Enter fullscreen mode Exit fullscreen mode

Then, in app.module.ts, you can connect to MongoDB using MongooseModule:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';
import { UsersModule } from './users/users.module';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    UsersModule,
    ConfigModule.forRoot(),
    MongooseModule.forRoot(
      `mongodb+srv://${process.env.MONGODB_USER}:${process.env.MONGODB_PASSWORD}@cluster0.re3ha3x.mongodb.net/nestjs-crud-app`,
    ),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Defining the User Schema

A user entity in MongoDB is represented by a schema. Here’s the UserSchema defined in users.model.ts:

import * as mongoose from 'mongoose';

export const UserSchema = new mongoose.Schema({
  firstName: { type: String, required: true },
  lastName: { type: String, required: true },
});

export interface User extends mongoose.Document {
  id: string;
  firstName: string;
  lastName: string;
}
Enter fullscreen mode Exit fullscreen mode

Building the Controller

The controller handles incoming requests and returns responses. In the users.controller.ts, we define routes for all CRUD operations:

import {
  Controller,
  Post,
  Body,
  Get,
  Param,
  Patch,
  Delete,
} from '@nestjs/common';

import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  async addUser(
    @Body('firstName') userFirstName: string,
    @Body('lastName') userLastName: string,
  ) {
    const generatedId = await this.usersService.insertUser(
      userFirstName,
      userLastName,
    );
    return { message: 'User added successfully', id: generatedId };
  }

  @Get()
  async getAllUsers() {
    const users = await this.usersService.getUsers();
    return { message: 'All users retrieved successfully', users };
  }

  @Get(':id')
  getUser(@Param('id') userId: string) {
    return this.usersService.getSingleUser(userId);
  }

  @Patch(':id')
  async updateUser(
    @Param('id') userId: string,
    @Body('firstName') userFirstName: string,
    @Body('lastName') userLastName: string,
  ) {
    const updatedUser = await this.usersService.updateUser(
      userId,
      userFirstName,
      userLastName,
    );
    return { message: 'User updated successfully', updatedUser };
  }

  @Delete(':id')
  async removeUser(@Param('id') userId: string) {
    await this.usersService.deleteUser(userId);
    return { message: 'User deleted successfully' };
  }
}
Enter fullscreen mode Exit fullscreen mode

Service Layer

The service handles the business logic, interacting with the database. Below is an example from users.service.ts:

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';

import { User } from './users.model';

@Injectable()
export class UsersService {
  constructor(@InjectModel('User') private readonly userModel: Model<User>) {}

  async insertUser(firstName: string, lastName: string) {
    const newUser = new this.userModel({
      firstName,
      lastName: lastName,
    });
    const result = await newUser.save();
    return result.id as string;
  }

  async getUsers() {
    const users = await this.userModel.find().exec();
    return users.map((user) => ({
      id: user.id,
      firstName: user.firstName,
      lastName: user.lastName,
    }));
  }

  async getSingleUser(userId: string) {
    const user = await this.findUser(userId);
    return {
      id: user.id,
      firstName: user.firstName,
      lastName: user.lastName,
    };
  }

  async updateUser(userId: string, firstName: string, lastName: string) {
    const updatedUser = await this.findUser(userId);
    if (firstName) {
      updatedUser.firstName = firstName;
    }
    if (lastName) {
      updatedUser.lastName = lastName;
    }
    const savedUser = await updatedUser.save();
    return savedUser;
  }

  async deleteUser(userId: string) {
    const result = await this.userModel.deleteOne({ _id: userId }).exec();
    if (result.deletedCount === 0) {
      throw new NotFoundException('Could not find user.');
    }
  }

  private async findUser(id: string): Promise<User> {
    let user;
    try {
      user = await this.userModel.findById(id).exec();
    } catch (error) {
      throw new NotFoundException('Could not find user.');
    }
    if (!user) {
      throw new NotFoundException('Could not find user.');
    }
    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

Users Module

The UsersModule is a feature module that encapsulates the user-related components. Here’s an example from users.module.ts:

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UserSchema } from './users.model';

@Module({
  imports: [MongooseModule.forFeature([{ name: 'User', schema: UserSchema }])],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}
Enter fullscreen mode Exit fullscreen mode

App Controller

The AppController is the main controller that handles the root route. Here’s an example from app.controller.ts:

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}
Enter fullscreen mode Exit fullscreen mode

App Service

The AppService is the main service that returns a message when the API is running. Here’s an example from app.service.ts:

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'API is running!';
  }
}
Enter fullscreen mode Exit fullscreen mode

Main File

The main file, main.ts, is the entry point for the application. Here’s an example:

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Running the Application

Finally, to run your NestJS application, use the following command:

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

Your CRUD operations are now live and accessible through the defined routes.

Conclusion

This blog covered how to set up a simple CRUD application using NestJS and MongoDB. By following this pattern, you can easily extend it to manage more complex resources and integrate additional features like authentication, authorization, and error handling.

Happy coding!


Feel free to customize the content as needed. Let me know if you have any questions or need further assistance. Good luck with your project! 🚀

Exploring the Code

Visit the GitHub repository to explore the code in detail.


Top comments (0)