Maximizing Performance in Nest.js with Middlewares, Guards, Interceptors and Pipes

Introduction

Nest.js is a modern and efficient web development framework. It's written in TypeScript and includes a modular architecture, making it simple to maintain and scale web applications. Nest.js's asynchronous programming support ensures that your online apps are quick and responsive to user interactions. Its support for reactive programming also makes it an excellent choice for developing dynamic and interactive web apps. Nest.js is an excellent tool to have in your web development toolset, whether you are a newbie or an experienced developer.

Prerequisite

Understanding Nest.js Middleware

Middleware is a function or software that sits in between the request-response life cycle of a server and is called before the request handler. In web development, It can be used to modify requests, responses, or both, and to perform various tasks, such as authentication, logging, rate limiting, and error handling. This is similar to a relay race in which each runner takes the baton and moves it one step closer to the finish line, with each runner having their specific role and task to perform, but here in a middleware chain, a request is processed by middleware functions in sequence, each modifying it in some way before passing it on to the next.

How do you make use of middleware in Nest.js?

Similarly to Expressjs, Nestjs has a built-in middleware system that allows developers to easily create and utilize middleware in their applications, Middleware may be configured at the application, module, or route levels, and it can be set to execute before or after other middleware or handlers.

To make a middleware, start by defining your class with the @Injectable Decorator and implementing the NestMiddleware interface. The class should include a use() method that accepts a req object, a res object, and a next function. The req object represents the incoming request, the res object represents the response to the client, and the next function is used to send control to the next middleware function, which you all import from 'express', since you are making use of Express as the default platform.

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class ExampleMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request');
    next();
  }
}

Different types of middleware in Nest.js

There are various types of middleware. For this article, I will classify them into two major forms;

  • The Global level/Application level middleware.

  • The Route-specific middleware.

The Global level/Application level middleware:

A global-level middleware in Nest.js is a middleware that is applied to every incoming request regardless of the route being accessed. It intercepts the request and can be used to perform various tasks such as logging, error handling, authentication, and authorization. Using global-level middleware in Nest.js can help to improve code reusability and simplify the development process. It can easily be defined using the app.useGlobalMiddleware() method in your root file which is typically the main.ts.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MyGlobalMiddleware } from './my-global-middleware';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // Define and register the global middleware
  app.useGlobalMiddleware(MyGlobalMiddleware);
  // Start the server
  await app.listen(3000);
}
bootstrap();

Note that you can also implement a Global level middleware by making use of the configure method in the module class that implements NestModule and setting the forRoutes to a wildcard *, which shows that the middleware should be applied to all routes in the application. The Module here is the root AppModule, this is due to the fact that the root AppModule is the main entry point of a Nest.js application and is responsible for bootstrapping and configuring the application.

import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
import { MyGlobalMiddleware } from './my-global-middleware';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(MyGlobalMiddleware)
      .forRoutes('*');
  }
}

The Route-specific middleware:

In Nest.js, route-based middleware is a middleware that is linked to a single route or set of routes in the application. When compared to global middleware, this sort of middleware is applied just to the individual routes to which it is bound. Developers can apply specialized middleware to only the routes that require it by utilizing route-based middleware. This can increase application efficiency by minimizing excessive middleware processing on routes where it is not required.

You can add the middleware in the route module or feature module by making use of the configure method in the Module class which implements the NestModule.

import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
import { UserMiddleware } from './user-middleware';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(UserMiddleware)
      .forRoutes('user');
  }
}

So, therefore, in applying a middleware whether it's route-specific or global, the main purpose is to enhance our application by adding functionalities.

Nest.js Guards

What are Guards and why do we use them?

Guards in Nestjs are like what a bouncer is to a club, they help in controlling access, but in this case to a route, if authorized or with a permit they allow access but if unauthorized it throws an error. They are a feature or class designed using the @Injectable decorator which is used to define a class as a provider and it implements the CanExecute interface. So Nestjs guards are basically like a bouncer, but instead of throwing you out, they throw an error.

Types of guards in Nest.js

There are several types of guards in nestjs, depending on the way it is being used, in this article I will be categorizing the guards into three major types;

  • The AuthGuard

  • The RoleGuard

  • The ThrottlerGuard

The AuthGuard

