Ejercicio 1 — Superficie
POST /api/auth/register/ público 201
POST /api/auth/login/ público 200
GET /api/events/ público 200
POST /api/events/ organizador 201
GET /api/events/{uuid}/ público 200
PATCH /api/events/{uuid}/ organizador 200
GET /api/events/{uuid}/seats/ público 200
GET /api/events/{uuid}/availability/ público 200 (cacheada)
POST /api/reservations/ autenticado 201 / 409
GET /api/reservations/{uuid}/ dueño 200
DELETE /api/reservations/{uuid}/ dueño 204 (idempotente)
POST /api/reservations/{uuid}/payments/ dueño 202 (cola) / 201
POST /api/payments/webhook/ sistema (firma) 200
POST /api/gift-cards/{code}/redeem/ autenticado 200 / 409Crítica típica: POST /api/events/{id}/reserve/ (verbo en ruta) se convierte en POST /api/reservations/ con {event, seats} en el cuerpo — el recurso creado es la reserva. Sub-recurso de acción sí para payments/ (crea un pago DENTRO de la reserva: es creación de sub-recurso real, no verbo disfrazado).
Ejercicio 2 — Handler
from rest_framework.views import exception_handler
def problem_handler(exc, context):
response = exception_handler(exc, context)
if response is None:
if isinstance(exc, DomainError):
return Response(
{"type": f"https://docs.ticketflow.app/errors/{exc.code}",
"title": exc.title, "status": exc.status, "detail": exc.detail},
status=exc.status)
return None # 500 lo lleva el logger (26)
response.data = {"type": f"https://docs.ticketflow.app/errors/{response.status_code}",
"title": TITLES.get(response.status_code, "Error"),
"status": response.status_code,
"detail": response.data.get("detail", ""),
"errors": response.data.get("errors", [])}
return responseDomainError desde reservar() produce exactamente el cuerpo diseñado — el 409 de la 10 ahora habla contrato. 3. Mínimo obligatorio: type, title, status (detail opcional: hay errores que solo dicen "no" — el frontend mapea por type).
Ejercicio 3 — Paginación
DEFAULT_PAGINATION_CLASSpor vista conpagination_classoverride: CursorPagination en el listado público (orden estable porstarts_at), PageNumberPagination en admin.page=999en offset escanea y descarta 24.975 filas (OFFSET es O(n)); el cursor sigue siendo seek por índice: O(log n) + 25. Con 500 eventos no se nota; con 500k es la diferencia entre 300ms y 3ms.- django-filter:
filterset_fieldso FilterSet explícito;OrderingFilterconordering_fields = ["starts_at", "title"]— lo que no está en la lista, no ordena.
Ejercicio 4 — UUID
- Migración: AddField UUID default uuid4 nullable → backfill (11: por lotes) → unique. O default uuid4 unique de golpe (dataloader de Django llena las filas nuevas; en filas existentes el default aplica en Postgres moderno).
lookup_field = "uuid"en viewsets; serializers exponenuuid = UUIDField(read_only=True). El entero deja de ser direccionable.- Sí sería enumerable a priori (UUID no protege por sí): la capa que lo impide es la autorización (dueño o staff — Lección 21) + scopes del token. UUID = higiene anti-enumeración, no control de acceso.
Resumen del profesor
- Recursos en plural, jerarquía corta, acciones como sub-recursos de creación.
- Un formato de error (RFC 7807) con
typemapeable: el frontend deja de adivinar. - Cursor para lo público que crece; offset para el panel; whitelist en ordering.
- UUID público como higiene; la barrera real es la autorización.
Después: Lección 14 — Idempotencia y versionado.