The

Request lifecycle

The request lifecycle

Pipes are used for:

Data Validation

transformation

When it is not valid - throw an exception
If valid - pass it through unchanged

of data to the desired type

Pipes target the arguments of the Controller handler functions

Built-in Pipes:

ValidationPipe

ParseIntPipe

ParseBoolPipe

ParseArrayPipe

ParseUUIDPipe

DefaultValuePipe

Pipes can be applied to:

To a single route handler argument

To the route handler method

To the Controller class

Globally at the app level

Pipes applied

import { Get, Param, ParseIntPipe } from '@nestjs/common';

...
  
  @Get(':id')
  getPetByID(@Param('id', ParseIntPipe) id: string) {
    // if :id is not a numeric string - ParseIntPipe throws
    return this.petsService.getPetByID(id);
  }

To a single route handler argument

pets.controller.ts

Pipes applied

import { Post, Body, ValidationPipe } from '@nestjs/common';

...
  
@Post()
@UsePipes(ValidationPipe)
createPet(@Body() createPetDto: CreatePetDto) {
  return this.petsService.createPet(createPetDto);
}

To the route handler method

pets.controller.ts

Pipes applied

pets.controller.ts
import { ... , ValidationPipe } from '@nestjs/common';
  
@Controller('pets')
@UsePipes(ValidationPipe)
export class PetsController {
  
  constructor(private readonly petsService: PetsService) {}
  
  ...
}

To the Controller class

Pipes applied

main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

Globally at the app level

Pipes applied

app.module.ts
import { APP_PIPE } from '@nestjs/core';

@Module({
  imports: [PetsModule],
  providers: [
    { provide: APP_PIPE,  useClass: ValidationPipe }
  ],
})
export class AppModule {}

At the Module level

Handling Errors
in Nest

Built-in HTTP exceptions

Nest has many built-in exception types you can throw

Use them as is or extend and customize them.

BadRequestException
UnauthorizedException
NotFoundException
ForbiddenException
NotAcceptableException
RequestTimeoutException
ConflictException
GoneException
HttpVersionNotSupportedException
PayloadTooLargeException
UnsupportedMediaTypeException
UnprocessableEntityException
InternalServerErrorException
NotImplementedException
ImATeapotException
MethodNotAllowedException
BadGatewayException
ServiceUnavailableException
GatewayTimeoutException
PreconditionFailedException

Exception filters

You can also define your own custom exception filters

What

This is handled by the built-in global exception filter

Exceptions you throw, or unhandled runtime errors are caught by Nest, which automatically sends an appropriate user-friendly response.

for more control over handling output of those errors

i.e. stdout, error log files, error aggregation services,
monitoring, analysis, formatting etc...

Custom Exception filters

import { Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import * as log from '@ajar/marker';

@Catch()
export class ExceptionsLoggerFilter extends BaseExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    console.log('Exception thrown', exception.message); //Boring...
    log.error(exception); //Fancy color logger... :)
    super.catch(exception, host);
  }
}

Other than logging to the console you might want to
write an entry in a log file, send a notification to admins
aggregate errors in a service like:
 - sentry.io
 - elastic.co/logstash

Custom Exception filters

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,
      });
  }
}

Accessing the response object directly to gain full control on the response
object values and shape.

The @Catch() decorator may take a single parameter
or a comma-separated list.

Binding filters

@Post()
@UseFilters(ExceptionsLoggerFilter)
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

At the route handler method

As soon as an uncaught exception is encountered, the rest of the lifecycle is ignored and the request skips straight to the filter.

The @UseFilters() decorator may take a single filter or a comma-separated list.

@UseFilters(new ExceptionsLoggerFilter())
export class PetsController {}

At the Controller class level

If several filters exist at different levels - only one filter will be activated.

Binding filters

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new ExceptionsLoggerFilter());
  await app.listen(3000);
}
bootstrap();

Global scoped filter

Outside of the application scope. No DI...

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [ 
    { provide: APP_FILTER,  useClass: ExceptionsLoggerFilter}
  ],
})
export class AppModule {}

Better to use at the Module level

Middleware

A function or a sequence of functions

called before the route handler

 

Nest middleware are essentially express middleware

 

They can access the request and response objects,

and the next() middleware function in the queue

 

Middleware typically:

Execute code

