DEV Community

Cover image for NestJS: Request Lifecycle
ngtrvinc
ngtrvinc

Posted on

NestJS: Request Lifecycle

NestJS is a powerful Node.js framework for building scalable and maintainable server-side applications. One of the key concepts in NestJS is the request life cycle, which describes the sequence of events that occur when a request is made to a NestJS application. Understanding the request life cycle is essential for building efficient and robust applications.

Full Lifecycle

1. Middleware:

Middleware is the first thing that runs when a request reaches the server. It's used to process and modify the request information before it's sent to the part of the application that handles the request (the route handler). Because it's the first component called, it's usually configured first in a project. Middleware can perform various tasks, such as logging, authentication, authorization, and data transformation.

Middleware in this application is set up globally, affecting all incoming requests. This is commonly seen with packages like cors and helmet.

// logger.middleware.ts
import { Request, Response, NextFunction } from 'express';

export function loggerMiddleware(
req: Request, res: Response, next: NextFunction) {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next();
}

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { loggerMiddleware } from './middlewares/logger.middleware';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.use(loggerMiddleware); 

  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Module middleware is used to perform specific functions within a module.

// logger.middleware.ts
import { Request, Response, NextFunction } from 'express';

export function loggerMiddleware(
req: Request, res: Response, next: NextFunction) {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next();
}

// user.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { UserController } from './user.controller';
import { LoggerMiddleware } from '../middlewares/logger.middleware';

@Module({
  controllers: [UserController],
})
export class UserModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes(UserController); 
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Guards
The purpose of a Guard is to determine at runtime whether a request should be allowed to be processed by the route handler. This process is commonly referred to as authorization. You might wonder since both Guards and Middleware handle similar logic, what is the difference between them?

Essentially, middleware doesn't know which handler will be executed after calling the next() function. On the other hand, guards know exactly what's going to be executed next because they can access the ExecutionContext instance.

Hint
Guards are executed after all middleware but before any interceptor or pipe.

// access-token.guard.ts
import {
  CanActivate,
  ExecutionContext,
  Inject,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import jwtConfig from '../config/jwt.config';
import { ConfigType } from '@nestjs/config';
import { REQUEST_USER_KEY } from 'src/constants/auth.constant';
import { Request } from 'express';

@Injectable()
export class AccessTokenGuard implements CanActivate {
  constructor(
    private readonly jwtService: JwtService,
    @Inject(jwtConfig.KEY)
    private readonly jwtConfiguration: ConfigType<typeof jwtConfig>,
  ) {}

  private extractTokenFromHeader(request: Request): string | undefined {
    const [_, token] = request.headers.authorization?.split(' ') ?? [];
    return token;
  }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // Extract the request from the execution context
    const request = context.switchToHttp().getRequest();
    // Extract the token from the header
    const token = this.extractTokenFromHeader(request);

    if (!token) throw new UnauthorizedException();

    try {
      // Verify token with secret key
      const payload = await this.jwtService.verifyAsync(
        token,
        this.jwtConfiguration,
      );
      request[REQUEST_USER_KEY] = payload;
    } catch {
      throw new UnauthorizedException();
    }
    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

Apply the guard in global scope

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AccessTokenGuard } from './guards/access-token.guard';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalGuards(new AccessTokenGuard());

  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Apply the guard in module scope

/// user.module.ts
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { AccessTokenGuard } from '../guards/access-token.guard';

@Module({
  controllers: [UserController],
  providers: [AccessTokenGuard], 
})
export class UserModule {}

// user.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AccessTokenGuard } from '../guards/access-token.guard';

@Controller('users')
@UseGuards(AccessTokenGuard) // Apply guard for controller
export class UserController {
  @Get()
  @UseGuards(AccessTokenGuard) // Or you can apply for only a route
  getUsers() {
    return { message: 'List of users' };
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Interceptors
Interceptors can intercept the request before it reaches the handler or the response before it is sent back to the client. They can be used for tasks like logging, transforming data, or handling errors.

Since interceptors handle both requests and responses, they consist of two parts:

Pre-processing: Executed before reaching the controller's method handler.
Post-processing: Executed after receiving the response from the method handler.

3.1 Global interceptors

// logging.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    // Pre-processing
    console.log(`[PRE] Request to: ${request.method} ${request.url}`);

    const startTime = Date.now();

    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - startTime;
        console.log(`[POST] Response from: ${request.method} ${request.url} - Duration: ${duration}ms`);
      }),
      map((data) => {
        // Post-processing
        return {
          ...data,
          timestamp: new Date().toISOString(), 
        };
      }),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Apply Interceptor

// main.ts - Apply for global scope
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './interceptors/logging.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalInterceptors(new LoggingInterceptor()); 
  await app.listen(3000);
}
bootstrap();

// user.controller.ts - Apply for controller
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from '../interceptors/logging.interceptor';

