Binding & Global Interceptors
Writing an interceptor is only half the job — Nest has to know where to apply it. Interceptors can be bound at three scopes: a single method, an entire controller, or the whole application. Each scope trades reach for precision, and the global scope comes in two flavours (one of which keeps full dependency injection). This page covers every binding mechanism and explains how Nest orders interceptors when several are active at once.
Binding with @UseInterceptors
The @UseInterceptors() decorator attaches one or more interceptors to a method or a controller. Apply it to a single handler and only that route is wrapped; apply it to the controller class and every route inside inherits it. You can pass a class (Nest instantiates it through the DI container) or a ready-made instance (useful when you need to pass constructor arguments yourself).
// cats.controller.ts
import { Controller, Get, Post, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';
import { TransformInterceptor } from './transform.interceptor';
@Controller('cats')
@UseInterceptors(LoggingInterceptor) // applies to ALL routes below
export class CatsController {
@Get()
findAll() {
return [{ id: 1, name: 'Pixel' }];
}
@Post()
@UseInterceptors(TransformInterceptor) // applies to THIS route only
create() {
return { id: 2, name: 'Byte' };
}
}
Passing a class is almost always preferred: it lets Nest manage the lifecycle and inject dependencies, and it lets the framework reuse a single instance across the whole module rather than allocating one per request.
Prefer passing the interceptor class rather than
new LoggingInterceptor(). Instances you create yourself sit outside the DI container, so they cannot receive injected providers and you lose the framework’s instance reuse.
Global interceptors with useGlobalInterceptors
To wrap every route in the application, register the interceptor globally during bootstrap with app.useGlobalInterceptors(). This is the simplest global option, but it has one important limitation: because you instantiate the interceptor outside any module, it cannot use dependency injection.
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
await app.listen(3000);
}
bootstrap();
Global interceptors with APP_INTERCEPTOR (DI-friendly)
When a global interceptor needs to inject services — a logger, a config, a cache manager — register it as a provider using the APP_INTERCEPTOR token from @nestjs/core. Nest then constructs the interceptor inside the module context, so constructor injection works exactly as it does for any provider. You can register the token multiple times; each entry adds another global interceptor.
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './logging.interceptor';
import { TransformInterceptor } from './transform.interceptor';
@Module({
providers: [
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
],
})
export class AppModule {}
Because the interceptor is now a real provider, it can inject anything available in its module:
// logging.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
Logger,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const { method, url } = context.switchToHttp().getRequest();
const start = Date.now();
return next
.handle()
.pipe(tap(() => this.logger.log(`${method} ${url} +${Date.now() - start}ms`)));
}
}
Scope comparison
| Binding | Where | DI support | Typical use |
|---|---|---|---|
@UseInterceptors() on a method | One handler | Yes | Route-specific transforms or caching |
@UseInterceptors() on a controller | All routes in a controller | Yes | Per-feature cross-cutting logic |
app.useGlobalInterceptors() | Whole app | No | Quick global wiring, no dependencies |
APP_INTERCEPTOR provider | Whole app | Yes | Global logging, response shaping with deps |
Execution order
When multiple interceptors apply to the same request, Nest runs them in a predictable order. The pre-handler logic (everything before next.handle()) executes outermost first: global, then controller, then method. The handler runs, and then the post-handler logic (RxJS operators chained onto the stream) unwinds in reverse — method, then controller, then global. It behaves exactly like nested function calls or middleware onion layers.
Within a single @UseInterceptors(A, B) call, the array order is the nesting order: A wraps B.
// Two interceptors that log on the way in and the way out.
@Controller('demo')
@UseInterceptors(OuterInterceptor)
export class DemoController {
@Get()
@UseInterceptors(InnerInterceptor)
ping() {
return 'pong';
}
}
Output:
[OuterInterceptor] before
[InnerInterceptor] before
[InnerInterceptor] after
[OuterInterceptor] after
Order matters for correctness, not just logging. A caching interceptor must sit outside a transform interceptor so the cached value is the already-transformed payload — bind it globally or higher in the chain accordingly.
Best Practices
- Reach for
APP_INTERCEPTORwhenever a global interceptor needs injected dependencies; reserveuseGlobalInterceptors(new ...)for genuinely dependency-free logic. - Bind at the narrowest scope that satisfies the requirement — method-level over controller-level over global — to keep behaviour explicit and easy to reason about.
- Always pass interceptor classes to
@UseInterceptors()so Nest manages instantiation, lifecycle, and DI. - Remember the onion model: pre-handler code runs outside-in, post-handler operators run inside-out. Design ordering deliberately.
- Place caching interceptors outermost so they short-circuit before downstream transforms and the handler ever run.
- You may register
APP_INTERCEPTORmore than once; each entry stacks another global interceptor in registration order.