Standardizing API Responses and Exception Handling in NestJS

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/transform

Implement 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-exception

Implement 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.

Tags: NestJS TypeScript API Design Exception Handling swagger

Posted on Thu, 02 Jul 2026 16:26:50 +0000 by sholtzrevtek