DEV Community

Cover image for A Deep Dive into NestJS Core Components
DinhTr
DinhTr

Posted on

A Deep Dive into NestJS Core Components

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';
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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

Image description

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"
}
Enter fullscreen mode Exit fullscreen mode

So instead, we could define the error and throw it

@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
Enter fullscreen mode Exit fullscreen mode

and the server will respond with desired message and status code.

{
  "statusCode": 401,
  "message": "Forbidden"
}
Enter fullscreen mode Exit fullscreen mode

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,
      });
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Image description

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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';
  }
}
Enter fullscreen mode Exit fullscreen mode

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 (0)