Dtos
Chapter 24: Data Transfer Objects (DTOs)
Section titled “Chapter 24: Data Transfer Objects (DTOs)”Defining Data Shapes in TypeScript
Section titled “Defining Data Shapes in TypeScript”24.1 DTO Overview
Section titled “24.1 DTO Overview”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 | +------------------+ +------------------+
================================================================================24.2 Creating DTOs
Section titled “24.2 Creating DTOs”export class CreateUserDto { name: string; email: string; password: string; age?: number; role?: string;}
// src/dto/update-user.dto.tsexport class UpdateUserDto { name?: string; email?: string; password?: string; age?: number; role?: string;}
// src/dto/user-response.dto.tsexport class UserResponseDto { id: number; name: string; email: string; age?: number; role: string; isActive: boolean; createdAt: Date; updatedAt: Date;}
// src/dto/pagination.dto.tsexport class PaginationDto { page: number = 1; limit: number = 10;
get offset(): number { return (this.page - 1) * this.limit; }}24.3 Advanced DTOs with Validation
Section titled “24.3 Advanced DTOs with Validation”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 functionexport 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'; }}24.4 Response DTOs
Section titled “24.4 Response DTOs”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.tsexport 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), }, }; }}24.5 Converting Between DTOs and Entities
Section titled “24.5 Converting Between DTOs and Entities”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; }}24.6 Using DTOs in Controllers
Section titled “24.6 Using DTOs in Controllers”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); } }}24.7 Summary
Section titled “24.7 Summary”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:
- Always validate incoming data with DTOs
- Use separate DTOs for create, update, and response
- Map entities to DTOs before sending responses
- Provide meaningful validation error messages
Next Chapter
Section titled “Next Chapter”Chapter 25: Input Validation & Error Handling
Last Updated: February 2026