Binding & Execution Order
Know the four ways to bind pipeline components and the exact order guards, interceptors, pipes and filters fire.
When several guards, interceptors, and pipes are all active, the difference between a working feature and a subtle bug is order. Seniors keep the two dimensions - where you bind and when it runs - clearly separated in their heads.
The four binding scopes
| Scope | How | DI? |
|---|---|---|
| Global (DI) | APP_GUARD / APP_INTERCEPTOR / APP_PIPE / APP_FILTER token | Yes |
| Global (plain) | app.useGlobalGuards(new ...) | No |
| Controller | @UseGuards() on the class | Yes |
| Handler | @UseGuards() on the method | Yes |
The execution order
For a request that succeeds, components fire in this sequence:
- Middleware
- Guards
- Interceptors (pre, before
next.handle()) - Pipes
- Route handler
- Interceptors (post, after the stream emits)
- Exception filters (only if something threw)
Within one layer: global to local
When several components of the same kind apply, they run global first, then controller, then handler. Interceptors additionally nest like an onion on the way out, so the first interceptor to start is the last to finish. Filters are the exception: the closest (handler-level) filter wins.
// Fires: GlobalGuard -> ControllerGuard -> HandlerGuard
@UseGuards(HandlerGuard)
@Get()
find() {}Why it matters
Auth belongs in a guard so it runs before pipes waste time validating a body the caller isn't allowed to send. Response shaping belongs in a post-interceptor so it sees the final value. Put logic in the wrong layer and it either runs too late or never.
Example
import {
Module,
Controller,
Get,
UseGuards,
UseInterceptors,
Injectable,
CanActivate,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { tap } from 'rxjs/operators';
@Injectable()
class OrderTracer implements NestInterceptor {
constructor(private readonly label: string) {}
intercept(_ctx: ExecutionContext, next: CallHandler) {
console.log(`${this.label}: before`);
return next.handle().pipe(tap(() => console.log(`${this.label}: after`)));
}
}
@Injectable()
class GlobalGuard implements CanActivate {
canActivate(_c: ExecutionContext) {
console.log('guard: global');
return true;
}
}
@Controller('demo')
@UseInterceptors(new OrderTracer('controller-interceptor'))
export class DemoController {
@Get()
@UseInterceptors(new OrderTracer('handler-interceptor'))
find() {
console.log('handler: running');
return 'ok';
}
}
@Module({
controllers: [DemoController],
providers: [
{ provide: APP_GUARD, useClass: GlobalGuard },
{ provide: APP_INTERCEPTOR, useValue: new OrderTracer('global-interceptor') },
],
})
export class DemoModule {}
// Observed log order for GET /demo:
// guard: global
// global-interceptor: before
// controller-interceptor: before
// handler-interceptor: before
// handler: running
// handler-interceptor: after
// controller-interceptor: after
// global-interceptor: afterWhen to use it
- A developer debugging 401 errors checks the binding order to confirm that the JWT guard runs before the RolesGuard and before any interceptor sees the request.
- A team decides to use APP_GUARD for JWT auth (global) and @UseGuards for RolesGuard (controller) knowing that globally-bound components run before locally-bound ones.
- An engineer uses module-level providers (APP_PIPE, APP_INTERCEPTOR) to register pipeline components so they participate in the DI container and can inject services.
More examples
Four binding levels
Shows the four binding levels: global (main.ts), module (APP_GUARD), controller class, and method — from lowest to highest specificity.
// 1. Global — main.ts
app.useGlobalGuards(new JwtAuthGuard());
// 2. Module — providers array
{ provide: APP_GUARD, useClass: RolesGuard }
// 3. Controller — decorator
@UseGuards(ThrottleGuard)
// 4. Method — decorator
@UseGuards(OwnershipGuard)Execution order in a request
Documents the exact 15-step execution order for a fully decorated route so developers can predict where each concern runs.
// For a guarded, intercepted, piped route the order is:
// ─ Inbound ─────────────────────────────────────────
// 1. Middleware (app.use / NestModule.configure)
// 2. Global guard (app.useGlobalGuards)
// 3. Controller guard (@UseGuards on class)
// 4. Method guard (@UseGuards on method)
// 5. Global interceptor (before next.handle)
// 6. Controller interceptor
// 7. Method interceptor
// 8. Global pipe
// 9. Controller pipe
// 10. Method / param pipe
// 11. Handler
// ─ Outbound ────────────────────────────────────────
// 12. Method interceptor (after next.handle)
// 13. Controller interceptor
// 14. Global interceptor
// 15. Exception filter (if error thrown)Global pipe via APP_PIPE for DI access
Registers ValidationPipe globally via APP_PIPE in a module provider, which (unlike app.useGlobalPipes) participates in the DI container.
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
@Module({
providers: [
{
provide: APP_PIPE,
useFactory: () => new ValidationPipe({ whitelist: true, transform: true }),
},
],
})
export class AppModule {}
Discussion