Apply changes to the request and/or the response objects

End the request-response cycle or redirect to another route.

Must call the next() middleware in the queue if it didn't redirect or ended the response cycle.

* Otherwise, the request is left hanging and doesn't reach the route handler

Middleware

logger.middleware.ts

Example

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

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(`${req.method} ${req.originalUrl} ${res.statusCode}`);
    next();
  }
}
app.module.ts

Middleware

Apply using a Module

Modules that include middleware have to implement the NestModule interface

Use the configure() method of the module class.

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { LoggerMiddleware } from './middleware/logger.middleware';
import { PetsModule } from './pets/pets.module';

@Module({
  imports: [PetsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('pets');
  }
}

Middleware

The .forRoutes() method

Can take a single string, multiple strings, a RouteInfo object, a controller class, a list of controllers separated by commas, wildcard routes similar to express

Routes wildcards

a single string

a RouteInfo object

a Controller class

.forRoutes('pets');
.forRoutes({ path: 'cats', method: RequestMethod.GET });
.forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
.forRoutes(PetsController);

Middleware

Excluding routes

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .exclude(
        { path: 'pets', method: RequestMethod.PUT },
        { path: 'pets', method: RequestMethod.DELETE },
        'pets/(.*)', // exclude nested routes...
      )
      .forRoutes(PetsController);
  }
}

Middleware

Declared as a function

import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`${req.method} ${req.originalUrl} ${res.statusCode}`);
  next();
}
logger.middleware.ts
consumer
  .apply(logger)
  .forRoutes(PetsController);
app.module.ts

Middleware

use multiple middleware functions

import * as helmet from 'helmet';
import * as morgan from 'morgan';
import * as responseTime from 'response-time';

@Module({
  imports: [PetsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(helmet(), morgan('dev'), responseTime(), logger)
      .forRoutes(PetsController);
  }
}
app.module.ts

Middleware

Apply middleware globally

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(helmet(), morgan('dev'), responseTime(), logger);
  await app.listen(3000);
}
bootstrap()
main.ts

Guards

Guards have a single responsibility. They either allow a request to be handled by a route handler or not.
As their name implies, they protect our routes handlers

This is determined by a set of
authorization conditions
like permissions, roles, ACLs, etc.

 

Unlike middleware that knows nothing about the next() step in the chain, Guards know exactly what's going to be executed next via an ExecutionContext instance.
 

What

Guards

A guard is a class annotated with the @Injectable() decorator.

Guards should implement the CanActivate interface.

Much like Pipes or exception filters, Guards can be:
controller-scoped, method-scoped, or global-scoped.

How

Interceptors

They make it possible to:

Bind extra logic before / after method execution

Transform the result returned from a function

Transform the exception thrown from a function

Extend the basic function behavior

Completely override a function depending on specific conditions (e.g., for caching purposes)

What

Interceptors

Should implement the NestInterceptor interface.

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> {
    console.log('Before...');

    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}

The CallHandler interface implements the handle() method

A class annotated with the @Injectable() decorator

How

Binding Interceptors

@UseInterceptors(LoggingInterceptor)
export class PetsController {}

At the route handler method

At the Controller class level

@Post()
@UseInterceptors(LoggingInterceptor)
createPet(@Body() createPetDto: CreatePetDto) {
  return this.petsService.createPet(createPetDto);
}

How

Applied to all handler methods of the Controller

Binding Interceptors

What

const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());

Global scoped

Outside of the application scope. No DI...

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';

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

Better to use at the Module level

The request lifecycle

Summary

  1. Incoming request

  2. Globally bound middleware
  3. Module bound middleware
  4. Global guards
  5. Controller guards
  6. Route guards
  7. Global interceptors (pre-controller)
  8. Controller interceptors (pre-controller)
  9. Route interceptors (pre-controller)
  10. Global pipes
  11. Controller pipes
  12. Route pipes
  13. Route parameter pipes
  14. Controller (method handler)
  15. Service (if exists)
  16. Route interceptor (post-request)
  17. Controller interceptor (post-request)
  18. Global interceptor (post-request)
  19. Exception filters (route, then controller, then global)
  20. Server response

The complete request lifecycle is:

Demo time!

Nest.js Request lifecycle

By Yariv Gilad

Nest.js Request lifecycle

  • 6,159