Introduction:
NestJS, a powerful and modular Node.js framework, has gained widespread popularity for building scalable and maintainable server-side applications. At the core of NestJS lies a well-defined request lifecycle, orchestrating the flow of HTTP requests from initiation to response. In this blog post, we’ll embark on a journey to unravel the intricacies of the NestJS request lifecycle, exploring each stage and understanding how it contributes to the framework’s robustness.
Request lifecycle
Nest applications handle requests and produce responses in a sequence we refer to as the request lifecycle. With the use of middleware, pipes, guards, and interceptors, it can be challenging to track down where a particular piece of code executes during the request lifecycle, especially as global, controller level, and route level components come into play. In general, a request flows through middleware to guards, then to interceptors, then to pipes and finally back to interceptors on the return path (as the response is generated).
Middleware
Middleware is executed in a particular sequence. First, Nest runs globally bound middleware (such as middleware bound with app.use) and then it runs module bound middleware, which are determined on paths. Middleware are run sequentially in the order they are bound, similar to the way middleware in Express works. In the case of middleware bound across different modules, the middleware bound to the root module will run first, and then middleware will run in the order that the modules are added to the imports array.
Guards
Guard execution starts with global guards, then proceeds to controller guards, and finally to route guards. As with middleware, guards run in the order in which they are bound. For example:
@UseGuards(Guard1, Guard2)
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@UseGuards(Guard3)
@Get()
getCats(): Cats[] {
return this.catsService.getCats();
}
}
Guard1 will execute before Guard2 and both will execute before Guard3.
HINT: When speaking about globally bound vs controller or locally bound, the difference is where the guard (or other component is bound). If you are using app.useGlobalGuard() or providing the component via a module, it is globally bound. Otherwise, it is bound to a controller if the decorator precedes a controller class, or to a route if the decorator proceeds a route declaration.
Interceptors
Interceptors, for the most part, follow the same pattern as guards, with one catch: as interceptors return RxJS Observables, the observables will be resolved in a first in last out manner. So inbound requests will go through the standard global, controller, route level resolution, but the response side of the request (i.e., after returning from the controller method handler) will be resolved from route to controller to global. Also, any errors thrown by pipes, controllers, or services can be read in the catchError operator of an interceptor.
Pipes
Pipes follow the standard global to controller to route bound sequence, with the same first in first out in regards to the @UsePipes() parameters. However, at a route parameter level, if you have multiple pipes running, they will run in the order of the last parameter with a pipe to the first. This also applies to the route level and controller level pipes. For example, if we have the following controller:
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@UsePipes(RouteSpecificPipe)
@Patch(':id')
updateCat(
@Body() body: UpdateCatDTO,
@Param() params: UpdateCatParams,
@Query() query: UpdateCatQuery,
) {
return this.catsService.updateCat(body, params, query);
}
}
then the GeneralValidationPipe will run for the query, then the params, and then the body objects before moving on to the RouteSpecificPipe, which follows the same order. If any parameter-specific pipes were in place, they would run (again, from the last to first parameter) after the controller and route level pipes.
Filters
Filters are the only component that do not resolve global first. Instead, filters resolve from the lowest level possible, meaning execution starts with any route bound filters and proceeding next to controller level, and finally to global filters. Note that exceptions cannot be passed from filter to filter; if a route level filter catches the exception, a controller or global level filter cannot catch the same exception. The only way to achieve an effect like this is to use inheritance between the filters.
HINT: Filters are only executed if any uncaught exception occurs during the request process. Caught exceptions, such as those caught with a try/catch will not trigger Exception Filters to fire. As soon as an uncaught exception is encountered, the rest of the lifecycle is ignored and the request skips straight to the filter.
Conclusion:
Understanding the request lifecycle in NestJS is fundamental for building robust and efficient server-side applications. Each phase of the lifecycle contributes to the framework’s modular and extensible nature, allowing developers to implement complex features while maintaining code organization and readability. As you delve into the world of NestJS development, a solid grasp of the request lifecycle will empower you to build scalable and maintainable applications with confidence.
Top comments (0)