Build a CRUD API using NestJS, MongoDB, Docker & Docker Compose
Tags: javascript, typescript, node, docker
Published: 2022-11-13 15:06:43

[NestJS](https://nestjs.com/) 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: 

```bash
npm i -g @nestjs/cli
```

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

```bash
nest new restaurant-menu
```
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](https://cdn.hashnode.com/res/hashnode/image/upload/v1668345268283/IHRwgkRSE.png)

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. 

```bash
yarn add @nestjs/mongoose mongoose
```
And in the `src/app.module.ts`, connect the application to the database. 

```ts
// 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 {}

```

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

```bash
yarn add class-validator class-transformer
```

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
```

* `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
```

The command above will prompt some options in the terminal. 

![Generating Menu resource](https://cdn.hashnode.com/res/hashnode/image/upload/v1668346736493/nNpNpCrEf.png)

**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](https://docs.nestjs.com/recipes/crud-generator)

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. 

```bash
├── 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
```

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

```ts
// src/entities/menu.entities.ts

export class Menu {
  name: string;
  description: string;
  price: string;
  created: Date;
  updated: Date;
}
```

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. 
```ts
// 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);
```
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. 

```ts
// 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;
}
```
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 with `created` and 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](https://docs.nestjs.com/pipes#pipes). 
```ts
// 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();
```
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`.
```ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateMenuDto } from './create-menu.dto';

export class UpdateMenuDto extends PartialType(CreateMenuDto) {}
```

### 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. 

```ts
// 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);
  }
}
```
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.
```ts
// 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);
  }
}
```

## 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. 
```ts
// 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 {}
```
Then, run the following command to run the server. 

```bash
yarn start:dev
```

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. 

```json
{
	"name": "Pain au chocolat",
	"description": "Chocolate bread in English.",
	"price": 10
}
```
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
```

> **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
}
```

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

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

{
	"price": 5
}
```

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](https://docs.nestjs.com/).

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

## Adding Docker (Optional)

[Docker](https://www.docker.com/)  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: 
-  [Docker Tutorial](https://www.youtube.com/watch?v=eN_O4zd4D9o&list=PLPoSdR46FgI5wOJuzcPQCNqS37t39zKkg) 
-  [Docker curriculum](https://docker-curriculum.com/) 

### 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
```
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
```

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.

```yaml
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:
```

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. 

```bash
APP_PORT=3000
MONGODB_URL=mongodb://mongodb:27017/Restaurant
```

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. 

```ts
// src/main.ts

async function bootstrap() {
...
  await app.listen(process.env.APP_PORT);
}
bootstrap();
```

```ts
// src/app.module.ts

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

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

```
docker-compose up -d --build
```

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](https://github.com/koladev32/nestjs-docker-api). 

*Article posted using [bloggu.io](https://bloggu.io). Try it for free.*