No leas esto sin haber entregado antes tus intentos. Los errores que no se cometen no se aprenden.
Ejercicio 1 — Anatomía con curl -v
- Línea de petición:
GET /api/health/ HTTP/1.1; línea de estado:HTTP/1.1 200 OK. Tres cabeceras (cualquiera de):Date,Server,Content-Type,Content-Length,Vary,Allow... Cuerpo:{"status":"ok","service":"ticketflow"}. - Con
Accept: text/html, DRF negocia contenido: como el endpoint usaapi_viewy el renderer HTML viene activo por defecto en DEBUG, devolvería el HTML navegable de DRF (o un 406 si no hay renderer compatible). La cabeceraAcceptguía qué representación quieres del mismo recurso.
Ejercicio 2 — Diseño del contrato
Petición (caso feliz):
POST /api/events/42/reserve HTTP/1.1
Host: ticketflow.example.com
Content-Type: application/json
Authorization: Bearer <token>
Idempotency-Key: 7f3c9a2e ← clave anti-duplicado si el cliente reintenta
{"seats": [1051, 1052]}Respuesta feliz:
HTTP/1.1 201 Created
Location: /api/reservations/9172/
Content-Type: application/json
{"id": 9172, "status": "pending_payment", "seats": [1051, 1052], "expires_at": "..."}Respuesta con conflicto:
HTTP/1.1 409 Conflict
{"code": "seat_already_reserved", "detail": "El asiento 1051 ya está reservado"}Comentario: 409 Conflict es el código para choques de estado (asiento ocupado). 422 también se defiende, pero 409 comunica mejor "tu petición es válida, pero el mundo cambió". Veremos esto a fondo en la Lección 10 (transacciones: el asiento se decide en una transacción con bloqueo, no por "comprobar y reservar").
Ejercicio 3 — Corrige el código
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.exceptions import NotFound
@api_view(["POST", "DELETE"]) # si se usa como cancelación vía DELETE, DELETE es idempotente
def cancel_reservation(request, pk):
try:
reservation = Reservation.objects.get(pk=pk)
except Reservation.DoesNotExist:
return Response({"detail": "No existe"}, status=status.HTTP_404_NOT_FOUND)
reservation.status = "cancelled"
reservation.save()
return Response({"status": "cancelled"}, status=status.HTTP_200_OK)Fallos originales:
- 200 implícito: sin
status=, DRF responde 200. Si el endpoint actualiza el estado, 200 es aceptable; pero si el diseño fuera "DELETE de la reserva", lo correcto es 204 No Content. El error real es la ambigüedad: no dejar el código al azar. - 500 si no existe:
objects.getlanzaDoesNotExist→ Django lo traduce a 500 en una vista API salvo que lo captures o usesget_object_or_404. Un recurso inexistente es 404, no 500.
Bonus senior: cancelar debería ser idempotente: cancelar dos veces la misma reserva no debería fallar (devolver 200 con estado cancelled o 204). Lo retomamos en la Lección 14.
Ejercicio 4 — CORS en la práctica
- Con
Origin: http://localhost:5173: respuesta 200 OK (preflight) con cabeceras comoAccess-Control-Allow-Origin: http://localhost:5173,Access-Control-Allow-Methods:...POST...,Access-Control-Allow-Headers: authorization, content-type. El navegador considera la petición autorizada. - Con
Origin: http://evil.example: django-cors-headers no emite las cabecerasAccess-Control-*(y la petición puede responder 200 pero sin autorización CORS). El navegador la bloqueará. Nota fina: el bloqueo lo hace el navegador, no el servidor;curlnunca se bloquea. - Tras añadir
Idempotency-KeyaCORS_ALLOW_HEADERS, la respuesta del preflight la incluye enAccess-Control-Allow-Headers. Sin eso, el navegador rechazaría cualquier petición real que la envíe.
Lo que acabas de probar: el mecanismo exacto que usa un frontend en localhost:5173 para poder consumir tu API de localhost:8000.
Ejercicio 5 — Cookies seguras
Set-Cookie: sessionid=...; Path=/; SameSite=Lax(másHttpOnlysi es de sesión de Django; con el admin verás tambiéncsrftoken).- Con
SESSION_COOKIE_SECURE = True, la cookie no viaja por HTTP plano:curl http://...no la verá en la respuesta (o el navegador la descartaría). Solo se envía por HTTPS. - -
HttpOnly: JS no puede leerla → XSS no roba la sesión. Secure: no viaja en claro → MitM no la captura.SameSite=Lax/Strict: el navegador no la adjunta en peticiones cross-site → mitiga CSRF.
Ejercicio 6 — ETag y 304
- Primera respuesta: 200 con
ETag: "<hash>"yCache-Control: max-age=60,...(ademásExpires,Last-Modifiedsegún el helper). - Con
If-None-Match: "<tu-etag>": respuesta 304 Not Modified y cuerpo vacío. - Ahorro: el servidor no serializa ni transfiere el cuerpo (ni el cliente lo descarga); se intercambian solo cabeceras. En listados grandes es una ganancia enorme. Contra-partido: la validación implica comparar ETags (aquí trivial; con contenido dinámico hay que calcularlo bien para no regenerar todo).
Ejercicio 7 — Razona como diseñador
- PATCH
/reservations/1051con{"expires_at": "+10min"}(o subrecurso/reservations/1051/extension) → 200. Es una modificación parcial del recurso, no creación. - PATCH
/events/42con{"status": "cancelled"}→ 200 (o PUT si se reemplaza la representación completa; PUT sería idempotente). Marcar cancelado no "borra" nada: no es DELETE. - GET
/events/42/availability→ 200 (cacheable conCache-Controlcorto o ETag; la disponibilidad cambia, usa TTL bajo). - POST
/checkout→ 202 Accepted con cuerpo tipo{"job_id": "...", "status": "queued"}. El proceso se encola (Lección 06) y la confirmación llega por polling o webhook. 202 comunica exactamente "aceptado, aún no listo".
Ejercicio 8 — HTTPS y TLS
- Confidencialidad (anula eavesdropper en la red: Wi-Fi abierto, ISP), integridad (anula a quien altera paquetes/proxy malicioso) y autenticidad del servidor (anula phishing/MITM con certificado falso: la CA es la que te dice que el servidor es quien dice ser).
- TLS 1.3 reduce el handshake a 1 round-trip (y 0 con session resumption/TLS 1.3 + TCP ya establecida o QUIC): menos RTT antes de la primera petición = TTFB menor en cada conexión nueva. Además elimina cifrados legacy.
- El TLS termina en Nginx: el cliente cifra hasta Nginx; de Nginx a Gunicorn/Django viaja (normalmente) en claro dentro de la red privada. "Terminación TLS" es eso. Implicación: el tráfico interno debe estar protegido por otros medios (red privada, mTLS o TLS interno) — detalle de arquitectura que retomamos en la Lección 02 y en el Módulo 9.
Resumen del profesor
- HTTP es simple: método + ruta + cabeceras + cuerpo; lo difícil es la disciplina semántica (códigos correctos, idempotencia, seguridad en cabeceras).
- Un buen backend no solo responde JSON: responde con el código correcto, las cabeceras correctas y cuerpos consistentes.
- Todo lo de hoy se verifica con
curl -i/-v. Hazlo siempre: las cabeceras mienten menos que la documentación.