Standardize NestJS API responses and request logging with global interceptors
Standardize NestJS API responses and logging using global interceptors. Learn how to implement response envelopes and request timing using RxJS and APP_INTERCEPTOR.
16 Dec 2025, 23:58 UTC

Problem: Inconsistent envelopes and scattered logging across modules
In a modular NestJS application, each controller often returns its own data shape. One module might return raw entities, while another wraps data in a { result: ... } object. Simultaneously, logging is often duplicated across guards or services, polluting business logic. The solution is to move these cross-cutting concerns into interceptors: one to shape every successful response into a single envelope and another to log request metadata before and after handling.
Desired Outcome
All HTTP responses follow a predictable envelope (e.g., { success: true, data, timestamp }). Request and response metadata—including method, path, duration, and status—are recorded centrally without modifying individual controllers. These interceptors are applied globally but remain testable and removable.
Prerequisites
A NestJS application (v8+ recommended) with existing modules and controllers. RxJS is included by default in NestJS. Familiarity with providers and the APP_INTERCEPTOR token is required. No special system permissions are needed beyond standard source code access.
Focused Procedure
Create a Response Envelope Interceptor
Create src/common/interceptors/response-envelope.interceptor.ts. Implement the NestInterceptor interface. Use the RxJS map operator to wrap the returned value from the handler.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ResponseEnvelopeInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
Critical: You must return next.handle(). If the observable is not returned, the request pipeline will hang indefinitely, and the client will receive no response.
Create a Request Logging Interceptor
Create src/common/interceptors/request-logging.interceptor.ts. Capture the start time before the handler executes and use the tap operator to log the duration after the handler completes.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class RequestLoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const { method, url } = req;
const started = Date.now();
return next.handle().pipe(
tap({
next: () => {
const duration = Date.now() - started;
console.log(`${method} ${url} ${duration}ms`);
},
}),
);
}
}
Register Interceptors Globally
Centralize these providers in a shared module (e.g., CoreModule) to ensure feature modules remain decoupled from the transformation logic.
import { Module, APP_INTERCEPTOR } from '@nestjs/common';
import { ResponseEnvelopeInterceptor } from './interceptors/response-envelope.interceptor';
import { RequestLoggingInterceptor } from './interceptors/request-logging.interceptor';
@Module({
providers: [
{ provide: APP_INTERCEPTOR, useClass: RequestLoggingInterceptor },
{ provide: APP_INTERCEPTOR, useClass: ResponseEnvelopeInterceptor },
],
})
export class CoreModule {}
Import CoreModule into your root AppModule. Note that the order of providers matters: they are executed in the order they are registered. Registering the logger before the envelope ensures the timing includes the transformation overhead.
Expected Checks
Start the application and send an HTTP request to any existing endpoint. Verify the following:
- Response Body: The JSON should be nested under a
datakey and includesuccess: trueand atimestamp. - Server Logs: The console should display a line matching the format:
GET /api/resource 12ms. - Pipeline Flow: Ensure the request completes and does not timeout (verifying
next.handle()was returned).
Limitations and Risks
Interceptors run for every request passing through the HTTP layer. Avoid heavy synchronous computations or blocking I/O inside the intercept method to prevent latency spikes.
Error Handling: The map operator only triggers for successful responses. If an exception is thrown, it is handled by NestJS Exception Filters, bypassing the envelope transformation. To wrap errors, you must implement a global ExceptionFilter.
Recovery Options
If responses are double-wrapped or logging is duplicated, remove the APP_INTERCEPTOR providers from CoreModule and restart the application. Since this change is stateless, the application will immediately revert to returning raw controller values.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.