The sentry at the outer post counts those coming in. The treasury guard decides who may enter. Both end their duty at the same moment - when the visitor crosses the threshold. Neither sees what he walks out with.
The courier does. He rides to the governor with an order, waits for the reply, and brings it back - and along the way he may add something to it. An interceptor is the first mechanism of the request cycle that also runs code after the handler has finished, and that single sentence explains almost everything it does.
An interceptor is a class implementing the
interface. Not NestInterceptor
CanActivate - that belongs to the guards of the previous lesson; not PipeTransform - those are pipes, which come right after this lesson; not ExceptionFilter - that one deals with catching errors. The interface calls for a single method:1import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2import { Observable } from 'rxjs';
3
4@Injectable()
5export class AuditInterceptor implements NestInterceptor {
6 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
7 return next.handle();
8 }
9}The signature has three parts written in this order:
opens the method, intercept(
is the first argument, and context: ExecutionContext,
the second. The interceptor above does nothing yet - it passes the request on and hands back the result untouched.next: CallHandler)
You already know
ExecutionContext from guards: it is the same object, with the same getHandler() and switchToHttp() methods. What is new is CallHandler - a handle to the rest of the chain. Until you call handle() on it, the controller's handler does not run at all.An interceptor's work divides into four stages, always in this order:
intercept() method.next.handle()).The division is plain to the eye in the code, and one boundary is all you need to remember: everything you write before
is the before phase; everything that goes into return next.handle()
is the after phase. A single method covers both legs of the courier's journey - the ride out and the ride home..pipe(...)
That is exactly why an interceptor can measure how long a request took, which neither middleware nor a guard can do: it records a mark in the before phase and reads it in the after phase, within one method and off the same variable.
next.handle() does not return finished data but an Observable - a stream from the RxJS library into which the response will arrive. Operations on the stream are attached with the .pipe() method, and the operator most commonly used to execute logic after receiving a response is tap:1@Injectable()
2export class LoggingInterceptor implements NestInterceptor {
3 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
4 const start = Date.now();
5
6 return next.handle().pipe(
7 tap(() => console.log(`Time: ${Date.now() - start}ms`)),
8 );
9 }
10}const start = Date.now() runs in the before phase, the function inside tap in the after phase, once the response is back. The difference between the two readings is the time the request took.The name
tap comes from wiretapping: the operator looks at the value flowing past and lets it through unchanged. That is precisely what we want for logging - a client's response must not depend on whether we happen to be writing to the console. Its three neighbours in RxJS do something else: map replaces the value, filter can hold it back, and reduce folds a whole stream into one value and waits for it to end - in an HTTP request there is a single value, so there is nothing to fold.When the response really is meant to look different, you reach for
map. The typical use is a common envelope for every endpoint:1@Injectable()
2export class TransformInterceptor implements NestInterceptor {
3 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
4 return next.handle().pipe(
5 map((data) => ({ success: true, data })),
6 );
7 }
8}The controller returns a list of tributes and the client receives
{ success: true, data: [...] } - and the same at every other endpoint, without adding a single line to any controller. The difference from the previous example fits in one sentence: tap watches, map replaces. If the function in tap returns something, it is ignored; what the function in map returns becomes the response.RxJS has more such operators, and some are useful in interceptors straight away:
catchError catches an exception thrown by the handler, timeout cuts off a request that takes too long. All of them go into the same .pipe().An interceptor is attached with the
@UseInterceptors decorator - on a method or on a whole controller:1@Controller('tributes')
2@UseInterceptors(LoggingInterceptor)
3export class TributesController {
4 @Get()
5 findAll() {
6 return this.tributesService.findAll();
7 }
8}You pass the class, not an instance of it - creation is left to dependency injection, which is what lets an interceptor take its own dependencies in the constructor. Several interceptors separated by commas form a chain: the before phase runs in the order written, the after phase in reverse, because the stream comes home the way it went.
When the envelope is to cover the whole application, you register the interceptor globally:
app.useGlobalInterceptors(new TransformInterceptor()) in main.ts, or as a provider with the APP_INTERCEPTOR token. The second road is the better one wherever the interceptor needs something itself - a provider goes through dependency injection, new does not.The courier rides out and comes back, @name:
NestInterceptor interface - not CanActivate, not PipeTransform, not ExceptionFilter,intercept( → context: ExecutionContext, → next: CallHandler),intercept() → logic before passing the request (before) → executing the route handler (next.handle()) → response processing (after),return next.handle() is the before phase, inside .pipe(...) is the after phase,next.handle() returns an Observable; without calling it the handler never runs,tap is the operator most commonly used for logic after receiving a response - it watches the value and lets it through unchanged, unlike map (replaces), filter (holds back) and reduce (folds the stream),const start = Date.now() in the before phase, tap(() => ...) with a second Date.now() in the after phase,map((data) => ({ success: true, data })) wraps the response in a common envelope,@UseInterceptors(LoggingInterceptor), globally through useGlobalInterceptors or the APP_INTERCEPTOR token.In the next lesson you will meet pipes - the fourth and last mechanism of the cycle, which takes on the individual arguments of a method before it runs at all. For now remember: middleware and guards work on the way in; the interceptor alone rides both ways.