Stack: Django + DRF · Proyecto: TicketFlow Estado: Impartida — ejercicios pendientes de entrega Prerrequisito: Lección 00 — Entorno y primer endpoint
Objetivos
Al terminar esta lección podrás:
- Describir la anatomía exacta de una petición y una respuesta HTTP.
- Elegir método y código de estado correctos justificándolo (entrevistas).
- Explicar qué cabeceras controlan CORS, caché, cookies y compresión, y verificarlas con
curl. - Razonar sobre keep-alive, HTTPS/TLS y por qué importan en producción.
- Aplicar todo esto en DRF: por qué
Responseelige códigos por ti y cuándo no dejarle que lo haga.
1. Anatomía de una petición HTTP
HTTP es un protocolo de texto (ahora binario en HTTP/2) sobre TCP, sin estado: cada petición lleva toda la información necesaria. Estructura:
POST /api/events/42/reserve HTTP/1.1 ← método + ruta + versión
Host: ticketflow.example.com ← obligatoria en HTTP/1.1
Content-Type: application/json ← qué envío
Authorization: Bearer eyJhbGci... ← quién soy
Idempotency-Key: 7f3c9a2e ← anti-duplicados (Lección 14)
Content-Length: 58
{"seat_id": 1051, "quantity": 2} ← cuerpo (opcional)Y la respuesta:
HTTP/1.1 201 Created
Location: /api/reservations/9172/ ← dónde quedó lo creado
Content-Type: application/json
Cache-Control: no-store
{"id": 9172, "status": "pending_payment", "seats": [1051, 1052]}Concepto clave para entrevistas: HTTP es sin estado (stateless) pero puede mantener sesión mediante cookies o tokens. No confundas estado del protocolo con estado de la aplicación.
Métodos: seguro, idempotente, cacheable
| Método | Seguro | Idempotente | Uso típico en TicketFlow |
|---|---|---|---|
| GET | listar eventos, ver disponibilidad | ||
| POST | crear reserva, iniciar compra | ||
| PUT | reemplazar un evento completo | ||
| PATCH | * | editar precio de un evento | |
| DELETE | cancelar reserva |
\* PATCH es idempotente solo si el parche es absoluto ({"price": 20}), no relativo ({"price": "+2"}).
- Seguro = no modifica estado del servidor (el cliente puede repetirlo sin miedo).
- Idempotente = repetir N veces produce el mismo resultado que una vez. Crucial en pagos y webhooks (Lección 14).
Códigos de estado: los que de verdad usarás
| Código | Significado | Ejemplo TicketFlow |
|---|---|---|
| 200 OK | éxito genérico | GET de disponibilidad |
| 201 Created | recurso creado | POST de reserva (con Location) |
| 204 No Content | éxito sin cuerpo | DELETE de reserva |
| 400 Bad Request | petición mal formada / inválida | asientos negativos |
| 401 Unauthorized | no autenticado (el nombre engaña) | sin token |
| 403 Forbidden | autenticado pero sin permiso | comprador intenta editar evento |
| 404 Not Found | no existe (o no quieres decir que existe) | — |
| 409 Conflict | choque de estado | asiento ya reservado |
| 422 Unprocessable | válido en forma, inválido en negocio | evento ya pasado |
| 429 Too Many Requests | rate limit | bot comprando todo |
| 500 | bug nuestro | ¡nuestro fallo, no del cliente! |
Regla de oro: los 4xx son culpa del cliente; los 5xx, del servidor. Devolver 500 por una validación fallida es un bug de tu API.
2. Cabeceras que dominas o sufres
CORS (Cross-Origin Resource Sharing)
El navegador bloquea por defecto las respuestas de otro origen. CORS es cómo el servidor autoriza esos orígenes. Petición preflight OPTIONS con:
Access-Control-Allow-Origin: https://ticketflow.app
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type, Idempotency-KeyEn DRF lo gestiona django-cors-headers (lo instalamos en los ejercicios). El CORS se valida en el servidor, no es una medida de seguridad del cliente: si la API pública no tiene CORS, es que no quiere navegadores de otros orígenes; curl ignora CORS siempre.
Cookies y atributos de seguridad
Set-Cookie: sessionid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/HttpOnly→ JavaScript no puede leerla (mitiga XSS robando sesión).Secure→ solo viaja por HTTPS.SameSite=Lax|Strict|None→ mitiga CSRF restringiendo envíos cross-site.- Caduca:
Expires/Max-Age; sin ellas es cookie de sesión (muere al cerrar navegador).
Caché HTTP
Cache-Control: max-age=60, public # política fuerte (recomendada)
ETag: "v17" # identificador de la versión del recurso- Cliente repite petición con
If-None-Match: "v17"→ si no cambió, servidor responde 304 Not Modified sin cuerpo: ahorro de ancho de banda. Cache-Control: no-storepara datos sensibles (pagos).no-cache= "puedes cachear pero valida siempre" (nombre tramposo).- Caché compartida (CDN/proxy) frente a privada:
privateevita que un CDN cachee respuestas con datos de usuario.
Negociación y cuerpo
Content-Type: application/json(petición) y respuesta. DRF hace content negotiation: conAccept: application/jsonvstext/html(el navegador te muestra el HTML navegable del APIView).Accept-Language,Accept-Encoding: gzip, br→ compresión de respuesta (la hace el servidor o el proxy, Lección 02).
3. Keep-alive, HTTPS y TLS
Keep-alive y el coste de las conexiones
HTTP/1.0 abría una conexión TCP por petición (costoso: handshake TCP + TLS). HTTP/1.1 introdujo conexiones persistentes (Connection: keep-alive por defecto): muchas peticiones reutilizan la conexión. HTTP/2 va más allá con multiplexado (varias peticiones en paralelo por una sola conexión).
En producción tu Django no habla directamente con el mundo: Gunicorn/Uvicorn detrás de Nginx mantiene keep-alive hacia el cliente y conexiones eficientes hacia la app (Lección 02).
HTTPS/TLS en 4 pasos
- Handshake: cliente y servidor negocian versión y cifrados.
- Certificado: el servidor presenta su certificado firmado por una CA en la que el cliente confía.
- Claves: mediante (clásicamente) intercambio Diffie-Hellman se deriva una clave simétrica de sesión.
- Cifrado simétrico para todos los datos (rápido).
Esto da confidencialidad (nadie lee), integridad (nadie altera) y autenticidad (el servidor es quien dice ser). En 2026 el estándar es TLS 1.3: 1 round-trip (o 0 en reconexión) y cifrados modernos.
HSTS: cabecera
Strict-Transport-Securityque le dice al navegador "solo hables HTTPS conmigo durante X tiempo", impidiendo ataques de downgrade.
4. HTTP en DRF: lo que el framework hace (y no debe hacer) por ti
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(["POST"])
def reserve(request):
if request.method == "POST":
# DRF: request.data ya parsea JSON según Content-Type
# Response sin status= devolvería 200: ¡para crear un recurso di 201!
return Response({"status": "pending_payment"}, status=status.HTTP_201_CREATED)Puntos que un senior controla:
- Códigos explícitos: el default silencioso de
Responsees 200. ParaPOSTque crea → 201; para acciones de negocio → 200; para "aceptado para procesar en cola" → 202 (Lección 06). request.datavsrequest.query_params: cuerpo vs query string. Confundirlos es fuente de bugs.- Errores consistentes: DRF devuelve
{"detail":...}o{"campo": [errores]}; definiremos un formato de error propio y centralizado (Lección 26). - Cabeceras de respuesta:
Response(headers={"Location":...}),Cache-Controlen listados cacheables. - 401 vs 403 en DRF:
NotAuthenticated(401) vsPermissionDenied(403). DRF lo resuelve, pero debes saber explicarlo.
Autoevaluación (respóndeme en el chat)
- ¿Por qué
POST /reserveno es idempotente y qué consecuencias tiene si el cliente reintenta por timeout? - Cliente envía credenciales válidas pero su rol no permite esa acción. ¿401 o 403? ¿Por qué?
- ¿Qué cabecera(s) usarías para que un listado de eventos públicos se cachee 60s en CDN pero los datos de pago nunca?
- ¿Qué pasa exactamente cuando el navegador hace un preflight y el servidor no permite la cabecera
Idempotency-Key? - ¿Por qué devolver 500 cuando un JSON viene mal formado es un error del backend, no del cliente?
Continúa con los ejercicios. Las soluciones solo tras intentarlo.