In this article, we will explore how to implement a custom response interceptor in a NestJS application to format all API responses in a standardized structure. This is particularly useful for maintaining consistency across APIs, making it easier for front-end developers and other consumers of the API to parse and understand the responses.
What Is an Interceptor in NestJS?
In NestJS, interceptors are a powerful feature that allow you to:
- Transform data before sending it to the client.
- Manipulate the request/response.
- Perform logging, analytics, or caching.
- Add custom headers or structure the API response consistently.
Interceptors are executed before and/or after the route handler, giving you the flexibility to transform the incoming request or the outgoing response.
Custom Response Interceptor
Why Use a Response Interceptor?
The primary goal of this interceptor is to:
- Standardize API responses.
- Ensure every response contains metadata such as
status_code
,message
,timestamps
, etc. - Handle successful responses uniformly, reducing repeated response formatting in controllers.
Standardized Response Structure
Here’s the structure we aim to achieve:
{
"message": "Operation completed successfully",
"status": true,
"data": { "key": "value" },
"error": null,
"timestamps": "2024-12-05T10:45:00.000Z",
"status_code": 200,
"path": "/api/v1/example"
}
Custom Response Interceptor Code
Here’s the implementation of the custom response interceptor in NestJS:
Response Interface
The Response
interface defines the structure of the response:
export interface Response<T> {
message: string; // Describes the result of the operation
status: boolean; // Indicates success (true) or failure (false)
data: T; // The actual payload returned to the client
error: null; // Reserved for error details (set to null for successful responses)
timestamps: Date; // Timestamp of the response
status_code: number; // HTTP status code
}
Custom Response Interceptor
The ResponseInterceptor
intercepts the response and formats it before sending it to the client:
import { CallHandler, ExecutionContext, NestInterceptor } from "@nestjs/common";
import { map, Observable } from "rxjs";
export class ResponseInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<Response<T>> {
// Extract status code and request path
const statusCode = context.switchToHttp().getResponse().statusCode;
const path = context.switchToHttp().getRequest().url;
// Transform the outgoing response
return next.handle().pipe(
map((data) => ({
message: data.message, // Descriptive message for the operation
status: data.success, // Boolean flag indicating operation success
data: data.result, // Payload data
timestamps: new Date(), // Current timestamp
status_code: statusCode, // HTTP status code
path, // Request path
error: null, // No error in success response
}))
);
}
}
How It Works
- The
intercept
method captures the response object. - The
context
parameter provides details like HTTP status code and request URL. - The
next.handle()
processes the response, andmap
transforms the output into the standardized format.
Using the Interceptor Globally
To apply the interceptor globally across all endpoints, modify the main.ts
file:
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ResponseInterceptor } from "./response.interceptor";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Use the custom ResponseInterceptor globally
app.useGlobalInterceptors(new ResponseInterceptor());
await app.listen(3000);
}
bootstrap();
Testing the Response Interceptor
When you define a route in a controller like this:
import { Controller, Get } from "@nestjs/common";
@Controller("example")
export class ExampleController {
@Get()
getExample() {
return {
message: "Operation completed successfully",
success: true,
result: { key: "value" },
};
}
}
The API response will be intercepted and transformed into:
{
"message": "Operation completed successfully",
"status": true,
"data": { "key": "value" },
"error": null,
"timestamps": "2024-12-05T10:45:00.000Z",
"status_code": 200,
"path": "/example"
}
Benefits of Using a Custom Response Interceptor
Consistency:
- Ensures all responses follow the same format, making APIs predictable and easier to consume.
Code Simplification:
- Removes the need to manually format responses in every controller.
Error Handling:
- You can extend this interceptor to handle error responses uniformly.
Enhanced Debugging:
- Metadata like
timestamps
andpath
make debugging API issues easier.
- Metadata like
Extending the Interceptor
Handle Error Responses
You can modify the interceptor to check if the response is an error and format it accordingly:
return next.handle().pipe(
map((data) => {
if (data instanceof Error) {
return {
message: data.message,
status: false,
data: null,
error: data.stack,
timestamps: new Date(),
status_code: statusCode,
path,
};
}
return {
message: data.message,
status: data.success,
data: data.result,
timestamps: new Date(),
status_code: statusCode,
path,
error: null,
};
})
);
Best Practices
Versioning:
- Include API versioning in the response for better maintainability.
Extend Error Handling:
- Customize error responses with specific error codes and messages.
Logging:
- Combine this with a logging service to record all responses.
Type Safety:
- Use TypeScript generics for stronger type checking in your
Response
interface.
- Use TypeScript generics for stronger type checking in your
Conclusion
Using a custom response interceptor in NestJS significantly improves the structure and readability of API responses. It promotes consistency, simplifies controller logic, and makes your API more consumer-friendly. By implementing the example above, your APIs will follow a standardized response format, reducing confusion and errors for API consumers.