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