The Key word here is "Authorization", this type of guard is primarily used in giving access to users that have permits or authenticated users to specific routes in other to be able to perform certain functionalities. A popular use of this AuthGuard is in implementing JSON Web Tokens (JWT) Access, here users are verified based on the access tokens extracted from the request header.

After the JwtStrategy has been configured, you can then go on to create a class JwtAuthGuard that extends from AuthGuard('jwt') so it can be called directly using the @useguard decorator.

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

The JwtAuthGuard is then imported and passed in as a parameter to the @UseGuards decorator.

import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './jwt-auth.guard';

@Controller()
export class UserController {
  @Get('profile')
  @UseGuards(JwtAuthGuard)
  viewProfile() {
    // Code to get user profile
  }
}

This @useGuards(JwtAuthGuard) decorator set here ensures that only a user with a valid JWT access token will be granted permission to this route.

The RolesGuard

Most of the time, when allowing access to users, you want to be able to distinguish between a regular user and an admin user, since they may have distinct features, and a simple concept does this for us; The RBAC Implementation stands for Role-based access control; imagine being a guest in a house and being the house owner; the guest can only go to certain parts of the house, but the house owner may walk about freely and access everywhere.

To get started, it is customary to have a selection of different types of individuals that can access your application, you can use an enum to do this.

export enum Role {
User = 'User',
Admin = 'Admin'
}

You then go on to create a custom decorator in which you import the Role Enum and also the SetMetadata property from @nestjs/common, the SetMetadata here is what allows you to be able to tell the guard what role is expected at different routes.

import { SetMetadata } from '@nestjs/common';
import { Role } from './users/enums/role.enum';

export const ROLES_KEY = 'roles'
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

You now have a custom roles decorator which you can apply on any route of your choice, import it in your controller scope and apply it on the method that calls the route

import { Get, Controller } from '@nestjs/common';
import { Roles } from '../custom.decorator';
import { Role } from './enums/role.enum';

@Controller('user')
export class UserController {
constructor(private usersService: UsersService) {}
@Get('all-users')
@Roles(Role.Admin)
async getAllUsers(){
//code to get all users using usersService
}
}

Now that you have created a custom decorator for roles and applied them, you move on to ensure that it is fully enforced using the role guards, to do this you make use of the reflector auxiliary class, which is inbuilt and can be accessed via the @nestjs/core module.

The rolesGuard class is defined, which implements the CanActivate interface, which returns a boolean indicating whether or not navigating a route is permitted. The reflector helper class is then injected via the constructor which allows access to the metadata that has been set using the @SetMetadata() decorator and contains the roles that are permitted to access the route.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from './role.enum';
import { ROLES_KEY } from '../custom.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<Role[]>(ROLES_KEY, context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return roles.some((role) => user.roles?.includes(role));
  }
}

Here, the guard logic is implemented in the canActivate function. It receives an ExecutionContext parameter, which provides access to information about the incoming request. For instance, ExecutionContext can be used to obtain the current route and query parameters, which can then be used to determine whether or not the request should be allowed to continue.

You make use of the this.reflector.get<Role[]> in fetching the metadata, in this case, which you applied to a method in the class, that is why you make use of context.getHandler(), if it was applied to the controller class itself you would make use of context.getClass().

A conditional is used to check if any roles exist, if there are no roles specified for the method it returns true and the request is allowed to process, but if there are roles, the guard checks if there are any required roles needed for access, if the user has any of the roles specified, it returns true and the user is allowed to proceed, else if the user doesn't have the permit or has specified roles, the request is denied and throws an error. For instance, if a request comes in trying to access a page requiring the 'admin' role, and it is from a regular user, the guard will throw an error and deny the request.

Now that you have created a RolesGuard, you can then apply it to your route using the @UseGuards.

import { Get, Controller } from '@nestjs/common';
import { Roles } from '../custom.decorator';
import { Role } from './enums/role.enum';

@Controller('user')
export class UserController {
constructor(private userService: UsersService) {}

@Get('all-users')
@UseGuards(RolesGuard)
@Roles(Role.Admin)
async getAllUsers(){
//code to get all users using userService
}
}

Note, for this to work you must have specified a Role Column when you defined your User Entity, so it will be present in the request.user object, in other, for the RolesGuard to be able to verify the specified user.

