DEV Community

Cover image for Build a CRUD API using NestJS, MongoDB, Docker & Docker Compose
Mangabo Kolawole
Mangabo Kolawole

Posted on • Originally published at Medium

Build a CRUD API using NestJS, MongoDB, Docker & Docker Compose

NestJS is a framework built upon Nodejs that helps developers build scalable applications with a big emphasis on developer experience 💖. In this article, I'll show you how to build a Menu Restaurant API app from scratch with NestJS and MongoDB. The Docker part is optional but it'll be useful if you want to learn something new 😎.

The REST API we will build will have the following endpoints:

Endpoint HTTP Method Description
/menus POST Create a menu.
/menus GET Get the list of menus.
/menus/menu_id GET Retrieve data about a menu.
/menus/menu_id PUT Modify data about a menu.
/menus/menu_id DELETE Delete a menu.

Great, let's dive in.🤟‍

Project Setup

NestJS provides a CLI tool that can help scaffold a project very quickly. If you don't have the Nest CLI on your machine, use the following command to install it:

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

After the installation, create a new project with the following command:

nest new restaurant-menu
Enter fullscreen mode Exit fullscreen mode

The CLI will prompt some options to choose from for the package manager. I usually work with yarn or pnpm but feel free to change them according to your needs and preferences :).

Creating NestJS project

After the creation of the project, we need to install some tools for MongoDB integration but also validation for the REST API. NestJS is actually pretty easy to integrate with SQL or NoSQL databases and the ecosystem provides an interesting integration when working with Mongoose, the most popular MongoDB object modeling tool.

yarn add @nestjs/mongoose mongoose
Enter fullscreen mode Exit fullscreen mode

And in the src/app.module.ts, connect the application to the database.

// src/app.module.ts

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

