Objetivos de Aprendizaje
Al finalizar esta clase, seras capaz de:
- Entender que es un DTO (Data Transfer Object)
- Crear DTOs con Pydantic
- Diferenciar DTOs de entrada y salida
- Mapear entre Entities y DTOs
- Validar datos con Pydantic
Que es un DTO? (10 min)
DTO (Data Transfer Object) es un objeto que transporta datos entre procesos, sin logica de negocio.
Por que usar DTOs?
- Separar API del modelo de dominio
- Controlar que datos se exponen
- Validar entrada de usuario
- Documentar la API automaticamente
Problema sin DTOs
# ❌ Exponer entity directamente
@router.get("/usuarios/{id}")
def obtener(id: int):
usuario = repo.obtener(id)
return usuario # Expone password_hash!
Solucion con DTOs
# ✅ Usar DTO para controlar respuesta
@router.get("/usuarios/{id}", response_model=UsuarioResponse)
def obtener(id: int):
usuario = repo.obtener(id)
return UsuarioResponse.from_entity(usuario) # Sin password
Pydantic Models (20 min)
from pydantic import BaseModel, Field, EmailStr, field_validator
from typing import Optional
from datetime import datetime
class UsuarioBase(BaseModel):
"""DTO base con campos comunes."""
username: str = Field(..., min_length=3, max_length=50)
email: EmailStr
class UsuarioCreate(UsuarioBase):
"""DTO para crear usuario."""
password: str = Field(..., min_length=8, max_length=100)
nombre_completo: Optional[str] = Field(None, max_length=100)
@field_validator('password')
@classmethod
def validar_password(cls, v: str) -> str:
if not any(c.isupper() for c in v):
raise ValueError('Password debe tener al menos una mayuscula')
if not any(c.isdigit() for c in v):
raise ValueError('Password debe tener al menos un numero')
return v
class UsuarioUpdate(BaseModel):
"""DTO para actualizar usuario."""
email: Optional[EmailStr] = None
nombre_completo: Optional[str] = Field(None, max_length=100)
class UsuarioResponse(UsuarioBase):
"""DTO para respuesta."""
id: int
nombre_completo: Optional[str]
activo: bool
creado_en: datetime
model_config = {"from_attributes": True}
Tipos de DTOs (15 min)
| Tipo | Uso | Ejemplo |
|---|---|---|
| Create | Datos para crear | UsuarioCreate |
| Update | Datos para actualizar | UsuarioUpdate |
| Response | Datos a devolver | UsuarioResponse |
| List | Lista de items | UsuarioListResponse |
DTO para listar con paginacion
from pydantic import BaseModel
from typing import Generic, TypeVar, List
T = TypeVar("T")
class PaginatedResponse(BaseModel, Generic[T]):
"""Respuesta paginada generica."""
items: List[T]
total: int
page: int
page_size: int
pages: int
@classmethod
def create(cls, items: List[T], total: int, page: int, page_size: int):
return cls(
items=items,
total=total,
page=page,
page_size=page_size,
pages=(total + page_size - 1) // page_size
)
# Uso
class UsuarioListResponse(PaginatedResponse[UsuarioResponse]):
pass
Mapeo Entity-DTO (20 min)
# domain/entities.py
class Usuario:
"""Entity de dominio."""
def __init__(self, id: int, username: str, email: str,
password_hash: str, activo: bool = True):
self.id = id
self.username = username
self.email = email
self.password_hash = password_hash
self.activo = activo
self.creado_en = datetime.now(timezone.utc)
# api/schemas/usuario.py
class UsuarioResponse(BaseModel):
id: int
username: str
email: str
activo: bool
creado_en: datetime
@classmethod
def from_entity(cls, usuario: Usuario) -> "UsuarioResponse":
"""Convierte Entity a DTO."""
return cls(
id=usuario.id,
username=usuario.username,
email=usuario.email,
activo=usuario.activo,
creado_en=usuario.creado_en
)
# application/services/usuario_service.py
class UsuarioService:
def obtener(self, id: int) -> UsuarioResponse:
usuario = self.repo.obtener_por_id(id)
if not usuario:
raise ValueError("Usuario no encontrado")
return UsuarioResponse.from_entity(usuario)
def crear(self, data: UsuarioCreate) -> UsuarioResponse:
# Convertir DTO a Entity
usuario = Usuario(
id=0, # Se asigna en repo
username=data.username,
email=data.email,
password_hash=self._hash_password(data.password)
)
# Guardar
usuario = self.repo.crear(usuario)
# Convertir Entity a DTO
return UsuarioResponse.from_entity(usuario)
Tip: El mapeo puede automatizarse con librerias como
pydantic-mapper o dataclasses.
Ejercicio: DTOs Tarea (15 min)
Crear DTOs completos para Tarea:
# api/schemas/tarea.py
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
from enum import Enum
class Prioridad(str, Enum):
BAJA = "baja"
MEDIA = "media"
ALTA = "alta"
URGENTE = "urgente"
class TareaBase(BaseModel):
titulo: str = Field(..., min_length=1, max_length=200)
descripcion: Optional[str] = Field(None, max_length=1000)
prioridad: Prioridad = Prioridad.MEDIA
class TareaCreate(TareaBase):
proyecto_id: int
class TareaUpdate(BaseModel):
titulo: Optional[str] = Field(None, min_length=1, max_length=200)
descripcion: Optional[str] = Field(None, max_length=1000)
completada: Optional[bool] = None
prioridad: Optional[Prioridad] = None
class TareaResponse(TareaBase):
id: int
completada: bool
proyecto_id: int
creado_en: datetime
model_config = {"from_attributes": True}
E6 Proyecto Final: Implementa DTOs para todas las entidades del sistema.
Resumen
- DTO: Objeto para transferir datos entre capas
- Pydantic: Libreria para crear DTOs con validacion
- Create/Update/Response: Diferentes DTOs por operacion
- Mapeo: Conversion entre Entity y DTO
- Validacion: Pydantic valida automaticamente