Writing Clean and Maintainable Code in NestJS
Part 10 of 12
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 toclass-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