Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

StepToolTypical task
1MiddlewareRequest log, headers
2GuardsAccess checks
3Interceptors (before the method)Timing, cache
4PipesChecking and converting arguments
5Controller methodThe actual work
6Interceptors (after the method)Changing the response
7Exception filtersThe 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.

Thanks for reading!

Donate to the site — we work hard for you.