Technical DocumentationAPI ApplicationAPI Response Interceptor

API Response Interceptor

Overview

Global interceptor system ensuring all API responses follow a consistent structure with standardized error handling.

Architecture

┌─────────────────────────────┐
│    Incoming HTTP Request    │
└──────────────┬──────────────┘


┌─────────────────────────────┐
│      ValidationPipe         │
│  • Validates DTOs           │
└──────────────┬──────────────┘

        ┌──────┴──────┐
        │             │
     Success       Error
        │             │
        ▼             ▼
   Controller   HttpExceptionFilter
        │             │
        ▼             │
 ResponseInterceptor  │
        │             │
        └──────┬──────┘

    Standardized Response

Response Structure

All responses follow this structure:

{
  data: any;              // Response payload
  success: boolean;       // true for success, false for errors
  metadata?: any;         // Optional (pagination, etc.)
  error?: {               // Only present when success: false
    code: string;
    message: string;
    details?: unknown;
  };
}

Success Examples

Simple Response:

{
  "success": true,
  "data": { "message": "User created", "id": 123 }
}

With Metadata:

{
  "success": true,
  "data": [{ "id": 1, "name": "User 1" }],
  "metadata": { "total": 100, "page": 1 }
}

Error Examples

Validation Error:

{
  "success": false,
  "data": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [{ "property": "email", "constraints": {...} }]
  }
}

Not Found:

{
  "success": false,
  "data": null,
  "error": {
    "code": "NOT_FOUND",
    "message": "User not found"
  }
}

Error Codes

CodeStatusDescription
VALIDATION_ERROR400DTO validation failed
BAD_REQUEST400Invalid input
UNAUTHORIZED401Authentication failed
FORBIDDEN403Not authorized
NOT_FOUND404Resource not found
TOO_MANY_REQUESTS429Rate limited
INTERNAL_SERVER_ERROR500Unexpected error
SERVICE_UNAVAILABLE503Service down

Implementation

Components

Type Definitions: packages/types/src/types/api-response.types.ts

  • ApiResponse<T> interface
  • ApiError interface
  • ApiErrorCode enum

Response Interceptor: src/common/interceptors/response.interceptor.ts

  • Wraps successful responses automatically
  • Detects pre-wrapped responses to avoid double-wrapping

Exception Filter: src/common/filters/http-exception.filter.ts

  • Catches all exceptions
  • Formats validation errors with details
  • Maps HTTP status codes to error codes
  • Logs errors appropriately

Registration: src/main.ts

app.useGlobalPipes(new ValidationPipe({...}));
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new ResponseInterceptor());

Usage

Controllers

No changes required - existing controllers work automatically:

@Get(':id')
async findOne(@Param('id') id: string) {
  const user = await this.service.findOne(id);
  if (!user) throw new NotFoundException('User not found');
  return user;  // Auto-wrapped
}

Validation

Use DTOs with decorators:

export class CreateUserDto {
  @IsEmail()
  email: string;
 
  @MinLength(3)
  name: string;
}
 
@Post()
create(@Body() dto: CreateUserDto) {
  // Validation errors auto-formatted
  return this.service.create(dto);
}

With Metadata

Return data and metadata separately:

@Get()
async findAll(@Query() query: PaginationDto) {
  const { items, total, page } = await this.service.findAll(query);
  return { data: items, metadata: { total, page } };
}

Custom Status Codes

Use @HttpCode() decorator:

@Post()
@HttpCode(201)
create(@Body() dto: CreateDto) {
  return this.service.create(dto);
}

Testing

Manual testing:

# Success response
curl http://localhost:5500/
 
# 404 Error
curl http://localhost:5500/nonexistent
 
# Validation error
curl -X POST http://localhost:5500/auth \
  -H "Content-Type: application/json" -d '{}'

E2E tests expect wrapped responses:

it("should return wrapped response", () => {
  return request(app.getHttpServer())
    .get("/users")
    .expect(200)
    .expect((res) => {
      expect(res.body).toHaveProperty("success", true);
      expect(res.body).toHaveProperty("data");
    });
});

Best Practices

  1. Return data directly - Let the interceptor handle wrapping
  2. Use standard exceptions - throw new NotFoundException(...)
  3. Include metadata for lists - Pagination info, record counts
  4. Use DTOs for validation - Leverage class-validator
  5. Keep error messages clear - User-friendly and actionable

Frontend Integration

The frontend ApiClient handles the structure automatically:

const response = await apiClient.get<User[]>("/users");
// response.data contains unwrapped data
// response.success indicates success/failure
// response.error contains error details if failed

Migration

Existing endpoints work without changes:

// Before & After - Same code
@Get()
findAll() {
  return [{ id: 1 }, { id: 2 }];
}
// Response auto-wrapped: { success: true, data: [...] }

For custom responses, structure as data + metadata:

// Before
return { success: true, users: [...], count: 10 };
 
// After
return { data: [...], metadata: { count: 10 } };