NestJS — Enterprise Node.js

Interceptors

17 min Lesson 14 of 48

Interceptors

An interceptor wraps a handler, running logic before and after it. Inspired by Aspect-Oriented Programming, interceptors are perfect for cross-cutting behaviour you do not want to repeat in every handler: transforming responses, logging timings, caching, and adding timeouts.

The NestInterceptor interface

An interceptor implements intercept(context, next). The key is next.handle(): calling it runs the route handler and returns an RxJS Observable of the response. You add operators to that stream to act after the handler:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const now = Date.now(); // code here runs BEFORE the handler return next .handle() .pipe(tap(() => console.log(`Took ${Date.now() - now}ms`))); // code in .pipe() runs AFTER the handler } }

Transforming the response

A classic use is wrapping every response in a consistent envelope using the map operator:

import { map } from 'rxjs/operators'; @Injectable() export class TransformInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( map((data) => ({ success: true, data })), ); } } // handler returns { id: 1 } -> client receives { success: true, data: { id: 1 } }

Before and after, in one place

Because the logic before next.handle() and inside .pipe() live in the same method, an interceptor can measure duration, mutate the result, catch errors, or short-circuit with a cached value — all without touching the handler.

Common interceptor patterns

  • Response shaping — wrap data in a standard structure (with map).
  • Logging / timing — record how long each request takes (with tap).
  • Caching — return a cached value and skip the handler entirely.
  • Timeouts — fail a request that runs too long (with the timeout operator).
  • Serialization — strip sensitive fields (e.g. ClassSerializerInterceptor).

Applying interceptors

import { UseInterceptors } from '@nestjs/common'; @UseInterceptors(LoggingInterceptor) // method or controller level @Get() findAll() {} // or globally: app.useGlobalInterceptors(new TransformInterceptor());
Interceptors see the response; guards do not. A guard runs before the handler and only allows/denies. An interceptor runs around the handler and can read and reshape what it returns — that "after" phase is the difference.
Interceptors are built on RxJS. The handler's return value becomes an Observable. If you are unfamiliar with operators like map and tap, start with those two — they cover the majority of real-world interceptors.

Summary

Interceptors wrap a handler with before/after logic via intercept(context, next). Calling next.handle() runs the handler and returns an Observable you transform with RxJS operators like map (reshape) and tap (side effects). Use them for response envelopes, logging, caching, and timeouts, applied with @UseInterceptors(). Next: exception filters, which control what happens when something goes wrong.