This chapter follows Providers and dependency injection in NestJS.
In plain Express, everything that happens before a handler is middleware, and the order is whatever order app.use() was called in. Nest splits that work into five kinds of component, each with one job and a fixed position. Knowing the positions answers most “where does this code go?” questions: authentication belongs where the handler is already known, validation belongs right before the arguments are handed over, and error formatting belongs in one place at the end. The five chapters after this one cover each stage in turn.
The path of a request
flowchart LR req(["HTTP request"]) --> mw[Middleware] mw --> guards[Guards] guards --> icIn["Interceptors (before)"] icIn --> pipes[Pipes] pipes --> handler["Route handler"] handler --> icOut["Interceptors (after)"] icOut --> res(["HTTP response"]) mw -. throws .-> filters["Exception filters"] guards -. throws .-> filters icIn -. throws .-> filters pipes -. throws .-> filters handler -. throws .-> filters icOut -. throws .-> filters filters --> res
- Middleware runs first, before Nest has matched the request to a handler.
- Guards decide whether the request may reach the handler at all.
- Interceptors run code before the handler and can also act on its result.
- Pipes transform and validate each handler argument.
- The route handler runs, usually by calling a service.
- Interceptors see the returned value on its way out, in reverse order.
- Exception filters turn anything thrown along the way into a response.
The response leaves through the interceptors or through a filter, never both.
What each stage is for
| Stage | Job | Knows the handler? | Typical use |
|---|---|---|---|
| Middleware | Inspect or change the raw request and response | No | Request IDs, CORS, compression, body parsing |
| Guard | Allow or deny the request | Yes | Authentication, roles, feature flags |
| Interceptor | Wrap the handler call | Yes | Logging with timing, response mapping, caching, timeouts |
| Pipe | Transform or reject one argument | Yes, and the argument | Parsing IDs, validating DTOs, default values |
| Exception filter | Turn an exception into a response | Yes | Error format, logging, mapping domain errors to HTTP |
The “knows the handler” column is why the stages exist separately. Middleware runs before routing, so it cannot tell which controller method will run or what metadata it carries. Guards, interceptors, pipes, and filters all receive an ExecutionContext (or, for filters, an ArgumentsHost) that points at the handler and its class. That is what lets a guard read a @Roles('admin') decorator from the method it protects.
Where a component is bound
Guards, interceptors, pipes, and filters can each be bound at three levels, and pipes at a fourth:
@Controller('cats')@UseGuards(AuthGuard)@UseInterceptors(LoggingInterceptor)export class CatsController { @Post() @UseGuards(RolesGuard) @UsePipes(new ValidationPipe({ whitelist: true })) create(@Body() dto: CreateCatDto) {}
@Get(':id') findOne(@Param('id', ParseIntPipe) id: number) {}}- Global:
app.useGlobalGuards()and its siblings inmain.ts, or a provider with theAPP_GUARD,APP_INTERCEPTOR,APP_PIPE, orAPP_FILTERtoken in any module. Only the provider form can inject dependencies. - Controller: a
@UseGuards(),@UseInterceptors(),@UsePipes(), or@UseFilters()decorator on the class. - Route: the same decorators on one method.
- Parameter (pipes only): a pipe passed into
@Param(),@Query(), or@Body().
Middleware is bound differently, in a module’s configure() method or with app.use(), and is covered in Middleware in NestJS.
The order within each stage
When the same kind of component is bound at several levels, all of them run. The order follows from how specific each level is:
| Stage | Order |
|---|---|
| Middleware | Global (app.use), then module-bound |
| Guards | Global, then controller, then route |
| Interceptors, before the handler | Global, then controller, then route |
| Pipes | Global, then controller, then route, then parameter |
| Interceptors, after the handler | Route, then controller, then global |
| Exception filters | Route, then controller, then global; the first match handles it |
Interceptors reverse on the way out because each one wraps the next: the global interceptor’s “before” code runs first, so its “after” code runs last. Filters are the only stage that starts at the most specific level. Once a route-level filter catches an exception, no controller or global filter sees it.
Within one level, components run in the order they are listed, so @UseGuards(AuthGuard, RolesGuard) authenticates before it checks roles.
When something fails
Any stage can stop the request by throwing. A guard that returns false makes Nest throw a ForbiddenException. A pipe that rejects an argument throws a BadRequestException. Either way the handler never runs, and the exception goes to the filters. If no filter you registered matches, Nest’s built-in filter answers: an HttpException becomes its status code and message, and anything else becomes 500 Internal Server Error.
Exceptions thrown inside middleware also reach the exceptions layer. Middleware is not attached to a controller, though, so only global filters can catch them.
This is also the reason to throw rather than return error objects from services: one global filter can then give every error in the application the same JSON shape. Exception filters in NestJS shows how.
Next: Middleware in NestJS.
Check your understanding
Question 1 of 5
0 of 5 checked