Introduction
The Request-Response Lifecycle in NestJS is a clearly defined sequence of steps that a request must pass through before a response is sent back to the client.
Simply put, it is like a production line: each department has its own task to check, transform, or process the data.
Key Components
The typical execution order is as follows:
- Incoming Request: The request sent from the client.
- Middleware: Performs tasks such as logging, basic authentication, or modifying the request object.
- Guards: Responsible for security, deciding whether this request is allowed to proceed (Authentication/Authorization).
- Interceptors (Pre-controller): Allows you to intervene in the logic before it reaches the Controller.
- Pipes: Used for data transformation (Transformation) and validating the input data (Validation).
- Controller: Where the main business logic resides and processes the request.
- Interceptors (Post-controller): Processes the data after the Controller returns it (e.g., changing the JSON structure).
- Exception Filters: The final "net" to catch and process any errors that arise during the above process.
Mastering this lifecycle helps you know exactly where to place code to optimize performance and security. For example: you should not check access rights in Pipes, as Guards are born to do that more efficiently before consuming resources to process data.
Detail
First, create a NestJS project as follows:
npm install -g @nestjs/cli
nest new {project name}
First, create the environment.service.ts file to manage environment variables
import {Injectable} from '@nestjs/common'
import {ConfigService} from '@nestjs/config'
@Injectable()
export class EnvironmentService {
customHeader: string
verifySecret: string
constructor(private readonly configService: ConfigService) {
this.customHeader = this.configService.get<string>('CUSTOM_HEADER') || ''
this.verifySecret = this.configService.get<string>('VERIFY_SECRET') || ''
}
}
The .env file information is as follows
CUSTOM_HEADER = x-origin-verify
VERIFY_SECRET = VERIFY_SECRET
Create the origin-auth.middleware.ts file to check the header in the request before allowing the request to continue being processed
import {ForbiddenException, Injectable, NestMiddleware} from '@nestjs/common'
import {NextFunction, Request, Response} from 'express'
import {EnvironmentService} from 'src/service/environment.service'
@Injectable()
export class OriginAuthMiddleware implements NestMiddleware {
constructor(private readonly envService: EnvironmentService) {}
use(req: Request, res: Response, next: NextFunction) {
console.log('Middleware')
const secret = req.headers[this.envService.customHeader]
if (
this.envService.verifySecret &&
secret !== this.envService.verifySecret
) {
throw new ForbiddenException(
'Access denied: Direct access is not allowed'
)
}
next()
}
}
Create the test.interceptor.ts file to process the interceptor, the code implemented before the return is for Interceptors (Pre-controller) before entering the controller, and the code inside the return is for Interceptors (Post-controller) after exiting the controller
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common'
import {map, Observable} from 'rxjs'
@Injectable()
export class TestInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler<any>
): Observable<any> {
console.log('Interceptor Request')
const request = context.switchToHttp().getRequest()
request.customHeader = 'custom request header value'
return next.handle().pipe(
map(data => {
console.log('Interceptor Response')
return {
statusCode: context.switchToHttp().getResponse().statusCode,
data,
timestamp: new Date().toISOString(),
}
})
)
}
}
Create the test.pipe.ts file to transform data, for the value input param, depending on where you use the pipe, the value will change correspondingly, such as receiving from the Body request, query value, or param value
import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common'
@Injectable()
export class TestPipe implements PipeTransform<any> {
async transform(value: {status: boolean}, metadata: ArgumentMetadata) {
console.log('Pipe')
if (!value.status) {
throw new BadRequestException('Validation failed')
}
return value
}
}
Create the test-exception.filter.ts file to catch errors if any occur throughout the lifecycle, usually used to show a friendly message to the user
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common'
import {Request, Response} from 'express'
import {EnvironmentService} from 'src/service/environment.service'
@Catch(HttpException)
export class TestExceptionFilter implements ExceptionFilter {
constructor(private readonly envService: EnvironmentService) {}
catch(exception: HttpException, host: ArgumentsHost) {
console.log('ExceptionFilter')
const ctx = host.switchToHttp()
const request = ctx.getRequest<Request>()
const response = ctx.getResponse<Response>()
const status = exception.getStatus()
response.setHeader(
'exception-filter',
request.headers[this.envService.customHeader] + ' updated'
)
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
message: 'Catched with ExceptionFilter',
})
}
}
Create test.controller.ts to process main logic, you can use lifecycles at the route level or individual api level or apply to both depending on the case
import {
Body,
Controller,
Logger,
Post,
UseFilters,
UseGuards,
UseInterceptors,
UsePipes,
} from '@nestjs/common'
import {TestExceptionFilter} from 'src/exception-filter/test-exception.filter'
import {TestGuard} from 'src/guard/test.guard'
import {TestInterceptor} from 'src/interceptor/test.interceptor'
import {TestPipe} from 'src/pipe/test.pipe'
@Controller('test')
@UseGuards(TestGuard)
@UseInterceptors(TestInterceptor)
@UsePipes(TestPipe)
@UseFilters(TestExceptionFilter)
export class TestController {
private readonly logger = new Logger(TestController.name)
@Post('life-cycle')
@UseGuards(TestGuard)
@UseInterceptors(TestInterceptor)
@UsePipes(TestPipe)
@UseFilters(TestExceptionFilter)
async lifeCycle(@Body() dto: {status: boolean}) {
this.logger.log('Controller')
return {dto, message: 'Success'}
}
}
Update the app.module.ts file to use lifecycles, you can add lifecycles to providers to apply to that entire module, for middleware, use the configure method to config for the routes you want to use (here I am using * to apply to all routes)
import {
MiddlewareConsumer,
Module,
NestModule,
} from '@nestjs/common'
import {ConfigModule} from '@nestjs/config'
import {OriginAuthMiddleware} from './middleware/origin-auth.middleware'
import {EnvironmentService} from './service/environment.service'
import {APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE} from '@nestjs/core'
import {TestGuard} from './guard/test.guard'
import {TestPipe} from './pipe/test.pipe'
import {TestExceptionFilter} from './exception-filter/test-exception.filter'
import {TestInterceptor} from './interceptor/test.interceptor'
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
],
controllers: [TestController],
providers: [
{
provide: APP_GUARD,
useClass: TestGuard,
},
{
provide: APP_INTERCEPTOR,
useClass: TestInterceptor,
},
{
provide: APP_PIPE,
useClass: TestPipe,
},
{
provide: APP_FILTER,
useClass: TestExceptionFilter,
},
EnvironmentService,
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(OriginAuthMiddleware)
.forRoutes('*')
}
}
Update the main.ts file, when using lifecycles here, it applies at the global level, affecting the entire server application
import {NestFactory} from '@nestjs/core'
import {AppModule} from './app.module'
import {TestExceptionFilter} from './exception-filter/test-exception.filter'
import {TestGuard} from './guard/test.guard'
import {TestPipe} from './pipe/test.pipe'
import {OriginAuthMiddleware} from './middleware/origin-auth.middleware'
import {TestInterceptor} from './interceptor/test.interceptor'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
const originAuthMiddleware = app.get(OriginAuthMiddleware)
const testGuard = app.get(TestGuard)
const testInterceptor = app.get(TestInterceptor)
const testPipe = app.get(TestPipe)
const testExceptionFilter = app.get(TestExceptionFilter)
app.use(originAuthMiddleware)
app.useGlobalGuards(testGuard)
app.useGlobalInterceptors(testInterceptor)
app.useGlobalPipes(testPipe)
app.useGlobalFilters(testExceptionFilter)
await app.listen(process.env.PORT ?? 3000)
}
bootstrap()
Use Postman to check the results as follows
Check the log you will see the corresponding running order of the lifecycle
Happy coding!
See more articles here.
Comments
Post a Comment