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
After the installation, create a new project with the following command:
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 :).
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
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 {}
Finally, add the following packages for client payload validation in the REST API.
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.
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
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;
}
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);
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;
}
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
createdand 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();
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) {}
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);
}
}
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);
}
}
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 {}
Then, run the following command to run the server.
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.
{
"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.
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
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.
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.
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.
// src/main.ts
async function bootstrap() {
...
await app.listen(process.env.APP_PORT);
}
bootstrap();
// 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.
Article posted using bloggu.io. Try it for free.