ThrottlerGuard

The Throttler Guard is a unique type of guard that is primarily used in handling excess requests to a route, this is called rate-limiting, it is a defence against server attacks like Distributed Denial-of-Service (DDoS) which overwhelms a server by sending in too many requests from multiple sources.

To use the throttler Guard, you must first install the @nestjs/throttler package using the command below on your terminal.

$ npm i --save @nestjs/throttler

when the package has been downloaded completely you then configure it in your root AppModule like this.

import { Module } from '@nestjs/common';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { UsersModule } from '../User.module';

@Module({
  imports: [
    ThrottlerModule.forRoot({
      ttl: 60,
      limit: 10,
    }),
    UsersModule
  ],
  providers: [],
})
export class AppModule {}

The ttl:60 means that each request has an expiry time of 60 seconds and the limit:10 here implies that a given IP address can only send requests a total of 10 times on a given route, So therefore, a client IP can send a maximum of 10 requests on a given route within 60 seconds. You can then go on to apply the ThrottleGuard by passing it to the @UseGuard decorator and setting it on a route method.

import { Controller, Post, UseGuards } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';

@Controller('user')
@UseGuards(ThrottlerGuard)
export class UsersController {
  @Post()
  createUser() {
    return 'This creates a user';
  }
}

Note: You can set all guards to be global and not just route-specific by configuring it in your root Appmodule by adding it to your Providers array and using the provide and useClass options to make them available to all modules in your application.

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { RolesGuard } from './guards/roles.guard';
import { AuthGuard } from './guards/auth.guard';
import { ThrottlerGuard } from '@nestjs/throttler';

@Module({
  imports: [/* Your other imports here */],
  controllers: [/* Your controllers here */],
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,
    },
  ],
})
export class AppModule {}

Nest.js Interceptors

What interceptors are and what they do in web development

An Interceptor like its counterparts are classes that are defined using the @Injectable decorator, meaning it can be added as a provider to a service. It illustrates Aspect Oriented Programming (AOP) which is a programming model that allows developers to write code that may be applied in many portions of an application without breaking the central logic. A possible implementation could be to transform the response data that is being sent to the client. It might also be used to handle errors or to cache data reducing the number of requests made to external services.

How then do we make use of interceptors in Nest.js?

Firstly you create an interceptor class by defining it with an @Injectable Decorator, the class implements NestInterceptor interface, an intercept method is defined in the class and it takes in two parameters, which are the current Executioncontext, and a callHandler which is the next function that tells your application to perform the following pipeline middleware. An example implementation of this can be seen here below.

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req = context.switchToHttp().getRequest();
    console.log(`[${req.method} ${req.url}]`);
    return next.handle().pipe(
      tap(() => console.log(`[${req.method} ${req.url}] response sent`)),
    );
  }
}

Here the context. switchToHttp(). getRequest() obtains the transport layer, which contains information about the incoming request such as headers, content, query parameters, HTTP methods, and URLs, and saves it in the req object, and then here it logs the request method and the url to the terminal, After this, the request is then forwarded to the next interceptor in the pipeline through the next.handle() is a method. To configure an observable stream for the request, use the pipe() function. The tap() operator is then used to execute a side effect, which in this example logs a message stating that the request method and URL, have been delivered.

Can we classify interceptors in Nest.js ?

Interceptors can be defined according to the context in which they are used, such as request interceptors and response interceptors. Request interceptors handle incoming requests, whereas response interceptors alter the response before returning it to the client. A response interceptor may be implemented using the transform method. For instance, a response interceptor could be used to adjust the response header according to certain criteria before sending it back to the client, a request interceptor can be used to validate who a user is and modify information on the request object. So it depends on what feature you want to implement with it on your application.

Also, you can classify interceptors on the scope they are being applied,

In your application, you can implement nest interceptors using the @useInterceptors() decorator, you can add it your controller and even directly on the defined methods.

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';
import { TransformInterceptor } from './transform.interceptor';

@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
  @Get()
  @UseInterceptors(TransformInterceptor)
  getUsers() {
  // code to retrieve users while transforming the response.
  }
}

