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?
- Data Validation: Ensure incoming data matches the expected format.
- Type Safety: Leverage TypeScript to enforce data types.
- Decoupling: Separate data structure from business logic.
- 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 theCreateUserDto
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 therole
field is one of the predefined values in theUserRole
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.
Extra 2: Group Related DTOs
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