NestJS Interceptors
An interceptor is a class that wraps the execution of a route handler. It runs before and after the controller method — letting you transform the request, transform the response, add timing, cache results, or handle exceptions in a centralized way. Interceptors use RxJS observables to tap into both sides of the request-response cycle.
What Interceptors Can Do
- Transform the response data (wrap every response in a standard envelope)
- Add timing information to responses
- Cache the result of a route handler
- Log request and response data together
- Override exceptions thrown by the handler
- Extend basic handler behavior without touching the controller
Creating an Interceptor
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } 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();
console.log('Before route handler...');
return next
.handle() // calls the route handler
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`))
);
}
}
The next.handle() call triggers the actual controller method. Code before it runs before the handler. Code in the pipe() operator runs after the handler returns a response.
Interceptor Execution Flow
HTTP Request
|
v
Interceptor: before next.handle()
|
v
Route Handler (Controller method)
|
v
Interceptor: inside pipe() operators
|
v
HTTP Response
Response Transformation Interceptor
A very common use case: wrapping every API response in a consistent envelope structure:
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({
status: 'success',
data: data,
timestamp: new Date().toISOString(),
}))
);
}
}
Before interceptor:
Response: { "id": 1, "name": "Alice" }
After interceptor:
Response:
{
"status": "success",
"data": { "id": 1, "name": "Alice" },
"timestamp": "2025-01-15T10:00:00.000Z"
}
Caching Interceptor
@Injectable()
export class CacheInterceptor implements NestInterceptor {
private cache = new Map<string, any>();
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const key = request.url;
if (this.cache.has(key)) {
return of(this.cache.get(key)); // return cached result immediately
}
return next.handle().pipe(
tap(data => this.cache.set(key, data)) // store result in cache
);
}
}
Applying Interceptors
// On a specific route
@Get()
@UseInterceptors(LoggingInterceptor)
findAll() { ... }
// On an entire controller
@Controller('users')
@UseInterceptors(TransformInterceptor)
export class UsersController { ... }
// Globally — applied to every route
app.useGlobalInterceptors(new TransformInterceptor());
Exception Mapping
Interceptors can catch errors thrown inside the handler and transform them using RxJS catchError:
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
@Injectable()
export class ErrorInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError(err => {
// Transform or log the error before re-throwing
console.error('Caught by interceptor:', err.message);
return throwError(() => new InternalServerErrorException('Something went wrong'));
})
);
}
}
Interceptors vs Middleware
Interceptors Middleware ──────────────────────────────── ──────────────────────────────── Runs before AND after handler Runs before handler only Access to response data No access to response data Uses RxJS observables Uses callback (next function) Applied with @UseInterceptors() Applied with configure() Has ExecutionContext Has only req/res objects
Use interceptors when you need to transform responses, measure execution time, or apply any logic that depends on what the handler returned. Use middleware when you only need to process the incoming request — before knowing the result. The global response transform interceptor is one of the most practically useful patterns in any NestJS application because it ensures every endpoint returns data in a consistent, predictable format.
