Suggest an editImprove this articleRefine the answer for “What are interceptors in NestJS?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**NestJS interceptor** is a class that wraps route handler execution via RxJS Observables, running logic before and after the handler fires. ```typescript @Injectable() export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const now = Date.now(); return next.handle().pipe(tap(() => console.log(`Elapsed: ${Date.now() - now}ms`))); } } ``` **Key point:** code before `next.handle()` runs pre-handler; `.pipe()` operators run post-handler.Shown above the full answer for quick recall.Answer (EN)Image**NestJS interceptor** is a class that wraps route handler execution using RxJS Observables, running custom logic both before the handler fires and after it responds. ## Theory ### TL;DR - Interceptors sit in the pipeline after guards and wrap the handler on both sides - `next.handle()` returns an `Observable`; everything inside `.pipe()` runs post-handler - Analogy: a security checkpoint that scans your bag before boarding and stamps it after you land - Use them for logging, response wrapping, caching, and exception mapping across multiple routes - Not a replacement for guards (auth) or pipes (input validation); those run at different stages ### Quick example ```typescript 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> { console.log('Before handler'); const now = Date.now(); return next.handle().pipe( tap(() => console.log(`After handler. Elapsed: ${Date.now() - now}ms`)), ); } } ``` Code before `next.handle()` runs first. The `tap` callback runs after the handler completes. That split is the entire model. ### Key difference vs middleware Express middleware operates on raw `req` and `res` objects in a linear chain before the handler. Interceptors wrap the handler's `Observable` directly, giving you async stream control: `retry`, `timeout`, `catchError`, all composable with standard RxJS operators. Post-handler logic is cleaner here because you react to what the handler actually returned, rather than modifying `res` imperatively after the fact. ### When to use - Global response envelope → interceptor (wrap every response in `{ data, success, timestamp }`) - Execution time logging across all routes → interceptor - Cache hit short-circuit → interceptor (`of(cached)` skips the handler entirely) - Exception type mapping (DB error to HTTP 503) → interceptor with `catchError` - Auth check → guard (runs before interceptors, short-circuits cleanly) - Input validation → pipe ### How it works internally When a request arrives, NestJS resolves the interceptor chain via reflection metadata and calls each `intercept()` in registration order. `CallHandler.handle()` triggers the actual route handler when subscribed. Your `.pipe()` sits on top of that Observable. Errors travel through `throwError()`, which is why `catchError` catches handler failures but `tap` does not. Global interceptors registered with `APP_INTERCEPTOR` run in the order they appear in `providers`. Worth knowing if you have a transform interceptor and a logging interceptor where order affects what actually gets logged. ### Applying interceptors ```typescript // Method level @Get() @UseInterceptors(LoggingInterceptor) findAll() { ... } // Controller level @Controller('users') @UseInterceptors(TransformInterceptor) export class UsersController {} // Global via module (supports DI) @Module({ providers: [ { provide: APP_INTERCEPTOR, useClass: TransformInterceptor }, { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, ], }) export class AppModule {} ``` ### Request pipeline position ``` Request → Middleware → Guards → Interceptors (pre-handler) → Pipes → Route Handler → Interceptors (post-handler) → Exception Filters Response ``` ### Common mistakes **Mistake 1: Calling `.then()` on `next.handle()`** ```typescript // Wrong return next.handle().then(data => ({ wrapped: data })); // TypeError at runtime ``` `next.handle()` returns an `Observable`, not a `Promise`. Use `.pipe(map(...))` instead. This is the most common NestJS interceptor error on Stack Overflow. **Mistake 2: Using `tap()` for error handling** ```typescript // Wrong - errors from the handler never reach tap() return next.handle().pipe( tap(() => { throw new Error('boom'); }), ); ``` `tap` only fires on successful emissions. Handler errors bypass it completely. Use `catchError` for anything on the error path, otherwise production errors go unlogged. **Mistake 3: Accidentally skipping the handler** ```typescript // Missing return next.handle() on cache miss async intercept(ctx: ExecutionContext, next: CallHandler) { const cached = await this.cache.get(key); if (cached) return of(cached); // forgot: return next.handle().pipe(...) } ``` This silently returns `undefined` for every non-cached request. Always make sure both branches return something. **Mistake 4: Forgetting the `Promise<Observable>` return type for async intercept** ```typescript // Wrong return type annotation async intercept(ctx, next): Observable<any> { ... } // TypeScript complains // Correct async intercept(ctx, next): Promise<Observable<any>> { ... } ``` The moment you add `async` to `intercept()`, the return type becomes `Promise<Observable<any>>`, not `Observable<any>`. TypeScript will catch this, but it confuses devs who mix async/await with Observables for the first time. ### Real-world usage - NestJS boilerplates → `TransformInterceptor` for consistent `{ success: true, data }` envelope on every endpoint - Prisma + NestJS setups → query timing interceptor to surface slow queries in logs - BullMQ job processors → global interceptor to normalize job result shapes - Error monitoring (Sentry, Datadog) → global `catchError` interceptor before exception filters catch anything ### Follow-up questions **Q:** Where exactly do interceptors sit relative to guards and pipes? **A:** After guards on the way in, after the handler on the way out. Guards can reject a request before an interceptor ever runs. Pipes run between interceptors and the handler. **Q:** Why RxJS instead of plain async/await? **A:** Observables let you compose `retry`, `timeout`, and error recovery declaratively. With callbacks you would wire those manually. The `timeout` operator alone justifies the dependency for any production API. **Q:** How do you apply an interceptor only to POST requests? **A:** Check `context.switchToHttp().getRequest().method` inside `intercept()` and call `next.handle()` conditionally. Or use `@UseInterceptors()` only on the POST-decorated method at the controller level. **Q:** What is the overhead of a global interceptor? **A:** Around 1-2ms per request for typical transform or logging work. Measure it with a timing interceptor before treating it as a problem. **Q (senior):** Implement a timeout interceptor for slow DB queries. **A:** `return next.handle().pipe(timeout(5000), catchError(() => { throw new HttpException('Request timeout', 408); }))`. The `timeout` RxJS operator emits an error if the Observable does not complete within the given milliseconds, which you then map to an HTTP response. ## Examples ### Basic: request logging ```typescript 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 req = context.switchToHttp().getRequest(); const now = Date.now(); return next.handle().pipe( tap(() => { console.log(`${req.method} ${req.url} - ${Date.now() - now}ms`); }), ); } } ``` Register globally via `{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }`. Every route now logs method, path, and elapsed time after the handler returns, with zero changes to individual controllers. ### Intermediate: API response envelope ```typescript import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; interface Response<T> { data: T; success: boolean; timestamp: string; } @Injectable() export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> { intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> { return next.handle().pipe( map((data) => ({ data, success: true, timestamp: new Date().toISOString(), })), ); } } // GET /products // Before: [{ id: 1, name: 'Keyboard' }] // After: { data: [{ id: 1, name: 'Keyboard' }], success: true, timestamp: "2024-..." } ``` Apply this globally and every endpoint returns a consistent shape. Frontend code can always read `response.data` without handling different formats per endpoint. ### Advanced: cache interceptor with short-circuit ```typescript import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable, of } from 'rxjs'; import { tap, catchError } from 'rxjs/operators'; import { HttpException, HttpStatus } from '@nestjs/common'; import { Cache } from 'cache-manager'; @Injectable() export class CacheInterceptor implements NestInterceptor { constructor(private cacheManager: Cache) {} async intercept( context: ExecutionContext, next: CallHandler, ): Promise<Observable<any>> { const key = context.switchToHttp().getRequest().url; const cached = await this.cacheManager.get(key); if (cached) return of(cached); // handler never runs return next.handle().pipe( tap(async (data) => { await this.cacheManager.set(key, data, 300); // 5 min TTL }), catchError(() => { throw new HttpException('Service unavailable', HttpStatus.SERVICE_UNAVAILABLE); }), ); } } ``` When `of(cached)` returns, the route handler is never invoked. On a miss the handler runs, stores the result, and returns it. One edge case worth knowing: concurrent requests arriving before the cache is populated all hit the DB simultaneously. That is the cache stampede problem and solving it requires a separate lock on top of this pattern.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.