Data Transfer Objects (DTOs)
Design DTOs for clean API contracts.
By EMEPublished: February 20, 2025
dtoapi designmappingcontracts
A Simple Analogy
DTOs are like translators between layers. Your internal model talks to the API model through a DTO.
Why DTOs?
- Encapsulation: Hide internal structure
- Versioning: Change internals without API breaks
- Validation: Input validation at boundary
- Projection: Expose only needed fields
- Security: Don't expose sensitive data
Basic DTO
// Entity (internal)
public class User
{
public int Id { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public string PhoneNumber { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
}
// DTO (API contract)
public class UserDto
{
public int Id { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
}
// Note: PasswordHash and IsActive are hidden
Input DTO
// Request DTO with validation
public class CreateUserRequest
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[MinLength(8)]
public string Password { get; set; }
[Phone]
public string PhoneNumber { get; set; }
}
// Handler
[HttpPost]
public async Task<ActionResult<UserDto>> CreateUser(CreateUserRequest request)
{
var user = new User
{
Email = request.Email,
PasswordHash = HashPassword(request.Password),
PhoneNumber = request.PhoneNumber
};
await _repository.SaveAsync(user);
return Ok(MapToDto(user));
}
Mapping with AutoMapper
// Configuration
public class UserMappingProfile : Profile
{
public UserMappingProfile()
{
CreateMap<User, UserDto>();
CreateMap<CreateUserRequest, User>()
.ForMember(u => u.PasswordHash,
opt => opt.MapFrom(r => HashPassword(r.Password)));
}
}
// Usage
public async Task<UserDto> GetUserAsync(int id)
{
var user = await _repository.GetAsync(id);
return _mapper.Map<UserDto>(user);
}
Nested DTOs
public class OrderDto
{
public int Id { get; set; }
public string OrderNumber { get; set; }
public List<OrderItemDto> Items { get; set; }
public CustomerDto Customer { get; set; }
}
public class OrderItemDto
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}
public class CustomerDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
Best Practices
- One per operation: Request and response DTOs separate
- Validation: Add data annotations
- Immutability: Use records when possible
- Versioning: Use separate DTOs for API versions
- Documentation: Document required/optional fields
Related Concepts
- Value objects
- Anemic models
- Anti-patterns
- API versioning
Summary
Use DTOs to create clean API contracts. Hide implementation details and provide input validation at boundaries.