Back to all posts

Clean API Design in NestJS

S
sonhp
10 min read

Clean API Design in NestJS

A well-structured API is essential for scalability, maintainability, and clarity. Without clean API design, controllers become bloated, validation is inconsistent, and response handling becomes chaotic.

In this guide, we’ll cover:

  • Structuring controllers and request handlers properly
  • Keeping DTOs and validation separate
  • Handling request and response transformations effectively

1. Structuring Controllers in NestJS

NestJS follows an MVC-like architecture, where:
Controllers handle incoming requests and delegate logic
Services contain the business logic
DTOs (Data Transfer Objects) handle request validation
Interceptors manage response transformations

A clean controller should:

  • Be responsible only for handling requests and responses
  • Delegate all business logic to services
  • Ensure consistent response formats

Example: Well-Structured Controller

import { Controller, Get, Param } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(':id')
  async getUser(@Param('id') id: string) {
    return this.userService.getUserById(id);
  }
}

Clean separation – Controller delegates logic to the service
Readable – Handles only routing, no business logic inside


Example: Bad Controller Design

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(':id')
  async getUser(@Param('id') id: string) {
    const user = await this.userService.findUserById(id);
    if (!user) {
      throw new NotFoundException('User not found');
    }
    return { id: user.id, name: user.name, email: user.email };
  }
}

🚨 Problem: The controller is doing too much!
Business logic should be in the service layer
Response transformation should be handled in a serializer/interceptor


2. Keeping DTOs and Validation Separate

What is a DTO?

A DTO (Data Transfer Object) ensures that only valid data reaches the service layer.

Why use DTOs?

  • ✅ Prevents unnecessary data processing
  • ✅ Improves validation consistency
  • ✅ Reduces controller clutter

Example: DTO for User Creation

📌 user.dto.ts

import { IsEmail, IsNotEmpty, IsString } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  name: string;

  @IsEmail()
  email: string;

  @IsString()
  password: string;
}

Using DTOs in a Controller

import { Controller, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/user.dto';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post()
  async createUser(@Body() createUserDto: CreateUserDto) {
    return this.userService.createUser(createUserDto);
  }
}

🚀 Benefits of DTOs:
Validation is automatic (thanks to class-validator)
Ensures only necessary data is passed
Keeps controllers clean


3. Handling Request and Response Transformations

Why Transform Requests & Responses?

Keep API responses consistent
Hide sensitive/internal fields
Improve data formatting


Using Interceptors for Response Formatting

📌 response.interceptor.ts

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) => ({
        success: true,
        data,
      })),
    );
  }
}

Applying Interceptor to Controllers

import { UseInterceptors } from '@nestjs/common';
import { ResponseInterceptor } from './interceptors/response.interceptor';

@UseInterceptors(ResponseInterceptor)
@Controller('users')
export class UserController {
  // Controller methods...
}

Now, all responses will be wrapped in { success: true, data }.


4. Summary: Best Practices for Clean API Design

Keep controllers focused on routing, not business logic
Use DTOs for validation and data consistency
Use interceptors for response transformations
Ensure request handlers are simple and readable

S

Written by sonhp

Technical writer and developer passionate about web technologies.

Related Articles