NestJS Interceptors

An interceptor is a class that wraps the execution of a route handler. It runs before and after the controller method — letting you transform the request, transform the response, add timing, cache results, or handle exceptions in a centralized way. Interceptors use RxJS observables to tap into both sides of the request-response cycle.

What Interceptors Can Do

  • Transform the response data (wrap every response in a standard envelope)
  • Add timing information to responses
  • Cache the result of a route handler
  • Log request and response data together
  • Override exceptions thrown by the handler
  • Extend basic handler behavior without touching the controller

Creating an Interceptor

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();
    console.log('Before route handler...');

    return next
      .handle()                                       // calls the route handler
      .pipe(
        tap(() => console.log(`After... ${Date.now() - now}ms`))
      );
  }
}

The next.handle() call triggers the actual controller method. Code before it runs before the handler. Code in the pipe() operator runs after the handler returns a response.

Interceptor Execution Flow

HTTP Request
     |
     v
Interceptor: before next.handle()
     |
     v
Route Handler (Controller method)
     |
     v
Interceptor: inside pipe() operators
     |
     v
HTTP Response

Response Transformation Interceptor

A very common use case: wrapping every API response in a consistent envelope structure:

import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => ({
        status: 'success',
        data: data,
        timestamp: new Date().toISOString(),
      }))
    );
  }
}

Before interceptor:

Response: { "id": 1, "name": "Alice" }

After interceptor:

Response:
{
  "status": "success",
  "data": { "id": 1, "name": "Alice" },
  "timestamp": "2025-01-15T10:00:00.000Z"
}

Caching Interceptor

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  private cache = new Map<string, any>();

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const key = request.url;

    if (this.cache.has(key)) {
      return of(this.cache.get(key));  // return cached result immediately
    }

    return next.handle().pipe(
      tap(data => this.cache.set(key, data))  // store result in cache
    );
  }
}

Applying Interceptors

// On a specific route
@Get()
@UseInterceptors(LoggingInterceptor)
findAll() { ... }

// On an entire controller
@Controller('users')
@UseInterceptors(TransformInterceptor)
export class UsersController { ... }

// Globally — applied to every route
app.useGlobalInterceptors(new TransformInterceptor());

Exception Mapping

Interceptors can catch errors thrown inside the handler and transform them using RxJS catchError:

import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

@Injectable()
export class ErrorInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      catchError(err => {
        // Transform or log the error before re-throwing
        console.error('Caught by interceptor:', err.message);
        return throwError(() => new InternalServerErrorException('Something went wrong'));
      })
    );
  }
}

Interceptors vs Middleware

Interceptors                       Middleware
────────────────────────────────   ────────────────────────────────
Runs before AND after handler      Runs before handler only
Access to response data            No access to response data
Uses RxJS observables              Uses callback (next function)
Applied with @UseInterceptors()    Applied with configure()
Has ExecutionContext                Has only req/res objects

Use interceptors when you need to transform responses, measure execution time, or apply any logic that depends on what the handler returned. Use middleware when you only need to process the incoming request — before knowing the result. The global response transform interceptor is one of the most practically useful patterns in any NestJS application because it ensures every endpoint returns data in a consistent, predictable format.

Leave a Comment

Your email address will not be published. Required fields are marked *