Response Transformation Interceptor
Applications often require a consistent structure for API responses, such as wrapping data in an object containing status codes and messages. To achieve this globally in NestJS, a custom interceptor can be implemented to map successful request responses into a standardized format.
Generate the interceptor scaffold using the CLI:
nest generate interceptor common/interceptors/transformImplement the TransformInterceptor to modify the outgoing stream. This logic captures the response payload and wraps it within a generic ApiResponse object.
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface ApiResponse<T> {
statusCode: number;
message: string;
data: T;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
return next.handle().pipe(
map(payload => ({
statusCode: 200,
message: 'Request successful',
data: payload,
})),
);
}
}Register the interceptor globally in main.ts to apply it to all endpoints.
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TransformInterceptor<any>());Custom Exception Handling
Defining Business Exceptions
Standard HTTP status codes are often insufficient for describing specific business logic errors. Create a custom exception class to handle these scenarios, allowing for custom error codes and messages.
First, define an enum for application-specific error codes.
// common/enums/error-code.enum.ts
export enum AppErrorCode {
SERVER_ERROR = 500001,
RESOURCE_NOT_FOUND = 400001,
INVALID_INPUT = 400002,
}Create the BusinessException class extending the native HttpException.
// common/exceptions/business.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
import { AppErrorCode } from '../enums/error-code.enum';
export class BusinessException extends HttpException {
private readonly internalCode: AppErrorCode;
constructor(message: string, errorCode: AppErrorCode, httpStatus: HttpStatus = HttpStatus.OK) {
super(message, httpStatus);
this.internalCode = errorCode;
}
getErrorCode(): AppErrorCode {
return this.internalCode;
}
}Global Exception Filter
To ensure exceptions also adhere to the standardized response format, implement a global exception filter. This filter captures both standard HTTP exceptions and the custom business exceptions, formatting them appropriately.
nest generate filter common/filters/http-exceptionImplement the filter logic to handle different exception types.
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
import { BusinessException } from '../exceptions/business.exception';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
let code = HttpStatus.INTERNAL_SERVER_ERROR;
if (exception instanceof BusinessException) {
status = exception.getStatus();
message = exception.message;
code = exception.getErrorCode();
} else if (exception instanceof HttpException) {
status = exception.getStatus();
message = exception.message;
code = status;
}
response.status(status).json({
statusCode: code,
timestamp: new Date().toISOString(),
message: message,
path: ctx.getRequest().url,
});
}
}Apply the filter globally.
import { GlobalExceptionFilter } from './common/filters/http-exception.filter';
app.useGlobalFilters(new GlobalExceptionFilter());Swagger Integration for Wrapped Responses
Since the interceptor modifies the response structure, the Swagger module requires explicit configuration to display the correct schema. Because TypeScript does not preserve generic metadata at runtime, a custom decorator is necessary to define the response structure accurately in the API documentation.
Create a helper decorator ApiSuccessResponse that builds the schema definition for the wrapped data.
import { Type, applyDecorators } from '@nestjs/common';
import { ApiOkResponse, ApiExtraModels, getSchemaPath } from '@nestjs/swagger';
interface ResponseOptions<T> {
model: Type<T>;
description?: string;
isArray?: boolean;
}
export function ApiSuccessResponse<T>(options: ResponseOptions<T>) {
const { model, description = 'Success', isArray } = options;
const dataSchema = isArray
? { type: 'array', items: { $ref: getSchemaPath(model) } }
: { $ref: getSchemaPath(model) };
return applyDecorators(
ApiExtraModels(model),
ApiOkResponse({
description,
schema: {
properties: {
statusCode: { type: 'number', example: 200 },
message: { type: 'string', example: 'Request successful' },
data: dataSchema,
},
},
}),
);
}Use this decorator on controller methods to document the wrapped response.
@Post('login')
@ApiSuccessResponse({ model: LoginTokenDto })
async login(@Body() credentials: LoginDto) {
return this.authService.login(credentials);
}This setup ensures that the Swagger UI renders the full response object, including the nested data model, maintaining consistency between the actual API response and its documentation.