Data Transfer Objects (DTOs) are a fundamental concept in NestJS applications. They help structure the data that flows between your application layers, ensuring clean, maintainable, and scalable code. However, using DTOs effectively requires understanding their purpose, best practices, and potential pitfalls.

In this blog, we’ll explore DTOs in NestJS in detail, covering their usage, tips, tricks, and best practices. We’ll also provide a step-by-step approach with code examples to help you avoid common mistakes like decoupling issues.

What is a DTO?

A DTO (Data Transfer Object) is an object that defines how data will be sent over the network. It acts as a contract between the client and the server, ensuring that only the necessary data is transmitted and validated.

In NestJS, DTOs are often used in conjunction with Validation Pipes to validate incoming data and ensure it adheres to the expected structure.

Why Use DTOs?

  1. Data Validation: Ensure incoming data matches the expected format.
  2. Type Safety: Leverage TypeScript to enforce data types.
  3. Decoupling: Separate data structure from business logic.
  4. Documentation: Clearly define the shape of data for APIs.

Step-by-Step Guide to Using DTOs in NestJS

Let’s dive into a step-by-step guide to using DTOs in NestJS, complete with code examples.

Step 1: Install Required Packages

Before we start, ensure you have the necessary packages installed:

npm install class-validator class-transformer
  • class-validator: Provides decorators for validating DTOs.
  • class-transformer: Converts plain objects to instances of DTO classes.

Step 2: Create a Basic DTO

Let’s create a simple DTO for a user registration endpoint.

// src/users/dto/create-user.dto.ts
import { IsString, IsEmail, IsNotEmpty, MinLength } from 'class-validator';

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

  @IsEmail()
  @IsNotEmpty()
  readonly email: string;

  @IsString()
  @MinLength(8)
  readonly password: string;
}

Here, we’ve used decorators from class-validator to enforce validation rules:

  • @IsString(): Ensures the field is a string.
  • @IsNotEmpty(): Ensures the field is not empty.
  • @IsEmail(): Validates the email format.
  • @MinLength(8): Ensures the password is at least 8 characters long.

Step 3: Use the DTO in a Controller

Now, let’s use the DTO in a controller.

// src/users/users.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

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

In this example:

  • The @Body() decorator extracts the request body and maps it to the CreateUserDto class.
  • NestJS automatically validates the incoming data against the DTO using the ValidationPipe.

Step 4: Enable Global Validation

To enable automatic validation, add the ValidationPipe globally in your main.ts file.

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

The ValidationPipe ensures that all incoming data is validated against the DTOs.

Step 5: Transform and Sanitize Data

You can use the class-transformer library to transform and sanitize incoming data. For example, you can trim whitespace from strings or convert strings to numbers.

// src/users/dto/create-user.dto.ts
import { IsString, IsEmail, IsNotEmpty, MinLength } from 'class-validator';
import { Transform } from 'class-transformer';

enum UserRole {
  ADMIN = 'admin',
  USER = 'user',
}

export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  @Transform(({ value }) => value.trim())
  readonly name: string;

  @IsEmail()
  @IsNotEmpty()
  @Transform(({ value }) => value.toLowerCase())
  readonly email: string;

  @IsString()
  @MinLength(8)
  readonly password: string;

  @IsEnum(UserRole)
  readonly role: UserRole;
}

Here:

  • @Transform(({ value }) => value.trim()): Trims whitespace from the name field.
  • @Transform(({ value }) => value.toLowerCase()): Converts the email field to lowercase.
  • @IsEnum(UserRole): Ensures the role field is one of the predefined values in the UserRole enum.

Step 6: Avoid Decoupling Issues

One common mistake is tightly coupling DTOs to your database models. To avoid this:

  • Use separate DTOs for input and output.
  • Avoid exposing sensitive data in response DTOs.

For example, create a UserResponseDto for responses:

// src/users/dto/user-response.dto.ts
import { Exclude, Expose } from 'class-transformer';

export class UserResponseDto {
  @Expose()
  id: string;

  @Expose()
  name: string;

  @Expose()
  email: string;

  @Exclude()
  password: string;
}

In your service, transform the database model into the response DTO:

// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { UserResponseDto } from './dto/user-response.dto';
import { plainToInstance } from 'class-transformer';

@Injectable()
export class UsersService {
  async createUser(createUserDto: CreateUserDto): Promise<UserResponseDto> {
    // Save user to the database
    const user = await this.saveUserToDatabase(createUserDto);

    // Transform the user to UserResponseDto
    return plainToInstance(UserResponseDto, user, {
      excludeExtraneousValues: true,
    });
  }

  private async saveUserToDatabase(userData: CreateUserDto) {
    // Simulate saving to the database
    return {
      id: '1',
      ...userData,
    };
  }
}

Step 7: Use Partial DTOs for Updates

Instead of duplicating DTOs, you can reuse them by extending or composing them. NestJS provides utility types like PartialType, PickType, and OmitType to make this easier.

// src/users/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends PartialType(CreateUserDto) {}

Here:

UpdateUserDto inherits all fields from CreateUserDto, but makes them optional using PartialType.

Extra 1: Document DTOs

Use Swagger decorators (@ApiProperty) to document your DTOs for API documentation.

Example: Documenting DTOs with Swagger

// src/users/dto/create-user.dto.ts
import { IsString, IsEmail, IsNotEmpty, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class CreateUserDto {
  @ApiProperty({ description: 'The full name of the user', example: 'John Doe' })
  @IsString()
  @IsNotEmpty()
  readonly name: string;

  @ApiProperty({ description: 'The email address of the user', example: 'john.doe@example.com' })
  @IsEmail()
  @IsNotEmpty()
  readonly email: string;

  @ApiProperty({ description: 'The password of the user (min 8 characters)', example: 'password123' })
  @IsString()
  @MinLength(8)
  readonly password: string;
}

Here:

@ApiProperty adds metadata to each field, which is used by Swagger to generate API documentation.

Organize your DTOs in a logical folder structure to keep your codebase clean and maintainable.

Example: Folder Structure

src/
  users/
    dto/
      create-user.dto.ts
      update-user.dto.ts
      user-response.dto.ts
    users.controller.ts
    users.service.ts

This structure groups all DTOs related to the users module in a single dto folder.

Conclusion

DTOs are a powerful tool in NestJS for structuring and validating data. By following the step-by-step guide and best practices outlined in this blog, you can ensure your application is clean, maintainable, and scalable. Remember to avoid decoupling issues by separating input and output DTOs and leveraging transformation utilities.

Happy coding! 🚀

Cover image: unsplash

Author Of article : Cendekia Read full article