NestJS is a powerful framework for building server-side applications with Node.js, designed with a clean architecture inspired by Angular. It helps developers create organized and efficient code. In this post, we'll explore the fundamental components of NestJS, such as controllers, providers, and modules. We'll also delve into how middleware, exception filters, pipes, and guards function within the framework.
Whether you're new to NestJS or just want to learn more, this guide will help you understand how to use these tools to build great applications.
Controllers
This is the layer responsible for handling all incoming requests. It is the core process business logic of the application.
import { Controller, Get, Req } from '@nestjs/common';
@Controller('tasks')
export class CatsController {
@Get()
findTask(@Req() request: Request): string {
return 'This action returns all tasks';
}
}
Besides, it’s handling routing logic, redirection, all can be setup using just the decorator system
@Get('sub-task')
@Redirect('https://redirect-url.com', 301)
Provider
What is providers in Nest? Services, repository, factories, helpers,
The main idea of provider is that it can be injected as a dependency. Provider will use annotation @Injectable()
, which means this class will be managed by NestJS Inversion of Control container.
After that, the class can be defined in the constructor of another class and will be injected into it.
There are 3 types of provider scope in NestJS, the default one is a single instance, which is tied to the lifetime of the application. Once the application is bootstrapped, all singleton providers have been instantiated.
The second type is Request scoped provider, which will be created by each incoming request, after the request finishes its lifecycle, the provider will be garbage collected.
The last type is Transient, which will always be created as a new instance every time we inject into the constructor.
For performance concerns, request-scoped and transient providers could slow down you request because NestJS always create a new instance for each request or each injection. So use it wisely.
Durable Provider: This is an advanced provider concept in NestJS. It optimizes the performance of application by reusing the same provider across requests that share common attributes. Typically, this provider will be used in multi-tenant applications, where some providers only exist in one tenant and are separate from all other tenants.
Modules
Module in NestJs is a way to manage and group controllers and services in the application.
One module can contain multiple controllers, services and repositories. It can inject other providers into its constructor, but it can’t be injected into provider due to circular dependency.
Module can import other modules, and have access to exported resources of those modules
In NextJs, modules are singleton by default. If you want some providers available everywhere, group them on a global module using @Global
decorator.
Dynamic module: This is another term for modules. It is used when the module is used in multiple locations, and we want it to behave differently in each case. For example, ConfigModule
in this case, it will load the config from any folder specified in register
function. Many modules can use this module in different folders, based on its need.
@Module({
imports: [ConfigModule.register({ folder: './config' })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Middleware
NestJS middleware are equivalent to express middleware, which is defined as:
Can make changes to request and reponse object.
Can end the request response cycle.
Can call the next() function to move to the next middleware in the stack.
If the middle doesn’t end the request response cycle, it must call next function. If it doesn’t, the request will be left hanging
Use case: while NestJS provides a specilized construct like Pipes, Guard,…. for a specific scenario, middleware play a crucial on some scenario:
Custom Rate Limiting: in some cases, rate limiting logic might need access to external system, middleware is a great place to do that
Integrate with third-party middleware:
body-parser
,morgan
,… many middleware are originally for Express.js could be used as NestJS middleware.Global Request Modifications: in some cases, we need to modify the header, the request data of the whole application.
Exception Filters
This is the layer that handles all the unhandle exception across the application. In a request-response cycle, if there is an exception that our code doesn’t catch, it will be throw to this layer and Exception Filter will send back to user a meaningful response.
If the exception is an unrecognized exception, the builtin exception filter will send a general error to client:
{
"statusCode": 500,
"message": "Internal server error"
}
So instead, we could define the error and throw it
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
and the server will respond with desired message and status code.
{
"statusCode": 401,
"message": "Forbidden"
}
Custom Exception Filter
Normally, using built-in HTTP exceptions is enough. But if it doesn’t cover your cases, we could create a new Exception filter.
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
The @Catch
annotation tell us which type of exception will be handled by our ExceptionFilter.
Then we can access the request and reponse object to do some modification before send the response back to user.
We can bind this filter to api level, controller level, the whole module level or even global level.
Pipes
Pipes help us transform the request data to a desired form.
This layer sits before the main handler function. So that mean after request data being transform by Pipes, it will be passed next to the handler.
So Pipes have 2 typical use cases:
Tranformation: tranform the data before passing to controller
Validation: evaluate the data, pass unchanged data to the controller if valid, otherwise through exception.
Guard
Use for authentication, authorization, how it work is similar to Pipes, but it has another meaning.
Guard being executed after middleware but before any pipes or interceptor.
Example:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
If the canActivate
return true
, this request will be continued,
If false
, it will be denied
And how to use this Guard:
// controller scope
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
// method scope
@Controller('cats')
export class CatsController {
@Post()
@UseGuards(RolesGuard)
createCats() {
// This endpoint is protected by the AuthGuard
return 'This is a protected endpoint';
}
}
Guard can be bound to method scope, controller scope, or global scope.
By understanding these core elements, I hope you can leverage NestJS to create scalable and maintainable applications, whether you’re new to the framework or seeking to enhance your knowledge.
Top comments (2)
excellent explanation
Thanks a lot