8 min readUpdated May 24, 2025

Why NestJS Uses DTOs and Interfaces: Unlocking Their Power with class-transformer

NestJS uses interfaces for type safety and DTOs for runtime validation and transformation. While interfaces vanish at compile time, DTOs—enhanced by class-transformer—ensure clean, secure API data handling. Used together, they create robust and maintainable code.

Written by

TypeScriptNestJSReact, Vue and Typescript

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 with class-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!

About

Software Developer & Consultant specializing in JavaScript, TypeScript, and modern web technologies.

Share this article