Interceptors wrap around the entire execution of a route handler, letting you run logic both before and after it runs, and even transform its result. This lesson covers the NestInterceptor interface and RxJS basics needed to use it.
What Is an Interceptor?
An interceptor implements NestInterceptor with an intercept() method that receives the execution context and a CallHandler. Calling next.handle() invokes the route handler and returns an RxJS Observable of its result, which the interceptor can transform, log around, or even replace.
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<unknown> {
const start = Date.now();
return next.handle().pipe(
tap(() => console.log(`Request took ${Date.now() - start}ms`)),
);
}
}
next.handle() triggers the actual route handler. The tap() operator runs a side effect (logging) once the handler's result is emitted, without changing the value itself.
Interceptors can run logic both before (intercept()'s own code) and after (via RxJS operators on next.handle()) the handler executes.
map() transforms the handler's return value before it's sent to the client.
tap() runs a side effect without altering the emitted value.
Interceptors can also catch errors from the handler using catchError().
Interceptor Cheatsheet
Common RxJS operators used inside interceptors.
Operator
Use Case
tap()
Logging, side effects, without changing the response
map()
Reshaping or wrapping the response payload
catchError()
Transforming or logging errors from the handler
timeout()
Aborting a request that takes too long
Transforming the Response Shape
A common use case is wrapping every successful response in a consistent envelope, like { data: ..., timestamp: ... }, without repeating that logic in every controller method.
Guards decide whether a request proceeds at all. Pipes transform individual arguments before the handler runs. Interceptors wrap the entire handler execution, running before and after, and can transform the handler's return value, something neither guards nor pipes can do.
Common Mistakes
Forgetting to call next.handle(), which prevents the actual route handler from ever running.
Mutating the emitted value inside tap() when map() is the operator meant for transformation.
Writing response-shaping logic separately in every controller instead of one shared interceptor.
Not understanding that interceptor logic before next.handle() runs before the handler, and operators after it run after.
Key Takeaways
Interceptors implement NestInterceptor and wrap around a handler's entire execution.
next.handle() returns an RxJS Observable of the handler's result to operate on.
map(), tap(), and catchError() are the most common operators used inside interceptors.
Interceptors are the right tool for logging, response transformation, and caching, not authorization.
Pro Tip
If you find yourself writing the same map() response-wrapping logic in multiple interceptors, extract it into one shared global interceptor instead, registered once via APP_INTERCEPTOR, so every endpoint gets a consistent response shape automatically.
You now understand interceptors. Next, build a Custom Interceptor for a real caching use case.