Run Code Before AND After the Handler
Interceptors — Wrap the Pipeline
Interceptors are bidirectional. They see the call going in and the response coming out — ideal for logging, timing, caching, and response shaping.
What you'll learn
- Implement NestInterceptor with intercept()
- Use RxJS operators to transform the response
- Recognize built-in interceptors and when to reach for them
Middleware runs before. Filters run on errors. Interceptors run around the handler — they can mutate input on the way in, transform output on the way out, time the call, cache it, or short-circuit it entirely.
A Logging Interceptor
The intercept method gets the context and a CallHandler. Calling
next.handle() runs the handler and returns an Observable of the response.
RxJS operators let you tap or transform.
import {
CallHandler, ExecutionContext, Injectable, NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
const start = Date.now();
const handler = ctx.getHandler().name;
return next.handle().pipe(
tap(() => console.log(`${handler} took ${Date.now() - start}ms`)),
);
}
} Apply it like any pipeline piece: @UseInterceptors(LoggingInterceptor)
on a controller or method, or globally with app.useGlobalInterceptors(...).
Transforming the Response
map lets you reshape what the handler returned before the client sees it.
A common pattern: wrap every response in a { data: ... } envelope.
import { map } from 'rxjs';
@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
intercept(_ctx: ExecutionContext, next: CallHandler) {
return next.handle().pipe(map((data) => ({ data })));
}
} The handler can keep returning a User object — clients see
{ "data": { ... } } consistently across the API.
Timeouts and Cancellation
RxJS makes timeouts a one-liner. Cancel any handler that takes longer than 5 seconds:
import { catchError, throwError, timeout } from 'rxjs';
import { RequestTimeoutException } from '@nestjs/common';
intercept(_ctx: ExecutionContext, next: CallHandler) {
return next.handle().pipe(
timeout(5000),
catchError((err) =>
err.name === 'TimeoutError'
? throwError(() => new RequestTimeoutException())
: throwError(() => err),
),
);
} Built-In Interceptors
Nest ships a few you’ll reach for regularly:
| Interceptor | What it does |
|---|---|
ClassSerializerInterceptor | Runs class-transformer on the response |
CacheInterceptor | Caches handler results in memory or Redis |
LoggerErrorInterceptor | Logs exceptions thrown by handlers |
Combine them as you like — interceptors stack, and their pipe chains
compose naturally.
When NOT to Use an Interceptor
- Input validation? That’s pipes.
- Access control? Guards.
- Error response shaping? Exception filters (next lesson).
Interceptors are the right tool when you genuinely need to observe both the inbound call and the outbound response.
Exception Filters →