How to Implement Custom Interceptors for Response Transformation in NestJS
NestJS interceptors let you transform responses by implementing the NestInterceptor interface, calling next.handle() to access the handler's result, and using RxJS operators like map to modify the data before serialization.
The nestjs/nest repository provides a robust interceptor pattern that sits between your controller and the client, allowing you to implement custom interceptors for response transformation across your entire API or specific routes. This pattern leverages RxJS observables to manipulate the response stream after the route handler executes but before the final HTTP response is sent.
Understanding the NestJS Interceptor Pattern
Interceptors in NestJS implement the NestInterceptor<T, R> interface, which requires a single intercept method receiving two arguments: ExecutionContext and CallHandler. The ExecutionContext provides access to the request, response, and handler metadata, while CallHandler represents the next step in the pipeline.
When you call next.handle(), you receive an Observable that emits the route handler's return value. Because this runs after the handler but before serialization, you can wrap data in envelopes, strip sensitive fields, or convert entities to DTOs. The interceptor returns this modified observable (or a Promise), allowing Nest to complete the response cycle.
Creating a Response Transformation Interceptor
Basic Envelope Pattern
A common use case is wrapping every successful response in a consistent structure containing metadata like timestamps or status indicators. In sample/01-cats-app/src/core/interceptors/transform.interceptor.ts, the framework demonstrates this pattern.
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface ResponseEnvelope<T> {
status: 'success';
data: T;
timestamp: number;
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, ResponseEnvelope<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<ResponseEnvelope<T>> {
return next.handle().pipe(
map((data) => ({
status: 'success',
data,
timestamp: Date.now(),
})),
);
}
}
Conditional Field Stripping
You can access the request object via context.switchToHttp().getRequest() to conditionally transform responses based on user roles or permissions. This example removes sensitive fields for non-admin users:
@Injectable()
export class HideSensitiveDataInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const isAdmin = request.user?.roles?.includes('admin');
return next.handle().pipe(
map((result) => {
if (isAdmin) return result;
const cleanse = (obj: any) => {
const { password, ssn, ...rest } = obj;
return rest;
};
return Array.isArray(result)
? result.map(cleanse)
: cleanse(result);
}),
);
}
}
Global Date Formatting
For APIs requiring consistent date serialization, implement a recursive formatter that traverses the response object:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import * as dayjs from 'dayjs';
@Injectable()
export class DateFormattingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => this.formatDates(data)),
);
}
private formatDates(value: any): any {
if (value instanceof Date) {
return dayjs(value).format('YYYY-MM-DDTHH:mm:ssZ');
}
if (Array.isArray(value)) {
return value.map((v) => this.formatDates(v));
}
if (value && typeof value === 'object') {
const result: any = {};
for (const [k, v] of Object.entries(value)) {
result[k] = this.formatDates(v);
}
return result;
}
return value;
}
}
Applying Interceptors at Different Scopes
Route and Controller Level
Use the @UseInterceptors() decorator to bind interceptors to specific controllers or individual methods. According to the NestJS source, this is the most granular level of application.
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { TransformInterceptor } from './core/interceptors/transform.interceptor';
@Controller('cats')
@UseInterceptors(TransformInterceptor)
export class CatsController {
@Get()
findAll() {
return [{ id: 1, name: 'Whiskers' }];
}
}
Global Registration with APP_INTERCEPTOR
To apply an interceptor to every route in your application, register it as a provider using the APP_INTERCEPTOR token. This is defined in the core interceptors system and ensures the interceptor runs for all incoming requests.
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { TransformInterceptor } from './core/interceptors/transform.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: TransformInterceptor,
},
],
})
export class AppModule {}
Key Source Files in the NestJS Repository
The nestjs/nest repository contains several reference implementations that demonstrate production-grade interceptor patterns:
packages/common/serializer/class-serializer.interceptor.ts— Built-in interceptor usingclass-transformerto serialize class instances, showing how Nest handles automatic response transformation.sample/01-cats-app/src/core/interceptors/transform.interceptor.ts— Minimal example demonstrating the envelope pattern for wrapping handler results.sample/01-cats-app/src/core/interceptors/logging.interceptor.ts— Illustrates usingtap()operator to inspect the observable stream without modifying data, useful for combining logging with transformation.packages/core/interceptors/interceptors-consumer.ts— Core runtime logic that executes the interceptor chain and manages the order of execution.packages/core/interceptors/interceptors-context-creator.ts— Responsible for instantiatingExecutionContextand preparing dependencies for each interceptor invocation.
Summary
- Implement
NestInterceptorand override theinterceptmethod to gain access to the response stream vianext.handle(). - Use RxJS operators like
mapto transform data; the observable runs after the handler but before Nest serializes the response. - Access request context through
ExecutionContextto make transformation decisions based on headers, user roles, or query parameters. - Apply globally using
APP_INTERCEPTORprovider token, or locally using@UseInterceptors()decorator. - Reference built-in examples in
packages/common/serializer/class-serializer.interceptor.tsfor complex serialization logic.
Frequently Asked Questions
What is the difference between middleware and interceptors in NestJS?
Middleware operates before the route handler runs and cannot access the handler's return value, while interceptors wrap the entire execution context and receive the observable stream of the handler's result. According to the NestJS architecture in packages/core/interceptors/interceptors-consumer.ts, interceptors are the only construct that can transform the response body after the controller logic executes.
Can I modify the HTTP status code in a response interceptor?
Yes, but you must access the response object through context.switchToHttp().getResponse() and set the status before the observable completes. However, for status code changes, Guards or Exception Filters are often more appropriate; interceptors excel at data transformation rather than protocol-level modifications.
How do I access the request object inside an interceptor?
Use context.switchToHttp().getRequest() within the intercept method. The ExecutionContext instance provides this through the switchToHttp() helper, as implemented in packages/core/interceptors/interceptors-context-creator.ts. This gives you access to headers, user objects, and query parameters for conditional transformation logic.
Are interceptors executed in a specific order?
Yes, interceptors execute in the order they are registered. When multiple interceptors are bound, Nest chains them sequentially, with each interceptor's next.handle() calling the next in the chain. The execution follows the pattern shown in packages/core/interceptors/interceptors-consumer.ts, where the observable flows through each interceptor's RxJS operators before reaching the final serialization step.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →