Skip to content

Dtos


DTOs (Data Transfer Objects) define the shape of data being sent between layers. They provide type safety, validation, and documentation.

DTO Usage in Application
================================================================================
HTTP Request Service Layer Repository
| | |
| { | |
| "name": "John", -----> | validate(dto) |
| "email": "john@ | transform(dto) |
| test.com", -----> | process(dto) |
| "password": "1234" -----> | |
| } | |
| | |
| v |
| +------------------+ |
| | DTOs | |
| |------------------| |
| | CreateUserDTO | |
| | UpdateUserDTO | |
| | UserResponseDTO | |
| +------------------+ |
| |
v v
+------------------+ +------------------+
| Request Body | | Database |
| Validation | | Entities |
+------------------+ +------------------+
================================================================================

src/dto/create-user.dto.ts
export class CreateUserDto {
name: string;
email: string;
password: string;
age?: number;
role?: string;
}
// src/dto/update-user.dto.ts
export class UpdateUserDto {
name?: string;
email?: string;
password?: string;
age?: number;
role?: string;
}
// src/dto/user-response.dto.ts
export class UserResponseDto {
id: number;
name: string;
email: string;
age?: number;
role: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
// src/dto/pagination.dto.ts
export class PaginationDto {
page: number = 1;
limit: number = 10;
get offset(): number {
return (this.page - 1) * this.limit;
}
}

src/dto/create-user.dto.ts
export class CreateUserDto {
// String validations
name: string; // Required, will validate manually
// Email validation
email: string;
// Password validation
password: string;
// Optional number
age?: number;
// Enum
role?: 'user' | 'admin' | 'moderator';
// Date
birthDate?: Date;
// Array
tags?: string[];
// Nested object
address?: {
street: string;
city: string;
country: string;
postalCode: string;
};
}
// Manual validation function
export function validateCreateUserDto(data: any): CreateUserDto {
const errors: string[] = [];
// Name validation
if (!data.name || typeof data.name !== 'string') {
errors.push('Name is required');
} else if (data.name.length < 2 || data.name.length > 100) {
errors.push('Name must be between 2 and 100 characters');
}
// Email validation
if (!data.email || typeof data.email !== 'string') {
errors.push('Email is required');
} else if (!isValidEmail(data.email)) {
errors.push('Invalid email format');
}
// Password validation
if (!data.password || typeof data.password !== 'string') {
errors.push('Password is required');
} else if (data.password.length < 8) {
errors.push('Password must be at least 8 characters');
}
// Age validation
if (data.age !== undefined) {
if (typeof data.age !== 'number' || data.age < 0 || data.age > 150) {
errors.push('Age must be a valid number between 0 and 150');
}
}
// Role validation
const validRoles = ['user', 'admin', 'moderator'];
if (data.role && !validRoles.includes(data.role)) {
errors.push(`Role must be one of: ${validRoles.join(', ')}`);
}
if (errors.length > 0) {
throw new ValidationError(errors.join(', '));
}
return data as CreateUserDto;
}
function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
export class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}

src/dto/user-response.dto.ts
export class UserResponseDto {
static fromEntity(user: any): UserResponseDto {
return {
id: user.id,
name: user.name,
email: user.email,
age: user.age,
role: user.role,
isActive: user.isActive,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
};
}
static fromEntities(users: any[]): UserResponseDto[] {
return users.map(user => this.fromEntity(user));
}
}
// src/dto/api-response.dto.ts
export class ApiResponseDto<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
meta?: {
page: number;
limit: number;
total: number;
totalPages: number;
};
static success<T>(data: T, message?: string): ApiResponseDto<T> {
return {
success: true,
data,
message,
};
}
static error<T>(error: string): ApiResponseDto<T> {
return {
success: false,
error,
};
}
static paginated<T>(
data: T[],
page: number,
limit: number,
total: number
): ApiResponseDto<T[]> {
return {
success: true,
data,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
}
}

src/dto/mappers/user.mapper.ts
import { User } from '../entities/user.entity';
import { CreateUserDto, UpdateUserDto } from '../dto';
export class UserMapper {
// Entity to Response DTO
static toResponse(user: User): UserResponseDto {
return {
id: user.id,
name: user.name,
email: user.email,
age: user.age,
role: user.role,
isActive: user.isActive,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
};
}
// Entity to Response DTO (multiple)
static toResponseList(users: User[]): UserResponseDto[] {
return users.map(user => this.toResponse(user));
}
// Create DTO to Entity
static toEntity(dto: CreateUserDto): Partial<User> {
return {
name: dto.name,
email: dto.email,
password: dto.password, // Should be hashed
age: dto.age,
role: dto.role || 'user',
};
}
// Update DTO to Entity (partial)
static toUpdateEntity(dto: UpdateUserDto): Partial<User> {
const updateData: Partial<User> = {};
if (dto.name !== undefined) updateData.name = dto.name;
if (dto.email !== undefined) updateData.email = dto.email;
if (dto.password !== undefined) updateData.password = dto.password;
if (dto.age !== undefined) updateData.age = dto.age;
if (dto.role !== undefined) updateData.role = dto.role;
return updateData;
}
}

src/controllers/user.controller.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/user.service';
import {
CreateUserDto,
UpdateUserDto,
validateCreateUserDto,
} from '../dto';
import { ApiResponseDto, UserResponseDto } from '../dto/api-response.dto';
import { UserMapper } from '../dto/mappers/user.mapper';
export class UserController {
constructor(private userService: UserService) {}
async getAllUsers(req: Request, res: Response, next: NextFunction) {
try {
const { page = 1, limit = 10, search } = req.query;
const { data, total } = await this.userService.findWithPagination(
Number(page),
Number(limit),
search as string
);
const response = ApiResponseDto.paginated(
UserMapper.toResponseList(data),
Number(page),
Number(limit),
total
);
res.json(response);
} catch (error) {
next(error);
}
}
async getUserById(req: Request, res: Response, next: NextFunction) {
try {
const id = parseInt(req.params.id);
const user = await this.userService.findById(id);
if (!user) {
return res.status(404).json(
ApiResponseDto.error('User not found')
);
}
res.json(
ApiResponseDto.success(UserMapper.toResponse(user))
);
} catch (error) {
next(error);
}
}
async createUser(req: Request, res: Response, next: NextFunction) {
try {
// Validate DTO
const dto = validateCreateUserDto(req.body);
// Create user
const user = await this.userService.create(dto);
// Return response
res.status(201).json(
ApiResponseDto.success(
UserMapper.toResponse(user),
'User created successfully'
)
);
} catch (error) {
next(error);
}
}
async updateUser(req: Request, res: Response, next: NextFunction) {
try {
const id = parseInt(req.params.id);
const dto = req.body as UpdateUserDto;
const user = await this.userService.update(id, dto);
res.json(
ApiResponseDto.success(
UserMapper.toResponse(user),
'User updated successfully'
)
);
} catch (error) {
next(error);
}
}
async deleteUser(req: Request, res: Response, next: NextFunction) {
try {
const id = parseInt(req.params.id);
await this.userService.delete(id);
res.status(204).send();
} catch (error) {
next(error);
}
}
}

This chapter covered:

  • DTO Basics: Defining data shapes for API requests/responses
  • Validation: Manual validation with error handling
  • Response DTOs: Standardized API response format
  • Entity Mapping: Converting between DTOs and entities
  • Controller Integration: Using DTOs in HTTP handlers

Key takeaways:

  1. Always validate incoming data with DTOs
  2. Use separate DTOs for create, update, and response
  3. Map entities to DTOs before sending responses
  4. Provide meaningful validation error messages

Chapter 25: Input Validation & Error Handling


Last Updated: February 2026