Introduction
NestJS, a progressive Node.js framework, can be used to build an efficient, scalable, flexible, enterprise-ready NodeJS server-side application, which provides structure and releases APIs directly to the developer. The architecture is heavily inspired by the Angular framework, making it easy for any Angular developers to understand and learn. Behind the scenes, Nest makes use of the robust HTTP server framework, Express, by default. Nest also uses progressive JavaScript that fully supports TypeScript, enabling developers to code in pure JavaScript, and combines elements of object oriented programming (OOP), functional programming (FP), and functional reactive programming (FRP).
In this post, we will explore the architecture of NestJS, and set up a basic API using Typescript including its components, and a landing page using Swagger to gain an overview of all exposed endpoints.
CLI
NestJS has one the most powerful CLI tools. The CLI can be used to create any part of the framework automatically, and develop applications with NestJS without much of the graphical user interface (GUI) interactions. CLI can be used to create modules, controllers and services using command line, and is also consistent with Angular CLI.
Support
NestJS supports many popular tools under the hood like Swagger, TypeORM, Mongoose, GraphQL, Logging, Routing, Validation, Caching, WebSockets, and much more, with little to no configurations. The NestJS website provides very detailed documentation so there’s no need to reinvent the wheel.
Architecture
NestJS has three architecture tiers, which are:
- Controller - Handles the routing.
- Service - Handles the business logic.
- Data access layer - Handles the access of data from the data source.
The order of execution gives developers access to more functionality and application control, allowing them to implement some complex logics such as authentication, error handling, logging and validation, without writing custom logic.
This order of execution is an important consideration, as it will allow us to implement some solutions and business logic higher up the ladder, rather than in the service itself.
Middleware
A middleware is a function that is executed before the route handler, which has access to the request, the response, and the next function. It can be used to change a request and response object, and end a request response cycle. For instance, if we needed to validate the auth token in the request header, we can use a middleware to read and validate the token.
Guard
Guards are a single responsibility class which, as the name suggests, is responsible for guarding the routes. The guards are responsible for confirming the request has the required permissions, role or ACL to access the route. Developers can use this alongside authentication in use cases such as role-based access.
Interceptor
Interceptors are functions that can bind extra functionality before and after execution. Interceptors are really powerful as they are capable of updating the result, providing an exception, or even completely overriding the execution of a function.
Pipe
Pipes can be used to transform or validate any data member. In simpler terms, pipes can be used to transform a string to a number, or to validate if the member is not empty. By default, NestJS offers nine out-of-the-box pipes.
Controller
The controller is the entry point of the request to the main application using dependency injection, and is in charge of invoking the relevant service to handle the business logic. It’s worth noting that the controller is not to be used to implement any business logic, however there might be times where we need to break this convention. If required, a controller can inject multiple services.
Service
Service is the main brain of the application, and is where the business logic will be implemented. The service invokes the data access layer to receive data from the data source, apply any relevant business logic or data transformations, modifications or filtrations, and returns the response back to the controller. A servicer can also inject other services using dependency injection.
There is a fine line between what has to be used and where the concept should lie. Considerations should be made, such as doing justice to the implementation, while ensuring not to exploit any services. For instance, header validation can be done using both an interceptor and a middleware. Developers should be careful with what they use, depending on the use case.
Set Up
That’s enough of the boring theory, now let’s look at some cool stuff. Yes, code.
Let’s create a new to-do list app.
npm i -g @nestjs/cli
nest new todo
This creates the following scaffold files:
-
main.ts
The main entry point of application. This has the main core bootstrap function that creates a single instance NestFactory for the NestJS application. -
app.module.ts
The root module of application. All global modules that will be used by the application have to be injected here. -
app.service.ts
A basic injectable service with a single function. -
app.controller.ts
A basic controller with a single function -
app.controller.spec.ts
Unit tests for app.controller.
Here are a few useful commands:
npm run start
// start the application in the default port
npm run start:dev
// start the application in the default port with hot reload
Now, let’s create the basic modules and components.
Let’s start with an interface.
// models/ToDo.ts
export interface TodoList {
id: string;
description: string;
is_active: boolean;
created_at: Date;
}
Now we have to create new module, controller and service for ToDo.
nest generate module modules/todo
nest generate controller modules/todo
nest generate service modules/todo
This will create the following files:
src/modules/todo/todo.controller.spec.ts
src/modules/todo/todo.controller.ts
src/modules/todo/todo.module.ts
src/modules/todo/todo.service.spec.ts
src/modules/todo/todo.service.ts
Let's clean up before we start. We can now remove app.controller.ts, app.controller.spec.ts and app.service.ts since we need to create a level of abstraction between all the modules. We need to remove their reference from app.module.ts as well. The app.module.ts would already be updated by injecting todo.module.ts into it. This ensures the module is available for consumption by the app and if we run the app, we can hit the todo controller.
TodoService
This is a Singleton Injectable service that is responsible for all the business logic and getting the data from database or source systems. Here, we update the service to have basic CRUD operations.
import { Injectable } from '@nestjs/common';
import { ToDo } from 'src/models/ToDo';
@Injectable()
export class TodoService {
private readonly database: any; // We have to inject this through constructor.
// constructor(private readonly database: any) {}
getAll(): Promise<ToDo[]> {
return this.database.getAll();
}
get(id: string): Promise<ToDo> {
return this.database.get(id);
}
create(todo: ToDo): Promise<ToDo> {
return this.database.new(todo);
}
update(todo: ToDo): Promise<ToDo> {
return this.database.update(todo);
}
delete(id: string): Promise<ToDo> {
return this.database.delete(id);
}
// we can remove this and use the existing update function itself.
markAsInActive(id: string): Promise<ToDo> {
return this.get(id).then((todo: ToDo) => {
todo.is_active = false;
return this.update(todo);
});
}
}
ToDoController
ToDoController is the controller function that will expose the endpoints in the app. This controller injects the relevant service and calls the necessary actions.
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { ToDo } from 'src/models/ToDo';
import { TodoService } from './todo.service';
@Controller('todo')
export class TodoController {
constructor(private readonly todoService: TodoService) {}
@Get(':id')
get(@Param() params: { id: string }): Promise<ToDo> {
return this.todoService.get(params.id);
}
@Get('all')
getAll(): Promise<ToDo[]> {
return this.todoService.getAll();
}
@Post()
create(@Body() body: ToDo): Promise<ToDo> {
return this.todoService.create(body);
}
@Put()
update(@Body() body: ToDo): Promise<ToDo> {
return this.todoService.update(body);
}
@Put('inactive/:id')
markAsInActive(@Param() params: { id: string }): Promise<ToDo> {
return this.todoService.markAsInActive(params.id);
}
@Delete()
delete(@Param() params: { id: string }): Promise<ToDo> {
return this.todoService.delete(params.id);
}
}
Now let's start the application. This will start the application in port 3000.
From here, the available endpoints are unknown. We can make use of another handy package, Swagger.
Swagger
Let’s start by adding the Swagger package.
npm install --save @nestjs/swagger
We can now modify the main.ts to include Swagger and point it to the main page.
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('ToDo API')
.setDescription('The REST API for ToDo application')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('/', app, document); // Pointing to the main page.
await app.listen(3000);
console.log(`ToDo API is running on: ${await app.getUrl()}`);
}
bootstrap();
We can make a few more tweaks to clean up Swagger. The updated controller code will look like this.
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { ApiBody, ApiParam, ApiTags } from '@nestjs/swagger';
import { ToDo } from 'src/models/ToDo';
import { TodoService } from './todo.service';
@ApiTags('ToDo')
@Controller('todo')
export class TodoController {
constructor(private readonly todoService: TodoService) {}
@Get(':id')
@ApiParam({ name: 'id', required: true })
get(@Param() params: { id: string }): Promise<ToDo> {
return this.todoService.get(params.id);
}
@Get('all')
getAll(): Promise<ToDo[]> {
return this.todoService.getAll();
}
@Post()
@ApiBody({})
create(@Body() body: ToDo): Promise<ToDo> {
return this.todoService.create(body);
}
@Put()
@ApiBody({})
update(@Body() body: ToDo): Promise<ToDo> {
return this.todoService.update(body);
}
@Put('inactive/:id')
@ApiParam({ name: 'id', required: true })
markAsInActive(@Param() params: { id: string }): Promise<ToDo> {
return this.todoService.markAsInActive(params.id);
}
@Delete()
@ApiParam({ name: 'id', required: true })
delete(@Param() params: { id: string }): Promise<ToDo> {
return this.todoService.delete(params.id);
}
}
Now we can run the app again.
Now that we have a complete component with all the CRUD operations, we can continue creating more components in a similar fashion. The next stage would be to add authentication to protect the routes, add unit tests, validations, using middlewares and interceptors. There is much more that the Swagger is capable of, but all of that is for another day.
Code: https://github.com/rohithart/nestjs-todo
NestJS Documentation: https://docs.nestjs.com/
Top comments (0)