Skip to main content

Data Transfer Objects (DTOs)

DTOs are simple data containers that define the input and output boundaries of use cases. They decouple the application layer from external concerns and provide clear contracts for data exchange.

Purpose

DTOs in Soft-Bee API serve to:
  • Define clear boundaries between layers
  • Validate input data before it reaches business logic
  • Prevent exposure of internal domain models to external layers
  • Provide type safety and automatic validation
  • Enable easy serialization/deserialization for APIs

Implementation

Soft-Bee uses Pydantic for DTOs, providing automatic validation and documentation:
from pydantic import BaseModel, EmailStr, Field, validator
from datetime import datetime
from typing import Optional, Dict, Any

Authentication DTOs

Authentication feature DTOs are located in src/features/auth/application/dto/auth_dto.py:1

Login Request DTO

class LoginRequestDTO(BaseModel):
    """DTO para request de login"""
    email: EmailStr
    password: str = Field(..., min_length=1, description="Password del usuario")
    remember_me: bool = Field(default=False, description="Recordar sesión")
Purpose: Captures login credentials with optional “remember me” functionality. Validation:
  • email: Must be valid email format (Pydantic EmailStr)
  • password: Required, minimum 1 character
  • remember_me: Optional boolean, defaults to False
Usage in Use Case:
def execute(self, request: LoginRequestDTO) -> Tuple[Optional[LoginResponseDTO], Optional[str]]:
    user = self.user_repository.find_by_email(request.email)
    # ... validate password
    expires_in = 86400 if request.remember_me else 900

Login Response DTO

class LoginResponseDTO(BaseModel):
    """DTO para response de login"""
    access_token: str
    refresh_token: str
    token_type: str = "bearer"
    expires_in: int
    user: Dict[str, Any]
Purpose: Returns authentication tokens and user information. Fields:
  • access_token: JWT token for API authentication
  • refresh_token: Token for refreshing access token
  • token_type: Always “bearer” for JWT
  • expires_in: Token expiration time in seconds
  • user: User details (id, email, username, verification status)

Register Request DTO

class RegisterRequestDTO(BaseModel):
    """DTO para request de registro"""
    email: EmailStr
    username: str = Field(..., min_length=3, max_length=50)
    password: str = Field(..., min_length=8)
    confirm_password: str = Field(..., min_length=8)
    
    @validator('username')
    def validate_username(cls, v):
        if not v.replace('_', '').isalnum():
            raise ValueError('Username can only contain letters, numbers and underscores')
        return v
    
    @validator('confirm_password')
    def passwords_match(cls, v, values):
        if 'password' in values and v != values['password']:
            raise ValueError('Passwords do not match')
        return v
Location: src/features/auth/application/dto/auth_dto.py:19 Purpose: Validates new user registration data. Validation Rules:
  • email: Valid email format
  • username: 3-50 characters, alphanumeric plus underscores only
  • password: Minimum 8 characters
  • confirm_password: Must match password field
Custom Validators:
  1. validate_username - Ensures username contains only allowed characters
  2. passwords_match - Verifies password confirmation matches

Register Response DTO

class RegisterResponseDTO(BaseModel):
    """DTO para response de registro"""
    id: str
    email: str
    username: str
    is_verified: bool
    created_at: datetime
    message: str = "User registered successfully"
Purpose: Returns newly created user information. Fields:
  • id: Unique user identifier
  • email: User’s email address
  • username: Chosen username
  • is_verified: Email verification status (typically False for new users)
  • created_at: Account creation timestamp
  • message: Success message

Token Management DTOs

Refresh Token Request DTO

class RefreshTokenRequestDTO(BaseModel):
    """DTO para request de refresh token"""
    refresh_token: str
Purpose: Accepts refresh token for generating new access token.

Refresh Token Response DTO

class RefreshTokenResponseDTO(BaseModel):
    """DTO para response de refresh token"""
    access_token: str
    refresh_token: Optional[str] = None
    token_type: str = "bearer"
    expires_in: int
Purpose: Returns new access token and optionally a new refresh token.

Logout Request DTO

class LogoutRequestDTO(BaseModel):
    """DTO para request de logout"""
    refresh_token: Optional[str] = None
Purpose: Invalidates refresh token during logout.

Verify Token Request/Response DTOs

class VerifyTokenRequestDTO(BaseModel):
    """DTO para request de verificación de token"""
    token: str

class VerifyTokenResponseDTO(BaseModel):
    """DTO para response de verificación de token"""
    is_valid: bool
    user_id: Optional[str] = None
    email: Optional[str] = None
    expires_at: Optional[datetime] = None
Purpose: Validates JWT tokens and returns decoded information.

DTO Design Principles

1. Separation of Concerns

DTOs are distinct from domain entities:
# DTO - Simple data container
class RegisterRequestDTO(BaseModel):
    email: EmailStr
    username: str
    password: str

# Domain Entity - Contains business logic
class User:
    def __init__(self, email: Email, username: str, hashed_password: str):
        self.email = email  # Email is a value object
        self.username = username
        self.hashed_password = hashed_password
        self._events = []
    
    def login_successful(self):
        # Business logic here
        pass

2. Input Validation

Pydantic validators ensure data integrity before reaching use cases:
@validator('username')
def validate_username(cls, v):
    if not v.replace('_', '').isalnum():
        raise ValueError('Username can only contain letters, numbers and underscores')
    return v

3. Type Safety

Strong typing prevents runtime errors:
class LoginRequestDTO(BaseModel):
    email: EmailStr  # Must be valid email
    password: str    # Must be string
    remember_me: bool  # Must be boolean

4. Self-Documenting

Field descriptions provide API documentation:
password: str = Field(..., min_length=1, description="Password del usuario")
remember_me: bool = Field(default=False, description="Recordar sesión")

DTO Usage Pattern

Typical flow from API request to use case:
# 1. API Layer receives raw data
@auth_bp.route('/register', methods=['POST'])
def register():
    data = request.get_json()
    
    # 2. Create DTO (automatic validation)
    try:
        register_dto = RegisterRequestDTO(**data)
    except ValidationError as e:
        return jsonify({"error": str(e)}), 400
    
    # 3. Pass DTO to use case
    response, error = register_use_case.execute(register_dto)
    
    # 4. Return response DTO as JSON
    if error:
        return jsonify({"error": error}), 400
    return jsonify(response.dict()), 201

Benefits

  1. Validation at the Edge - Invalid data never reaches business logic
  2. Clear Contracts - Explicit input/output expectations
  3. Testability - Easy to create test data
  4. Documentation - Self-documenting API contracts
  5. Versioning - Can evolve DTOs without changing domain
  6. Framework Independence - Business logic doesn’t depend on Flask/FastAPI

Best Practices

  1. Keep DTOs Simple - No business logic, only data and validation
  2. One DTO per Operation - Don’t reuse DTOs across different operations
  3. Validate Early - Use Pydantic validators for all constraints
  4. Descriptive Names - Use Request and Response suffixes
  5. Optional Fields - Provide sensible defaults where appropriate
  6. Documentation - Add field descriptions for API documentation

See Also

Build docs developers (and LLMs) love