Interceptors — Wrap the Pipeline

Run Code Before AND After the Handler

Interceptors — Wrap the Pipeline

Interceptors are bidirectional. They see the call going in and the response coming out — ideal for logging, timing, caching, and response shaping.

4 min read Level 3/5 #nestjs#interceptors
What you'll learn
  • Implement NestInterceptor with intercept()
  • Use RxJS operators to transform the response
  • Recognize built-in interceptors and when to reach for them

Middleware runs before. Filters run on errors. Interceptors run around the handler — they can mutate input on the way in, transform output on the way out, time the call, cache it, or short-circuit it entirely.

A Logging Interceptor

The intercept method gets the context and a CallHandler. Calling next.handle() runs the handler and returns an Observable of the response. RxJS operators let you tap or transform.

import {
  CallHandler, ExecutionContext, Injectable, NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
    const start = Date.now();
    const handler = ctx.getHandler().name;
    return next.handle().pipe(
      tap(() => console.log(`${handler} took ${Date.now() - start}ms`)),
    );
  }
}

Apply it like any pipeline piece: @UseInterceptors(LoggingInterceptor) on a controller or method, or globally with app.useGlobalInterceptors(...).

Transforming the Response

map lets you reshape what the handler returned before the client sees it. A common pattern: wrap every response in a { data: ... } envelope.

import { map } from 'rxjs';

@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
  intercept(_ctx: ExecutionContext, next: CallHandler) {
    return next.handle().pipe(map((data) => ({ data })));
  }
}

The handler can keep returning a User object — clients see { "data": { ... } } consistently across the API.

Timeouts and Cancellation

RxJS makes timeouts a one-liner. Cancel any handler that takes longer than 5 seconds:

import { catchError, throwError, timeout } from 'rxjs';
import { RequestTimeoutException } from '@nestjs/common';

intercept(_ctx: ExecutionContext, next: CallHandler) {
  return next.handle().pipe(
    timeout(5000),
    catchError((err) =>
      err.name === 'TimeoutError'
        ? throwError(() => new RequestTimeoutException())
        : throwError(() => err),
    ),
  );
}

Built-In Interceptors

Nest ships a few you’ll reach for regularly:

InterceptorWhat it does
ClassSerializerInterceptorRuns class-transformer on the response
CacheInterceptorCaches handler results in memory or Redis
LoggerErrorInterceptorLogs exceptions thrown by handlers

Combine them as you like — interceptors stack, and their pipe chains compose naturally.

When NOT to Use an Interceptor

  • Input validation? That’s pipes.
  • Access control? Guards.
  • Error response shaping? Exception filters (next lesson).

Interceptors are the right tool when you genuinely need to observe both the inbound call and the outbound response.

Exception Filters →