This chapter follows Guards in NestJS.
Middleware and guards only see the request. Pipes only see one argument. An interceptor is the one stage that surrounds the handler call itself: it decides when the handler runs and receives its result as a stream it can change. That makes it the place for logging with timings, wrapping every response in the same envelope, timeouts, caching, and mapping errors from a library into HTTP exceptions.
The NestInterceptor contract
An interceptor implements NestInterceptor. Its intercept() method receives the ExecutionContext and a CallHandler. Calling next.handle() runs the rest of the pipeline (the pipes and the handler) and returns an RxJS Observable of the result. Code before next.handle() runs on the way in, and operators on the returned stream run on the way out.
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';import type { Request } from 'express';import { Observable, tap } from 'rxjs';
@Injectable()export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> { const { method, originalUrl } = context.switchToHttp().getRequest<Request>(); const started = Date.now();
return next.handle().pipe( tap(() => this.logger.log(`${method} ${originalUrl} ${Date.now() - started}ms`)), ); }}The handler does not have to return an Observable for this to work. Nest wraps a plain value or a Promise in one before the interceptor sees it. If intercept() never calls next.handle(), the handler never runs, which is how a cache interceptor returns a stored response.
Changing the response
map() replaces the value the handler returned. A response envelope is the usual example:
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';import { map, Observable } from 'rxjs';
@Injectable()export class EnvelopeInterceptor<T> implements NestInterceptor<T, { data: T }> { intercept(context: ExecutionContext, next: CallHandler<T>): Observable<{ data: T }> { return next.handle().pipe(map((data) => ({ data }))); }}Every route bound to this interceptor now answers { "data": ... }. A handler that injects @Res() without passthrough sends its own response, so its result never reaches map().
Timeouts and error mapping
Because the result is a stream, the RxJS error and time operators apply directly. This interceptor gives every request five seconds and turns the RxJS timeout into a 408:
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, RequestTimeoutException,} from '@nestjs/common';import { catchError, Observable, throwError, timeout, TimeoutError } from 'rxjs';
@Injectable()export class TimeoutInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> { return next.handle().pipe( timeout(5000), catchError((err) => throwError(() => (err instanceof TimeoutError ? new RequestTimeoutException() : err)), ), ); }}The timeout stops waiting, not the work. The handler’s database query keeps running in the background until it finishes. The same catchError shape maps library errors, for example a duplicate-key error from the database driver, to a ConflictException, although an exception filter is often the clearer place for that. If RxJS is new to you, Observables in JavaScript covers the model.
Binding interceptors
Interceptors bind like guards: @UseInterceptors() on a controller or a method, app.useGlobalInterceptors() in main.ts, or a provider with the APP_INTERCEPTOR token, which can inject dependencies.
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({ providers: [ { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, { provide: APP_INTERCEPTOR, useClass: TimeoutInterceptor }, ],})export class AppModule {}On the way in, global interceptors run before controller ones, and controller ones before route ones. On the way out the order reverses. The LoggingInterceptor registered first above is the outermost layer, so its timing includes the time spent in TimeoutInterceptor and in the handler.
Built-in interceptors
Nest ships two interceptors that cover common needs:
ClassSerializerInterceptorfrom@nestjs/commonruns the handler’s result throughclass-transformer. Properties decorated with@Exclude(), such as a password hash, are removed from the JSON. It only works when the handler returns class instances, not plain objects.CacheInterceptorfrom@nestjs/cache-managercachesGETresponses by URL.@CacheTTL()sets the lifetime per route. It is only safe for responses that are the same for every caller, because the cache key does not include the user.
import { Exclude } from 'class-transformer';
export class UserEntity { id: string; email: string;
@Exclude() passwordHash: string;
constructor(partial: Partial<UserEntity>) { Object.assign(this, partial); }}Check your understanding
Question 1 of 5
0 of 5 checked