Stack: Django REST Framework · Proyecto: TicketFlow Estado: Publicada Prerrequisito: Lección 12 — NoSQL
Objetivos
- Diseñar la API de TicketFlow como recursos consistentes (nombres, jerarquía, verbos).
- Definir un formato de error único (RFC 7807) que el frontend pueda tratar como contrato.
- Paginar, filtrar y ordenar con criterio (offset vs cursor).
- Decidir identidad pública: UUIDs expuestos, IDs internos nunca.
1. Recursos: sustantivos en plural, jerarquía corta
GET /api/events/ listado público
POST /api/events/ (organizador) crear
GET /api/events/{uuid}/ detalle
PATCH /api/events/{uuid}/ editar parcial
GET /api/events/{uuid}/seats/ asientos de un evento
GET /api/events/{uuid}/availability/ disponibilidad (cacheada)
POST /api/reservations/ crear reserva (los asientos van en el cuerpo)
GET /api/reservations/{uuid}/ mi reserva
DELETE /api/reservations/{uuid}/ cancelar (idempotente)
POST /api/reservations/{uuid}/payments/ iniciar pago de esa reserva
POST /api/payments/webhook/ webhook de la pasarelaReglas del oficio: verbos en el body, no en la ruta (la acción es el verbo HTTP; si de verdad es una acción de dominio, sub-recurso o POST corto como /payments/webhook/); sin verbos tipo /getEvents/; anidación máxima 1 nivel (más profundo = URLs frágiles); plurales consistentes.
2. Errores: un formato, todos los endpoints
RFC 7807 (application/problem+json) — el contrato de error:
{
"type": "https://docs.ticketflow.app/errors/seat-already-reserved",
"title": "El asiento ya está reservado",
"status": 409,
"detail": "El asiento A-12 acaba de reservarse por otra persona",
"instance": "/api/reservations/",
"errors": [{"field": "seats", "index": 1, "code": "taken"}]
}En DRF: un exception handler centralizado (Lección 26 lo sistematiza; aquí el diseño): todo error —validación de serializer, negocio, auth— sale con esta forma. type es un URI documentado (el frontend puede mapear códigos a textos/iconos); errors lleva el detalle por campo. Los códigos HTTP correctos (Lección 01) + este cuerpo = contrato completo.
3. Paginación, filtros y ordenación
- Offset (
?page=2&page_size=25): simple, pero salta filas si insertan mientras paginas, ypage=99999es caro (OFFSET escanea). Para paneles de admin: suficiente. - Cursor (
?cursor=eyJpZCI6MTIzfQ): estable ante inserciones y O(1) por página; opaco y solo-adelante. Para listados públicos de alto tráfico (feed de eventos): el correcto. - Filtros:
?state=PUBLISHED&starts_after=2027-01-01— nombres de campo reales, sin DSL inventado;django-filterlo estandariza. Rango:_after/_beforemejor que_min/_maxcuando es temporal. - Orden:
?ordering=-starts_atcon lista blanca de campos (nuncaordering=; DROP— el serializer no es SQL, pero el whitelist es higiene).
4. Identidad pública: UUIDs fuera, enteros dentro
La API expone uuid (no secuencial: no filtra volumen de negocio ni permite enumerar recursos); la PK entera queda interna. En DRF: lookup_field = "uuid" en el router/viewset, y SERIALIZE el uuid, no el id. Bonus de seguridad por oscuridad justificada: enumerar reservas ajenas probando IDs consecutivos deja de ser viable (la auth de la 18-21 es la barrera real; esto es higiene).
5. Contrato antes que suerte: OpenAPI adelantado
Con drf-spectacular, el schema OpenAPI se genera del código: los 15/26 consolidan esta práctica (validación y errores centralizados). Desde hoy: cada endpoint nuevo de TicketFlow se añade con su serializer documentado — el contrato se despliega con el código, no se escribe a mano después.
6. Cabeceras: la mitad oculta del contrato REST
El REST que se diseña en el cuerpo ignora que muchas decisiones viven en CABECERAS — y son parte del contrato exactamente igual:
| Cabecera | Qué promete | Lección que la explota |
|---|---|---|
ETag / If-None-Match | "si no cambió, 304 sin body" | 38/39: ahorro de ancho en el browse |
Vary: Accept-Encoding | "la respuesta depende del encoding" | 39: el caché no sirve comprimido a quien no lo pide |
Cache-Control | "quién puede cachear y hasta cuándo" | 38: public para el catálogo, no-store para lo personal |
Retry-After | "cuándo vale la pena reintentar" | 26/54: el 429 y el 503 se acompañan de segundos |
X-Request-ID | "el hilo de esta petición" | 45: el trace_id que el cliente puede reportar |
X-RateLimit-Remaining | "cuántas peticiones le quedan" | 23: el cliente ajusta su ritmo sin recibir 429 |
Location | "dónde quedó el recurso creado" | 14: el 202 del pago apunta al endpoint de estado |
Las dos reglas del proyecto: (1) si una cabecera cambia el comportamiento del cliente, entra en el test de contrato (35: los headers se verifican como el body); (2) Location en el 201 SIEMPRE — el cliente que crea no adivina la URL del recurso nuevo.
7. Idempotencia de los verbos, por la práctica
La tabla de verbos con su promesa de idempotencia (la que hace posible el retry de la 54): GET idempotente y seguro (no cambia nada); PUT idempotente (la misma entidad, el mismo resultado); DELETE idempotente (borrar lo ya borrado → 404 o 204, pero el estado final es el mismo); POST NO idempotente (por eso el Idempotency-Key de la 14 existe); PATCH depende del tipo de parche (un SET status=X es idempotente; un incremento NO). El diseño de TicketFlow respeta la tabla: el cobro es POST con clave de idempotencia; la cancelación de reserva es DELETE sobre la reserva (idempotente: cancelar dos veces no doble-cancela) — y el job de la 29 puede reintentarla sin miedo.
Autoevaluación
- ¿Por qué
POST /api/reservations/con asientos en el cuerpo y noPOST /api/events/{id}/reserve/? ¿Cuándo sí justificarías el sub-recurso de acción? - ¿Qué gana el frontend con
type(URI) en cada error, además deltitle? - Un listado público tendrá picos de 100 rps y crece sin fin: ¿offset o cursor y por qué?
- ¿Qué ataca el uso de UUIDs públicos y por qué NO sustituye a la autenticación/autorización?
- ¿Por qué el whitelist de campos de
orderinges obligatorio aunque DRF escape el parámetro?
Continúa con los ejercicios. Las soluciones solo tras intentarlo.