@Module({
  imports: [MongooseModule.forRoot(mongodb://lcoalhost:27017/Restaurant
)],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Enter fullscreen mode Exit fullscreen mode

Finally, add the following packages for client payload validation in the REST API.

yarn add class-validator class-transformer
Enter fullscreen mode Exit fullscreen mode

Great! We have nearly everything ready and we can now start writing the CRUD logic of the application.

Creating the REST API for the Menus

Before writing the code, let's quickly understand how NestJS works under the hood, as it will help with the next steps that will be automated with the Nest CLI 👀.
The NestJS architecture is based on three main components:

  • Controllers => The purpose of the controller is to receive requests from HTTP clients and return an adequate response to the client. NestJS integrate a routing mechanism that directly controls which controller handles which requests.
  • Service Layer => The service layer handles everything that concerns the business logic. This can concern for example the CRUD operations that should happen on the database.
  • Data Access Layer => The Data Access Layer provides logic to access data stored in a data store of some kind. In this article, the Data Access Layer is represented by Mongoose.

With the different layers explored, we have to also describe the folder architecture of the default NestJS application as everything that is added to the project is kind of plugged into this architecture.

src
| — app.controller.spec.ts
| — app.controller.ts
| — app.module.ts
| — app.service.ts
| — main.ts
Enter fullscreen mode Exit fullscreen mode
  • app.controller.ts: This file contains the controller with a single route.
  • app.controller.spec.ts: This file contains the unit tests for the controller.
  • app.module.ts: This file practically bundles all the controllers and providers of the application together.
  • app.service.ts: This file will include methods that will perform a certain operation. For example: Retrieving a user.
  • main.ts: This is the entry file of the application that will take in the module bundle and create a Nest application instance using the NestFactory provided by Nest.

So, if we have to add the routes for the Menu resource in the REST API, we need a controller to define the methods and routes, a module file to bundle the controller of the Menu resource but also a service for the CRUD operations that will happen on the Menu document.

The Nest CLI provides an interesting tool we can use to generate all these files for us and thus make the development faster 🚀.

cd src && nest g resource
Enter fullscreen mode Exit fullscreen mode

The command above will prompt some options in the terminal.

Generating Menu resource

Note

nest g resource command not only generates all the NestJS building blocks (module, service, controller classes) but also an entity class, DTO classes as well as the testing (.spec) files. From the docs

The command will generate all the building blocks for the Menu resource and will automatically make the configuration in the main module file app.module.ts. Next step, ensure to have the following file architecture in the newly created menus directory. You will need to create the entities directory used to describe the shape of menus objects and the schemas directory where we will write the Data Access Layer logic.

├── dto
│   ├── create-menu.dto.ts
│   └── update-menu.dto.ts
├── entities
│   └── menu.entity.ts
├── menus.controller.spec.ts
├── menus.controller.ts
├── menus.module.ts
├── menus.service.spec.ts
├── menus.service.ts
└── schemas
    └── menu.schema.ts
Enter fullscreen mode Exit fullscreen mode

Here's the content for the menu.entities.ts file.

// src/entities/menu.entities.ts

export class Menu {
  name: string;
  description: string;
  price: string;
  created: Date;
  updated: Date;
}
Enter fullscreen mode Exit fullscreen mode

This is the shape of a Menu object. Let's add the schemas.

Adding the Menu model

In the src/schemas/menu.schema.ts, add the following code.

// src/schemas/menu.schema.ts

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

export type MenuDocument = Menu & Document;

@Schema({
  timestamps: { createdAt: 'created', updatedAt: 'updated' },
})
export class Menu {
  @Prop({ required: true })
  name: string;

  @Prop({ required: true })
  description: string;

  @Prop({ required: true })
  price: number;
}

export const MenuSchema = SchemaFactory.createForClass(Menu);
Enter fullscreen mode Exit fullscreen mode

In the code above, we are adding the properties of the Class Menu or with a better context, we are defining the fields of the Menu model/document that will be created in the MongoDB. With the model written, we can now modify the code for the create-menu DTO.

Adding validation in the REST API

A DTO or Data Transfer Object is an object that defines how the data will be sent over the network. A DTO can be written using interfaces or simple classes. By default, the nest -g resource command will generate DTO as classes as recommended by the NestJS documentation. Then if we want to add validation in the REST API, we should add them on the DTO logic of the Menu resource.
In the src/menus/dto/create-menu.dto.ts, add the following lines.

// src/menus/dto/create-menu.dto.ts

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

export class CreateMenuDto {
  @IsString()
  @IsNotEmpty()
  @MinLength(5)
  name: string;

  @IsString()
  @IsNotEmpty()
  @MinLength(5)
  @MaxLength(300)
  description: string;

  @IsNumber()
  @IsNotEmpty()
  price: number;
}
Enter fullscreen mode Exit fullscreen mode

We are using decorators from the class-validator package and defining the validation rules for the name, description, and price fields. But technically, a menu object also comes withcreatedand updated fields. MongoDB already handles the modification of the values for these fields. To avoid clients passing invalid values for these fields, we must add a configuration in the src/main.ts file to exclude properties defined without decorators in the DTO. We will also take this occasion to add configuration for the validation pipe.

// src/main.ts

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

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

  app.useGlobalPipes(new ValidationPipe({ whitelist: true }));

  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

With the whitelist option set to true, ValidationPipe will automatically remove all non-whitelisted properties, where non-whitelisted means properties without any validation decorators or not present in the DTO.

There is no need to modify the update-menu.dto.ts file as it is partially extended from the CreateMenuDto.

import { PartialType } from '@nestjs/mapped-types';
import { CreateMenuDto } from './create-menu.dto';

export class UpdateMenuDto extends PartialType(CreateMenuDto) {}
Enter fullscreen mode Exit fullscreen mode

Adding Menu Services

The services contain the business logic of the application. Inside the src/menus/menus.services.ts file, we will add the CRUD methods that we will use on the controllers to interact with the Menu model.

// src/menus/menus.services.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { CreateMenuDto } from './dto/create-menu.dto';
import { UpdateMenuDto } from './dto/update-menu.dto';
import { Menu, MenuDocument } from './schemas/menu.schema';

@Injectable()
export class MenusService {
  constructor(
    @InjectModel(Menu.name) private readonly menuModel: Model<MenuDocument>,
  ) {}

  async create(createMenuDto: CreateMenuDto): Promise<MenuDocument> {
    const menu = new this.menuModel(createMenuDto);
    return menu.save();
  }

  async findAll(): Promise<MenuDocument[]> {
    return this.menuModel.find();
  }

  findOne(id: string) {
    return this.menuModel.findById(id);
  }

  async update(
    id: string,
    updateMenuDto: UpdateMenuDto,
  ): Promise<MenuDocument> {
    return this.menuModel.findByIdAndUpdate(id, updateMenuDto);
  }

  async remove(id: number) {
    return this.menuModel.findByIdAndRemove(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

With the services written, we can now add the logic for the controllers.

Adding Menu controllers

NestJS provides decorators you can use to define if a class is a Controller but also define the HTTP method attached to each of the methods of the controller. Modify the code in the src/menus/menus.controller.ts file.

// src/menus/menus.controller.ts

import {
  Controller,
  Get,
  Post,
  Body,
  Param,
  Delete,
  Put,
} from '@nestjs/common';
import { MenusService } from './menus.service';
import { CreateMenuDto } from './dto/create-menu.dto';
import { UpdateMenuDto } from './dto/update-menu.dto';

@Controller('menus')
export class MenusController {
  constructor(private readonly menusService: MenusService) {}

  @Post()
  create(@Body() createMenuDto: CreateMenuDto) {
    return this.menusService.create(createMenuDto);
  }

  @Get()
  findAll() {
    return this.menusService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.menusService.findOne(id);
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updateMenuDto: UpdateMenuDto) {
    return this.menusService.update(id, updateMenuDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.menusService.remove(+id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Running the project

We have implemented all the layers of the NestJS application. Let's add a last configuration in the src/menus/menus.module.ts to register the MenuSchema class.

// src/menus/menus.module.ts

import { Module } from '@nestjs/common';
import { MenusService } from './menus.service';
import { MenusController } from './menus.controller';
import { MongooseModule } from '@nestjs/mongoose';
import { Menu, MenuSchema } from './schemas/menu.schema';

@Module({
  imports: [
    MongooseModule.forFeature([
      {
        name: Menu.name,
        schema: MenuSchema,
      },
    ]),
  ],
  controllers: [MenusController],
  providers: [MenusService],
})
export class MenusModule {}
Enter fullscreen mode Exit fullscreen mode

Then, run the following command to run the server.

yarn start:dev
Enter fullscreen mode Exit fullscreen mode

This will start the server at http://localhost:3000 and the menus resource can be accessed at http://localhost:3000/menus. You can try a POST request with the following payload.

{
    "name": "Pain au chocolat",
    "description": "Chocolate bread in English.",
    "price": 10
}
Enter fullscreen mode Exit fullscreen mode

Here are some tests you can make to the API using Postman or Insomnia.

Get All Menus - http://localhost:3000/menus

GET http://localhost:3000/menus
Enter fullscreen mode Exit fullscreen mode

Create a menu - http://localhost:3000/menus

POST http://localhost:3000/menus
Content-Type: application/json

{
    "name": "Pain au chocolat",
    "description": "Chocolate bread in English.",
    "price": 10
}
Enter fullscreen mode Exit fullscreen mode

Update a menu - http://localhost:3000/menus/<menuId>

PUT http://localhost:3000/menus/<menuId>
Content-Type: application/json

{
    "price": 5
}
Enter fullscreen mode Exit fullscreen mode

Great! We have created an API using NestJS with very few steps. That is why NestJS is loved by the developer community. If you want to learn more about NestJS and its possibilities, feel free to read the documentation.

In the next section, we will learn how to integrate Docker+Docker Compose with NestJS and MongoDB.

Adding Docker (Optional)

Docker is an open platform for developing, shipping, and running applications inside containers. So, why use Docker?
It helps you separate your applications from your infrastructure and helps in delivering code faster. If it's your first time working with Docker, I highly recommend you go through a quick tutorial and read some documentation about it.

Here are some great resources that helped me:

Dockerfile

The Dockerfile designates a text document containing all the commands that we could call on the terminal of a machine to simply create an image. Add a Dockerfile file to the project root:

# Base image
FROM node:16-alpine

# Create app directory
WORKDIR /app

# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./

# Install app dependencies
RUN yarn install

# Bundle app source
COPY . .

# Creates a "dist" folder with the production build
RUN yarn build
Enter fullscreen mode Exit fullscreen mode

Here, we started with an Alpine-based Docker Image for Node. It's a lightweight Linux distribution designed for security and resource efficiency.

After that, we perform operations like:

  • Setting the working directory.
  • Copying the package.json file to the working directory.
  • Installing the project dependencies.
  • And lastly copying the entire project.

Also, let's add a .dockerignore file.

Dockerfile
node_modules
Enter fullscreen mode Exit fullscreen mode

After that, we can add the configuration to use Docker compose. You can use it to define and run multi-container Docker applications.
What do we need? Well, just a YAML file containing all the configuration of the application's services such as the MongoDB server and the application itself.
Then, with the docker compose command, we can create and start all those services.

version: '3.8'
services:
  mongodb:
    image: mongo:latest
    env_file:
      - .env
    ports:
      - 27017:27017
    volumes:
      - mongodb_data_container:/data/db

  api: 
    build: .
    volumes:
      - .:/app
      - /app/node_modules
    ports:
      - ${APP_PORT}:${APP_PORT}
    command: npm run start:dev
    env_file:
      - .env
    depends_on:
      - mongodb

volumes:
  mongodb_data_container:
Enter fullscreen mode Exit fullscreen mode

We have defined some configurations to tell Docker Compose to look into a .env file to find environment variables to run the containers. At the root of the project, create a .env file with the following content.

APP_PORT=3000
MONGODB_URL=mongodb://mongodb:27017/Restaurant
Enter fullscreen mode Exit fullscreen mode

Let's also modify the content in the src/main.ts and the src/app.module.ts files to use the environment variables for the port and the MongoDB configuration.

// src/main.ts

async function bootstrap() {
...
  await app.listen(process.env.APP_PORT);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode
// src/app.module.ts

@Module({
  imports: [MongooseModule.forRoot(process.env.MONGODB_URL), MenusModule],
  controllers: [AppController],
  providers: [AppService],
})
Enter fullscreen mode Exit fullscreen mode

The setup is completed. Let's build our containers and test if everything works locally.

docker-compose up -d --build
Enter fullscreen mode Exit fullscreen mode

Your project will be running on https://localhost:3000.

Summary

In this article, we've learned how to build an API using NestJS, TypeScript, MongoDB, and Docker.

And as every article can be made better so your suggestion or questions are welcome in the comment section. 😉

Check the code of this tutorial here.

Article posted using bloggu.io. Try it for free.

Top comments (0)