Standardizing API Responses in NestJS Using Global Interceptors
Learn how to use NestJS Interceptors to standardize API response structures globally, decoupling response formatting from business logic using RxJS.
17 Jul 2025, 20:59 UTC

The Problem: Inconsistent API Response Shapes
In many NestJS applications, different controllers return data in varying formats. One endpoint might return a raw array, while another returns an object with a message property. This inconsistency forces frontend developers to write conditional logic for every single API call to handle the response structure.
The solution is to use an Interceptor. Interceptors implement the Aspect-Oriented Programming (AOP) pattern, allowing you to decouple cross-cutting concerns—like response formatting and logging—from your core business logic. Instead of wrapping every controller return value in a standard object, a single interceptor can transform all outgoing data automatically.
Prerequisites
- A NestJS project (v8.0 or higher recommended).
- Basic familiarity with RxJS, as NestJS uses Observables to handle the request-response stream.
- Administrative access to the
main.tsfile or the rootAppModule.
Implementing a Response Transformation Interceptor
To standardize responses, you must create a class that implements the NestInterceptor interface. This requires implementing the intercept() method, which provides access to the ExecutionContext and the CallHandler.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
data: T;
timestamp: string;
path: string;
}
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<any>> {
const request = context.switchToHttp().getRequest();
const now = new Date().toISOString();
return next.handle().pipe(
map(data => ({
data,
timestamp: now,
path: request.url,
}))
);
}
}
In this implementation, next.handle() triggers the route handler. The map operator from RxJS is used to intercept the stream after the controller has returned its value but before it is sent to the client.
Applying the Interceptor
Depending on the scope of the transformation, you can apply the interceptor at three different levels:
1. Global Application Level
Use this for a strict, application-wide API contract. In main.ts, add the following line before app.listen():
// Run in main.ts
app.useGlobalInterceptors(new TransformInterceptor());
2. Controller Level
Use this if only a specific set of endpoints (e.g., /api/v2/) should follow the new format.
@UseInterceptors(TransformInterceptor)
@Controller('users')
export class UserController {}
3. Method Level
Use this for one-off transformations on specific endpoints.
@UseInterceptors(TransformInterceptor)
@Get('profile')
getProfile() {
return { name: 'John Doe' };
}
Comparison: Global vs. Provider-based Registration
| Method | Registration Location | Dependency Injection | Use Case |
|---|---|---|---|
app.useGlobalInterceptors() |
main.ts |
No (Manual instantiation) | Simple, stateless logic. |
APP_INTERCEPTOR Provider |
AppModule |
Yes (Full DI support) | Interceptors that require services (e.g., LoggerService). |
Verification and Testing
To verify the interceptor is functioning, use an HTTP client like cURL or Postman. If a controller method returns { "id": 1 }, the resulting HTTP response should now look like this:
{
"data": { "id": 1 },
"timestamp": "2026-10-16T09:22:37.277Z",
"path": "/users/1"
}
Diagnostic Check: If the response is still raw, ensure the @UseInterceptors decorator is placed above the method handler and that the TransformInterceptor is correctly exported and imported.
Limitations and Risks
- Latency: Global interceptors execute on every single request. Avoid performing heavy database queries or synchronous blocking I/O inside the
intercept()method. - Error Handling: Interceptors typically handle the success stream. To transform error responses, you must use an
Exception Filter, as exceptions bypass themapoperator in theCallHandler. - Breaking Changes: Applying a global interceptor to an existing API will change the JSON structure for all clients, potentially breaking frontend integrations. Always version your API (e.g.,
/v1/to/v2/) before introducing global transformations.
Rollback Procedure
Because this operation changes the state of the application's request pipeline, you can revert the changes by:
- Removing
app.useGlobalInterceptors()frommain.ts. - Deleting the
@UseInterceptors(TransformInterceptor)decorators from controllers or methods. - Restarting the NestJS server to clear the compiled interceptor chain.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.