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
Incoming request
- Globally bound middleware
- Module bound middleware
- Global guards
- Controller guards
- Route guards
- Global interceptors (pre-controller)
- Controller interceptors (pre-controller)
- Route interceptors (pre-controller)
- Global pipes
- Controller pipes
- Route pipes
- Route parameter pipes
- Controller (method handler)
- Service (if exists)
- Route interceptor (post-request)
- Controller interceptor (post-request)
- Global interceptor (post-request)
- Exception filters (route, then controller, then global)
- Server response
The complete request lifecycle is:
Demo time!
Nest.js Request lifecycle
By Yariv Gilad
Nest.js Request lifecycle
- 6,062