Isaac.

architecture

Data Transfer Objects (DTOs)

Design DTOs for clean API contracts.

By Emem IsaacMay 1, 20223 min read
#dto#api design#mapping#contracts
Share:

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

  1. One per operation: Request and response DTOs separate
  2. Validation: Add data annotations
  3. Immutability: Use records when possible
  4. Versioning: Use separate DTOs for API versions
  5. 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.

Share:

Written by Emem Isaac

Expert Software Engineer with 15+ years of experience building scalable enterprise applications. Specialized in ASP.NET Core, Azure, Docker, and modern web development. Passionate about sharing knowledge and helping developers grow.

Ready to Build Something Amazing?

Let's discuss your project and explore how my expertise can help you achieve your goals. Free consultation available.

💼 Trusted by 50+ companies worldwide | ⚡ Average response time: 24 hours