Photo by Eric Er on Unsplash

On this page

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

Diagram 1
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
  1. Middleware runs first, before Nest has matched the request to a handler.
  2. Guards decide whether the request may reach the handler at all.
  3. Interceptors run code before the handler and can also act on its result.
  4. Pipes transform and validate each handler argument.
  5. The route handler runs, usually by calling a service.
  6. Interceptors see the returned value on its way out, in reverse order.
  7. 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:

src/cats/cats.controller.ts
@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 in main.ts, or a provider with the APP_GUARD, APP_INTERCEPTOR, APP_PIPE, or APP_FILTER token 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

  1. 1. In which order do the stages run before the route handler?

Search articles

Type a word to search published articles.

Open the search page