Why NestJS Uses DTOs and Interfaces: Unlocking Their Power with class-transformer
If you’re new to NestJS, you might wonder why you need both DTOs (Data Transfer Objects) and interfaces when building APIs. They both seem to define data structures, so aren’t they redundant? This question, raised in a Stack Overflow post, is common among NestJS developers. In this post, we’ll clarify the roles of DTOs and interfaces, and show how the class-transformer
library supercharges DTOs for validation and transformation. Let’s dive in!
Why DTOs and Interfaces?
NestJS leverages TypeScript’s type system and a modular architecture to build robust APIs. DTOs and interfaces serve distinct purposes, but their synergy is key to clean, maintainable code.
What Are DTOs?
DTOs are classes that define the structure and validation rules for data transferred between the client and server. They’re used to:
- Validate input: Ensure incoming data (e.g., POST request payloads) meets requirements.
- Shape output: Control what data is sent back to the client.
- Encapsulate data: Keep API contracts separate from internal models.
DTOs are typically paired with class-validator
for runtime validation. For example, a CreateUserDto
ensures an email is valid and a password meets length requirements.
What Are Interfaces?
Interfaces are a TypeScript feature that define the shape of an object for compile-time type checking. They’re used to:
- Ensure type safety: Catch errors in development (e.g., accessing undefined properties).
- Describe internal models: Define the structure of entities or database models.
- Avoid runtime overhead: Interfaces are removed during compilation to JavaScript.
Unlike DTOs, interfaces have no runtime presence, so they can’t enforce validation or transformation.
Why Both?
DTOs and interfaces seem similar because they both describe data shapes, but they operate at different levels:
- DTOs are runtime-enforceable, used for API input/output, and often include validation decorators.
- Interfaces are compile-time-only, used for internal type safety, and don’t affect runtime behavior.
For example, a User
interface might define the structure of a database entity, while a CreateUserDto
validates and transforms incoming API data, ensuring only valid data reaches the business logic.
Enter class-transformer
The class-transformer
library enhances DTOs by converting plain JavaScript objects (e.g., request payloads) into class instances and transforming data (e.g., excluding sensitive fields or renaming properties). It’s commonly used in NestJS to:
- Map JSON to DTOs: Convert incoming requests into validated class instances.
- Serialize responses: Exclude sensitive data (e.g., passwords) or transform property names.
- Simplify data handling: Ensure consistent data formats across the application.
Combined with class-validator
, class-transformer
makes DTOs a powerful tool for API development.
A Practical Example
Let’s build a simple NestJS application with a UsersModule
to demonstrate DTOs, interfaces, and class-transformer
. We’ll create an endpoint to register a user and return a safe response without sensitive data.
Step 1: Setup
Create a new NestJS project:
1nest new nestjs-dto-example 2cd nestjs-dto-example 3npm install class-validator class-transformer @nestjs/mapped-types
Step 2: Define an Interface and DTOs
Create a User
interface for the internal model in src/users/user.interface.ts
:
1export interface User { 2 id: number; 3 email: string; 4 password: string; 5 name: string; 6}
Create a CreateUserDto
in src/users/dto/create-user.dto.ts
for input validation:
1import { IsEmail, IsString, MinLength } from 'class-validator'; 2 3export class CreateUserDto { 4 @IsEmail() 5 email: string; 6 7 @IsString() 8 @MinLength(6) 9 password: string; 10 11 @IsString() 12 name: string; 13}
Create a UserResponseDto
in src/users/dto/user-response.dto.ts
for safe output, using class-transformer
:
1import { Exclude, Expose } from 'class-transformer'; 2 3export class UserResponseDto { 4 @Expose() 5 id: number; 6 7 @Expose() 8 email: string; 9 10 @Expose() 11 name: string; 12 13 @Exclude() 14 password: string; // Excluded from response 15}
The @Exclude()
decorator ensures the password
isn’t included in the response, while @Expose()
explicitly includes other properties.
Step 3: Create the Users Service
In src/users/users.service.ts
:
1import { Injectable } from '@nestjs/common'; 2import { CreateUserDto } from './dto/create-user.dto'; 3import { UserResponseDto } from './dto/user-response.dto'; 4import { User } from './user.interface'; 5import { plainToClass } from 'class-transformer'; 6 7@Injectable() 8export class UsersService { 9 private users: User[] = []; 10 11 create(createUserDto: CreateUserDto): UserResponseDto { 12 const user: User = { 13 id: this.users.length + 1, 14 email: createUserDto.email, 15 password: createUserDto.password, // In real apps, hash this! 16 name: createUserDto.name, 17 }; 18 this.users.push(user); 19 return plainToClass(UserResponseDto, user); // Transform to safe DTO 20 } 21}
The plainToClass
function from class-transformer
converts the User
object to a UserResponseDto
, applying the @Exclude()
rule to remove the password.
Step 4: Create the Users Controller
In src/users/users.controller.ts
:
1import { Controller, Post, Body, ValidationPipe } from '@nestjs/common'; 2import { UsersService } from './users.service'; 3import { CreateUserDto } from './dto/create-user.dto'; 4import { UserResponseDto } from './dto/user-response.dto'; 5 6@Controller('users') 7export class UsersController { 8 constructor(private readonly usersService: UsersService) {} 9 10 @Post() 11 create(@Body(ValidationPipe) createUserDto: CreateUserDto): UserResponseDto { 12 return this.usersService.create(createUserDto); 13 } 14}
The ValidationPipe
automatically validates the incoming CreateUserDto
using class-validator
decorators.
Step 5: Set Up the Users Module
In src/users/users.module.ts
:
1import { Module } from '@nestjs/common'; 2import { UsersController } from './users.controller'; 3import { UsersService } from './users.service'; 4 5@Module({ 6 controllers: [UsersController], 7 providers: [UsersService], 8}) 9export class UsersModule {}
Update src/app.module.ts
:
1import { Module } from '@nestjs/common'; 2import { UsersModule } from './users/users.module'; 3 4@Module({ 5 imports: [UsersModule], 6}) 7export class AppModule {}
Step 6: Test the Endpoint
Run the app:
1npm run start:dev
Send a POST request to http://localhost:3000/users
:
1{ 2 "email": "john@example.com", 3 "password": "secure123", 4 "name": "John Doe" 5}
Response:
1{ 2 "id": 1, 3 "email": "john@example.com", 4 "name": "John Doe" 5}
The password
is excluded thanks to class-transformer
. If you send invalid data (e.g., an invalid email), ValidationPipe
returns a 400 error with details.
Step 7: Enable Global Validation
To apply validation globally, update src/main.ts
:
1import { NestFactory } from '@nestjs/core'; 2import { AppModule } from './app.module'; 3import { ValidationPipe } from '@nestjs/common'; 4 5async function bootstrap() { 6 const app = await NestFactory.create(AppModule); 7 app.useGlobalPipes(new ValidationPipe({ transform: true })); 8 await app.listen(3000); 9} 10bootstrap();
The transform: true
option ensures class-transformer
converts plain objects to DTO instances automatically.
Why class-transformer Matters
class-transformer
enhances DTOs by:
- Transforming Data: Converting JSON to DTOs or DTOs to other formats.
- Excluding Sensitive Data: Using
@Exclude()
to remove fields like passwords from responses. - Custom Transformations: Renaming properties or applying complex logic with
@Transform()
.
For example, you could add a transformation to convert emails to lowercase:
1import { Transform } from 'class-transformer'; 2 3export class CreateUserDto { 4 @IsEmail() 5 @Transform(({ value }) => value.toLowerCase()) 6 email: string; 7 8 @IsString() 9 @MinLength(6) 10 password: string; 11 12 @IsString() 13 name: string; 14}
Best Practices
- Use DTOs for API Contracts: Define DTOs for all input/output to ensure validation and consistency.
- Leverage Interfaces Internally: Use interfaces for internal models (e.g., database entities) to maintain type safety.
- Combine class-validator and class-transformer: Validate with
class-validator
and transform withclass-transformer
for robust APIs. - Keep DTOs Lean: Include only necessary fields to reduce coupling with internal models.
- Document with Swagger: Use
@nestjs/swagger
to generate API docs from DTOs. - Avoid Overusing Interfaces: Don’t use interfaces for validation, as they lack runtime enforcement.
Common Pitfalls
- Confusing DTOs with Entities: DTOs are for API data, not database models. Use separate classes to avoid exposing sensitive data.
- Missing ValidationPipe: Without it,
class-validator
decorators won’t work. - Overcomplicating DTOs: Keep transformations simple to maintain readability.
- Not Excluding Sensitive Data: Always use
@Exclude()
for fields like passwords.
Conclusion
DTOs and interfaces in NestJS serve complementary roles: DTOs enforce runtime validation and shape API data, while interfaces ensure compile-time type safety. The class-transformer
library takes DTOs to the next level by enabling seamless data transformation and serialization. By combining these tools, as shown in our example, you can build secure, maintainable APIs.
Have you used DTOs and class-transformer
in your NestJS projects? Share your tips or questions in the comments below!
Happy coding, and keep building robust NestJS apps!