Interceptors
An interceptor is a class that wraps a controller method call. It can run code before the method, after it, and change the returned value. That makes it handy for measuring response time or bringing all responses to one format.
Measuring time
nest g interceptor common/interceptors/timing --flat// src/common/interceptors/timing.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class TimingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const started = Date.now();
const handler = context.getHandler().name;
return next.handle().pipe(
tap(() => console.log(`${handler}: ${Date.now() - started} ms`)),
);
}
}next.handle() calls the controller method and returns an RxJS stream (Observable) — a promise-like object through which NestJS passes the result. Code before next.handle() runs before the method, and the tap() operator runs after it, when the response is ready. The rxjs library is already installed with NestJS.
Applying it
// src/posts/posts.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { TimingInterceptor } from '../common/interceptors/timing.interceptor';
import { PostsService } from './posts.service';
@Controller('posts')
@UseInterceptors(TimingInterceptor)
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Get()
findAll() {
return this.postsService.findAll();
}
}Open /posts — the server terminal shows a line like findAll: 2 ms. For the whole application the interceptor is enabled in src/main.ts: app.useGlobalInterceptors(new TimingInterceptor()).
Changing the response
The map() operator replaces the method's result. Let us wrap every response in an object with a data field:
// src/common/interceptors/wrap-data.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { map, Observable } from 'rxjs';
@Injectable()
export class WrapDataInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
return next.handle().pipe(map((data) => ({ data })));
}
}With this interceptor /posts returns {"data":[...]} instead of a bare array. The controllers do not change.
The order of request handling
| Step | Tool | Typical task |
|---|---|---|
| 1 | Middleware | Request log, headers |
| 2 | Guards | Access checks |
| 3 | Interceptors (before the method) | Timing, cache |
| 4 | Pipes | Checking and converting arguments |
| 5 | Controller method | The actual work |
| 6 | Interceptors (after the method) | Changing the response |
| 7 | Exception filters | The response when any step fails |
Common mistake
An interceptor that does not call next.handle() or does not return its result cuts the request off: the controller method never runs and the client gets no response. intercept() must always return next.handle() — with operators or without.