Interceptors
Part of the TypeScript and NestJS section of Coddy's Node.js journey. Lesson 23 of 24.
You want every answer to say how long it took, without touching each handler. An interceptor wraps the handler: code before next.handle() runs before it, and code on what next.handle() returns runs after it.
intercept(context: ExecutionContext, next: CallHandler) {
console.log('before');
return next.handle().pipe(
tap(() => console.log('after')),
);
}next.handle() gives the handler's result as an RxJS observable, a value that arrives later. .pipe(tap(...)) runs a function when the value arrives, before Nest sends it; this pipe has nothing to do with Nest's validation pipes. Guards run before any interceptor, so a request a guard refuses never reaches it. Return the piped observable: Nest sends what it carries. context.switchToHttp().getResponse() gives res, whose setHeader adds a header.
app.useGlobalInterceptors(...) applies an interceptor to every route; @UseInterceptors(...) to one controller.
Challenge
EasyRun starts your tracker again, using src/main.ts. The separate practice files can stay in the tree; this server does not start them.
Clients and your own monitoring want to know how long each request took. Add a response header, X-Response-Time, to every successful response, without touching any route.
- Open
src/common/response-time.interceptor.ts. InResponseTimeInterceptor'sinterceptmethod, first note the current time withDate.now(). Code at the top ofinterceptruns before the route's method. - Get Express's
reswithcontext.switchToHttp().getResponse(). next.handle()runs the route's method and gives back its result as an RxJS observable: a value that arrives later. Keep returning it, but pass it throughtap, which is already imported fromrxjs, using the observable's.pipe()method. The function you givetapruns when the result arrives, before Nest sends it. In that function, callres.setHeaderto setX-Response-Timeto the milliseconds since you noted the time, followed byms, like3ms. (RxJS's.pipe()has nothing to do with chapter 3's validation pipes. It chains a step onto the observable.)- In
src/main.ts, register the interceptor for the whole app withapp.useGlobalInterceptors(...), passing a newResponseTimeInterceptor, next to the pipe and the filter. - Response bodies must not change.
GET /tasksstill returns{ "items": [...], "page": 1, "total": 3, "pages": 2 }.
Try it: send GET /tasks and open the Headers tab of the response. You see X-Response-Time with a value like 0ms or 2ms. GET /stats has it too, and so does a POST of { "title": "Buy milk" } sent with the header x-api-key: tasks-key-123. Then send that POST without the key: 403, and no X-Response-Time. Nest runs guards before interceptors, so the guard refused the request before your interceptor started. An error thrown inside a route skips the header too, because tap only runs when a result arrives.
Try it yourself
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class ResponseTimeInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
// Note the time, let the handler run, then set X-Response-Time.
return next.handle();
}
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in TypeScript and NestJS
Practice on your own: Express playground