NestJS: Building a REST API Web Service with MongoDB

In this step-by-step guide, we will explore how to create a REST API web service using NestJS and MongoDB by implementing simple REST API which will create a user and retrieve all users. By leveraging the power of NestJS and the flexibility of MongoDB, you’ll gain the knowledge and skills needed to build high-quality backends for your applications. Let’s dive in and discover the world of RESTful API development with NestJS and MongoDB.

Complete project code for nestjs project is available in github repository.

Table of Contents:

Prerequisites

To effectively follow this step-by-step guide and build a REST API web service with NestJS and MongoDB, it is recommended to have the following prerequisites:

  1. Basic understanding of javascript
  2. Understanding of RESTFul API concepts
  3. Installation of NodeJs, npm – You can download the latest version of NodeJs from the official NodeJs website and follow the installation instructions for your operating system
  4. Installation of MongoDB – Follow the instructions for installation of mongo from the official website
Setting Up the Project

To start building a REST API web service with NestJS and MongoDB, you need to set up a new NestJS project. Follow these steps to get started:

Install NestJS CLI: Open your terminal or command prompt and run the following command to install the NestJS Command Line Interface (CLI) globally on your machine:

$ npm install -g @nestjs/cli

Create a new NestJS project: Once the NestJS CLI is installed, navigate to the directory where you want to create your project and run the following command to generate a new NestJS project:

$ nest new nest-mongodb-api

This command creates a new directory named “nest-mongodb-api” with the boilerplate code and dependencies needed for a NestJS project.

Install Dependencies

@nestjs/mongoose and mongoose: These dependencies are used for integrating MongoDB with your NestJS application. mongoose is a popular MongoDB object modeling tool, and @nestjs/mongoose provides a NestJS module for seamless integration and interaction with MongoDB. These dependencies allow you to establish a connection to MongoDB, define data models, perform CRUD operations, and more. To install, type below command

npm install --save @nestjs/mongoose mongoose

@nestjs/swagger and swagger-ui-express: These dependencies enable the integration of Swagger with your NestJS application. Swagger is a powerful tool for designing, building, documenting, and testing APIs. @nestjs/swagger provides a module to generate Swagger documentation based on decorators in your code, and swagger-ui-express is a middleware that serves the generated Swagger UI, allowing you to explore and interact with your API endpoints visually. To install, type below command

npm install --save @nestjs/swagger swagger-ui-express

class validator: class-validator is a library that provides decorators and validation functions for validating data objects based on specified rules. It allows you to define validation rules using decorators in your DTO (Data Transfer Object) classes, helping ensure that the received data meets the expected criteria. To install, type below command

npm install --save class-validator

class-transformer: class-transformer is a companion library to class-validator that provides functionality for transforming plain JavaScript objects into instances of defined classes. It is used in conjunction with class-validator to transform and validate incoming request payloads by applying decorators and rules defined in the DTO classes. To install, type below command

npm install --save class-transformer
Project Structure

After the project is created, you’ll have a basic structure with several files and directories. Remove the generated files like app.controller.ts and app.service.ts from src folder. Create the following directory structure:

nestjs
  • src: This directory contains the source code of our NestJS application.
  • src/controllers: Controllers directory will contain the controller files of our NestJS application. Create user.controller.ts file in the directory.
  • src/dto: The dto directory holds the Data Transfer Object (DTO) files. Create user.dto.ts file in the directory.
  • src/services: Services services directory contains the service files of our NestJS application which is used to handle the business logics. Create user.service.ts file in the directory.
  • src/utils: Utils directory stores utility or helper files that provide common functions, reusable code snippets, or utility classes used throughout the application. Create validation.pipe.ts file in the directory.
  • src/models: Models directory typically holds the data model files of your application. These models define the structure and schema of the data that is stored or retrieved from the database. Create user.model.ts file in the directory.
Configuring MongoDB

As we had created our project directory structure next, we need to configure the connection by updating the app.module.ts file with the following code:

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UserController } from './controllers/user.controller';
import { UserService } from './services/user.service';
import { UserSchema } from './models/user.model';

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost/nest-mongodb-api'),
    MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),
  ],
  controllers: [UserController],
  providers: [UserService],
})
export class AppModule {}
Defining the Data Model

Now, lets define the User model which holds name, email and password. Code is as below:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { ApiProperty } from '@nestjs/swagger';
import mongoose from 'mongoose';

export type UserDocument = User & Document;

@Schema({ timestamps: true })
export class User {

  @ApiProperty()
  @Prop({ required: true })
  name: string;

  @ApiProperty()
  @Prop()
  email: String;

  @ApiProperty()
  @Prop()
  password: string;
}

export const UserSchema = SchemaFactory.createForClass(User);
Defining API Endpoints

In this example, we are going to define two endpoints: one for retrieving users and another for creating a new user. The userService instance is injected into the controller to handle the business logic. Now lets define the user controller apis as below:

import { Controller, Get, Post, Body } from '@nestjs/common';
import { UserService } from '../services/user.service';
import { User } from '../models/user.model';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post()
  async createUser(@Body() user: User): Promise<User> {
    return this.userService.createUser(user);
  }

  @Get()
  async findAllUsers(): Promise<User[]> {
    return this.userService.findAllUsers();
  }
}
Implementing the Service

Now we are going to implement the create user function which save the user to mongoDB and fetch all user function which will retrieve all the stored user objects. Go Open the file named user.service.ts and implement the service class:

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User } from '../models/user.model';

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

  async createUser(user: User): Promise<User> {
    const newUser = new this.userModel(user);
    return newUser.save();
  }

  async findAllUsers(): Promise<User[]> {
    return this.userModel.find().exec();
  }
}

