Interceptors
Interceptors wrap a handler to add logic before and after it runs, such as logging or transforming results.
Syntax
intercept(context, next: CallHandler): Observable<any>An interceptor wraps around your route handler. It can run code before the handler, transform the returned value after, or both. Interceptors are built on RxJS.
Typical uses
- Measuring how long a request takes.
- Reshaping every response into a standard envelope.
- Caching results.
An interceptor implements NestInterceptor. You call next.handle() to run the handler, then use RxJS operators like map or tap on the resulting stream.
Example
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
return next
.handle()
.pipe(tap(() => console.log(`Took ${Date.now() - now}ms`)));
}
}When to use it
- A developer wraps all handlers with a LoggingInterceptor that records the handler execution time and logs slow requests over 500 ms.
- A team uses a TransformInterceptor to unwrap every response into { data: <original> } so the API has a consistent envelope format.
- An engineer implements a CacheInterceptor that returns a cached response for GET requests and bypasses the handler entirely on cache hits.
More examples
Logging interceptor
Measures and logs handler execution time by wrapping next.handle() with an RxJS tap operator.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
return next.handle().pipe(
tap(() => console.log(`Handler took ${Date.now() - start}ms`)),
);
}
}Response transform interceptor
Wraps every response in a { data: ... } envelope using RxJS map, enforcing a consistent API response format.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, map } from 'rxjs';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(_ctx: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(map(data => ({ data })));
}
}Applying interceptor globally
Registers the TransformInterceptor globally so every handler response is wrapped without decorating individual controllers.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(3000);
}
bootstrap();
Discussion