You can also implement the nest interceptor globally on your root AppModule, or to particular features by applying it to the feature module.

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './logging.interceptor';
import { AppController } from './app.controller';

@Module({
  controllers: [AppController],
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
  ],
})
export class AppModule {}

Nest.js Pipes

Nest.js Pipes like the customary definition of pipes you know of are used to connect different parts of a system. However, in this case, it might be functions or programs. The main use case of pipes in nest.js can be seen in validation and transformation. The transformation use case is modifying data as it passes through the pipe. A pipe, for example, can be used to transform a string into an integer or to format a date in a specified way. While the validation here ensures that the request that is being sent passes through some criteria that have been set. By using pipes for both incoming requests and outgoing responses, you can ensure that the data is both validated and transformed properly. This can help prevent errors and improve the overall performance of your API.

Different types of pipes in Nest.js (ValidationPipe, ParseIntPipe, etc.)

There are various implementations of pipes, asides you being able to create custom Pipes, NestJs Pipes can be classified into two categories the Transformation pipes and the validation pipes, Nestjs comes with some built-in pipes which are all provided in the @nestjs/common package which is about nine;

  • ValidationPipes

  • ParseIntPipe

  • ParseBoolPipe

  • ParseFloatPipe

  • ParseArrayPipe

  • ParseUUIDpipe

  • ParseFilePipe

  • ParseEnumPipe

  • DefaultValuePipe

Validation Pipes are used in verifying if incoming data passes all of the specified criteria, such as length, data type, and so on. This guarantees that the data is correct before it is saved in the database. Transformation Pipes are used in changing incoming data to a set data type, an example is with the ParseIntPipe, which converts incoming data to an integer type. Depending on their function, custom-made pipes might be used for validation or transformation.

In other to create custom pipes, the class is defined with an injectable and it implements the PipeTransform interface, you can create a file and do this manually or you can also decide to make use of the Nest-Cli, to quickly create a custom pipe, using this command nest generate pipe <pipename>, here is an example shown on the terminal below.

If you open your IDE, you will see this code sample, with some basic configuration for the custom Pipe.

import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class ExamplePipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}

Here the class is marked with a transform method which takes two arguments value and metadata. The value here represents the data that is going to be passed into the pipe for processing, while metadata is an optional parameter that tells us more information that can be provided to the pipe, in some cases metadata contains the validation rules that the pipe can use to validate the data.

How can we apply these Pipes in our Application?

There are several ways in which you can implement pipes in our application, the first one is by binding pipes o our methods, depending on the context it is being used, for example

import { Controller, Get, Param } from '@nestjs/common';
import { CapitalizePipe } from './capitalize.pipe';

@Controller('users')
export class UsersController {
  @Get(':username')
  getUserByUsername(@Param('username', CapitalizePipe) username: string) {
    return `Fetching user with username: ${username}`;
  }
}

Here, you set a custom-made pipe CapitalizePipe, which converts the param being passed to our route to Capital Letter.

Alternatively, you can also implement this pipe using the @UsePipe() decorator

import { Controller, Get, Param, UsePipes } from '@nestjs/common';
import { CapitalizePipe } from './capitalize.pipe';

@Controller('users')
export class UsersController {
  @Get(':username')
  @UsePipes(CapitalizePipe)
  getUserByUsername(@Param('username') username: string) {
    return `Fetching user with username: ${username}`;
  }
}

Also, you can apply it globally from our root file main.ts using the app.useGlobalPipes()

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());
  await app.listen(3000);
}
bootstrap();

In this code example, the built-in validation pipe is passed into the useGlobalPipes method, meaning that the validation pipe which will be applied to all incoming requests to our application.

Conclusion

The role of middleware, guards, interceptors, and pipes in a NestJS application and how to implement them in our application was discussed, be it globally scoped, controller-scoped or method-scoped using decorators to define them. Middleware is used in Nest.js to handle incoming requests and route them to the proper resources, while guards serve to prohibit unauthorized access to some parts of the application. Interceptors change the request or response before it reaches its destination, while pipes transform or validate the data from the request before it is utilized in the application.

If you enjoyed reading this, you can check out my other article Nest.js for Beginners: Understanding the Basics, and also follow me [Okoye Chukwuebuka Victor] and subscribe to my newsletter, Gracias!