Here, we inject the User model using @InjectModel from @nestjs/mongoose and defined methods for fetching users and creating a new user.

Adding Validation with DTOs

To ensure data consistency and validation, let’s create a Data Transfer Object (DTO). In the dto directory, create a file named user.dto.ts and add the below code:

import { IsString, IsEmail, MinLength, IsNotEmpty, ValidateNested } from 'class-validator';

export class UserDto {

  @IsNotEmpty()
  @IsString()
  name: string;

  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsString()
  @IsNotEmpty()
  @MinLength(8)
  password: string;
}

Update the user.controller.ts file to use the dto for request validation as below:

import { Controller, Get, Post, Body } from '@nestjs/common';
import { UserService } from '../services/user.service';
import { UserDto } from '../dto/user.dto';
import { User } from '../models/user.model';
import { ValidationPipe } from '../utils/validation.pipe';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post()
  async createUser(@Body(new ValidationPipe()) userDto: UserDto): Promise<User> {
    return this.userService.createUser(userDto);
  }

  @Get()
  async findAllUsers(): Promise<User[]> {
    return this.userService.findAllUsers();
  }
}

Same way, lets update the user.service.ts file to use the dto to transform it to User Mongoose model as below:

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User } from '../models/user.model';
import { UserDto } from '../dto/user.dto';

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

  async createUser(userDto: UserDto): Promise<User> {
    const newUser = new this.userModel(userDto);
    return newUser.save();
  }

  async findAllUsers(): Promise<User[]> {
    return this.userModel.find().exec();
  }
}

Add the below code in validation.pipe.ts file for validating request payload:

import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import { BadRequestException } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, metadata: ArgumentMetadata): Promise<any> {
    const { metatype } = metadata;
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToClass(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }

  private toValidate(metatype: any): boolean {
    const types: any[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}

The ValidationPipe class in NestJS serves as a custom pipe responsible for validating the incoming request payload, adhering to the defined validation rules specified in the DTO class. Let’s delve into the significant aspects of the ValidationPipe:

  • The transform method acts as the main entry point of the ValidationPipe and is executed upon receiving a request. It accepts two parameters:
    • value: Represents the value of the request payload.
    • metadata: Provides contextual information about the transformation, such as the value’s type.
  • The toValidate method determines whether the metatype (value type) necessitates validation. It excludes intrinsic JavaScript types like String, Boolean, Number, Array, and Object from the validation process.
  • The plainToClass function is employed to convert the plain object (request payload) into an instance of the specified class (metatype), enabling seamless validation.
  • The validate function performs the actual validation of the transformed object (UserDto instance) based on the decorators and rules defined in the DTO class.
  • The errors variable captures the validation errors returned by the validate function, allowing for further handling or reporting.
  • If validation errors are detected, the BadRequestException exception, imported from the @nestjs/common package, is thrown to indicate the presence of invalid data.
Testing the API

Lets run and test the API’s. To run the project use npm run start command in the command line. After successful startup, test the create user and retrieve all users API using then below curl request by importing it in postman.

Create User API:

curl --location 'http://localhost:3000/users' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "2eqqe",
    "email": "test02@gmail.com",
    "password": "test0200"
}'

Retrieve All Users API:

curl --location --request GET 'http://localhost:3000/users' \
--header 'Content-Type: application/json'
Integrating Swagger

As our APIs are working fine, lets go ahead and implement swagger for documenting our API’s. In main.ts file modify as below,

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder().setTitle('nest-mongodb-api')
                      .setDescription("nest mongodb api")
                      .setVersion('v1')
                      .addTag('users')
                      .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api-docs', app, document);
  
  await app.listen(3000);
}
bootstrap();

Lets update our controller classes with Swagger decorators to specify endpoints and request/response models. After modification user.controller.ts should look like below:

import { Controller, Get, Post, Body } from '@nestjs/common';
import { UserService } from '../services/user.service';
import { UserDto } from '../dto/user.dto';
import { User } from '../models/user.model';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { ValidationPipe } from '../utils/validation.pipe';

@ApiTags('users')
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @ApiOperation({ summary: 'Create User' })
  @ApiResponse({ status: 201, description: 'Returns created user', type: User })
  @Post()
  async createUser(@Body(new ValidationPipe()) userDto: UserDto): Promise<User> {
    return this.userService.createUser(userDto);
  }

  @ApiOperation({ summary: 'Get all users' })
  @ApiResponse({ status: 200, description: 'Creates a new user', type: User })
  @Get()
  async findAllUsers(): Promise<User[]> {
    return this.userService.findAllUsers();
  }
}

Update user.dto.ts file by adding @ApiProperty to properties like name, password and email. Complete user.dto.ts should look like below:

import { IsString, IsEmail, MinLength, IsNotEmpty, ValidateNested } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class UserDto {

  @ApiProperty()
  @IsNotEmpty()
  @IsString()
  name: string;

  @ApiProperty()
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @ApiProperty()
  @IsString()
  @IsNotEmpty()
  @MinLength(8)
  password: string;
}
Testing the API swagger doc

Start lets our NestJS application by executing npm run start command and access the Swagger UI at /api-docs endpoint. For example, visit http://localhost:3000/api-docs to explore and test your API using Swagger UI.

1 Response

Leave a Reply

Your email address will not be published. Required fields are marked *

Post comment