@Controller('users')
@UseInterceptors(LoggingInterceptor) 
export class UserController {
  @Get()
 @UseInterceptors(LoggingInterceptor) // Or you can apply for only this route
  getUsers() {
    return { message: 'List of users' };
  }
}
Enter fullscreen mode Exit fullscreen mode

4.Pipes
The main purpose of a pipe is to validate, transform, and/or filter data being sent and received from the client.

Typical use cases for using Pipes include:

  • Data Validation: Check if the data sent from the client is in the correct format and valid.

  • Data Transformation: Convert the data format sent from the client into a format that the server can understand, or conversely, transform the response data before sending it back to the client.

  • Data Filtering: Remove unnecessary, sensitive, or potentially harmful data.

Nest provides us with many pipes, you can refer to more at this link.


// main.ts - Global pipe
async function bootstrap() {
 const app = await NestFactory.create(AppModule);
 app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, 
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  await app.listen(3000);
}
bootstrap();

// user.controller.ts
@UsePipes(ParseControllerValidationPipe) // Apply for this controller
@Controller('users')
export class UserController {
  @Post('create')
  @UsePipes(new ValidationPipe()) // Apply ValidationPipe for this route
  createUser(@Body() createUserDto: CreateUserDto) {
    return { message: 'User created successfully', data: createUserDto };
  }

  @Post('update')
  updateUser(@Body() createUserDto: CreateUserDto) {
    return { message: 'User updated successfully', data: createUserDto };
  }
}

 @Get('profile/:id') // Or Apply for the param
  getUserProfile(@Param('id', ParseIntPipe) id: number) {
    return { message: `User profile for ID: ${id}` };
  }

Enter fullscreen mode Exit fullscreen mode

5. Controller
The controller is responsible for handling the request and invoking the appropriate service to process it.

  @Get('/:id')
  @UseGuards(AccessTokenGuard)
  @ApiOperation({
    summary: 'Get user profile with id',
  })
  @ApiResponse({
    status: HttpStatus.OK,
    description: 'Success',
  })
  @HttpCode(HttpStatus.OK)
  public async getUserProfileController(
    @Param('id', new ParseIntPipe()) userId: number,
  ): Promise<UserResponseDto> {
    const newUser = await this.usersService.getUserProfile(userId);
    return newUser;
  }

Enter fullscreen mode Exit fullscreen mode

6.Service
The service contains the business logic for processing the request. It may interact with databases, external APIs, or other services.

public async getUserProfile(userId: number) {
    const user = await this.userRepository.findUserByField('id', userId);
    if (!user) throw new BadRequestException('User is not found');
    return { ...user };
  }

Enter fullscreen mode Exit fullscreen mode

7.Exception Filters
Exception filters are invoked when an exception/error is thrown at any stage of the NestJS request lifecycle, including Middleware, Guards, Interceptors, Pipes, Controllers, or Services. When an exception occurs, NestJS looks for an appropriate exception filter to handle it.

import { Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch(HttpException)
export class HttpExceptionFilter extends BaseExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = exception.getStatus() || HttpStatus.INTERNAL_SERVER_ERROR;

    response.status(status).json({
      statusCode: status,
      message: exception.message || 'Internal Server Error',
      path: request.url,
    });
  }
}

// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalFilters(new HttpExceptionFilter()); 

  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

The response will be

{
  "statusCode": 500,
  "message": "Internal Server Error",
  "path": "/user/1",
}
Enter fullscreen mode Exit fullscreen mode

You also apply

//app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GlobalExceptionFilter } from './common/exceptionFilter/global.exceptionFilter';
import { ResponseFormatInterceptor } from './common/interceptor/response.interceptor';
import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    { provide: APP_FILTER, useClass: GlobalExceptionFilter },
    { provide: APP_INTERCEPTOR, useClass: ResponseFormatInterceptor },
  ],
})
export class AppModule {}

Enter fullscreen mode Exit fullscreen mode

Benefits of Understanding the Request Life Cycle
Understanding the Request Life Cycle provides several benefits:

Debugging: By understanding the different stages of the request life cycle, you can easily identify where an error occurred and how to fix it.

Performance Optimization: You can optimize the performance of your application by identifying bottlenecks in the request life cycle and implementing appropriate solutions.

Security: By using guards and interceptors, you can secure your application and protect it from unauthorized access.

Maintainability: Understanding the request life cycle makes it easier to maintain and extend your application.

Conclusion
The NestJS request life cycle is a fundamental concept for building robust and efficient applications. By understanding the different stages of the request life cycle, you can improve your application's performance, security, and maintainability.

I hope this blog post has been helpful in understanding the NestJS request life cycle. If you have any questions or comments, please feel free to leave them below.

Top comments (1)

Collapse
 
trieuthanhdat profile image
Trieu Thanh Dat

Good