#JSON cambia la representación, no las reglas
Una API permite que otro programa use tu aplicación mediante HTTP. JSON es una representación de datos, no una capa de seguridad ni una arquitectura especial. La autenticación, los permisos por objeto, los límites de tamaño y las transacciones siguen siendo necesarios.
No hace falta introducir otro framework para unos pocos endpoints. Django tiene JsonResponse, y los formularios pueden servir para validar un diccionario JSON. Si más adelante necesitas muchas APIs, esquemas, serialización compleja o generación de clientes, evalúa una herramienta dedicada y aprende su contrato; no la añadas antes de entender el que estás exponiendo.
#Un endpoint pequeño con entradas explícitas
Este ejemplo independiente valida una solicitud de presupuesto, sin guardarla ni llamar a un proveedor. Puede añadirse a articulos/views.py y registrarse como path("presupuesto/", views.presupuesto, name="presupuesto"). Usa una sesión de la web; por tanto conserva CSRF.
import json
from django import forms
from django.http import JsonResponse
from django.views.decorators.http import require_POST
class PresupuestoEntrada(forms.Form):
unidades = forms.IntegerField(min_value=1, max_value=100)
urgente = forms.BooleanField(required=False)
@require_POST
def presupuesto(request):
if not request.user.is_authenticated:
return JsonResponse({"error": "autenticacion_requerida"}, status=401)
if request.content_type != "application/json":
return JsonResponse({"error": "tipo_no_admitido"}, status=415)
if len(request.body) > 8192:
return JsonResponse({"error": "entrada_demasiado_grande"}, status=413)
try:
datos = json.loads(request.body)
except (json.JSONDecodeError, UnicodeDecodeError):
return JsonResponse({"error": "json_invalido"}, status=400)
if not isinstance(datos, dict) or set(datos) - {"unidades", "urgente"}:
return JsonResponse({"error": "campos_no_admitidos"}, status=400)
if "urgente" in datos and type(datos["urgente"]) is not bool:
return JsonResponse({"error": "urgente_debe_ser_booleano"}, status=400)
form = PresupuestoEntrada(datos)
if not form.is_valid():
return JsonResponse({"errores": form.errors.get_json_data()}, status=400)
return JsonResponse({"unidades": form.cleaned_data["unidades"], "aceptado": True})
La lista de campos aceptados evita que un cliente crea que descuento=90 fue aplicado cuando en realidad se ignoró. El campo booleano JSON exige true o false, no cadenas como "false" cuya interpretación puede sorprender. IntegerField convierte entradas admitidas por su contrato de formulario; si tu API exige estrictamente enteros JSON, añade type(datos.get("unidades")) is int antes de validarlo, rechazando también booleanos.
form.errors.get_json_data() devuelve mensajes y códigos estructurados. La respuesta nunca incluye un traceback ni la contraseña de la base. El límite sobre request.body sirve a nivel de aplicación, pero leer el cuerpo ya pudo consumir recursos: impón también el límite correspondiente en el proxy/servidor. CSRF puede rechazar antes de ejecutar esta función; un test de su contrato debe contemplar la capa de autenticación y no desactivarla en producción para obtener un JSON cómodo.
#Contratos que permiten evolucionar
Un endpoint de creación suele devolver 201 con un identificador y, si procede, Location. Uno de trabajo diferido puede devolver 202 con una URL donde consultar el estado. 202 significa aceptado, no terminado. Si la operación no llegó a guardarse o ponerse duraderamente en cola, anunciar 202 puede perder trabajo sin que el cliente lo sepa.
Define errores estables como intervalo_ocupado junto a un mensaje legible. Los clientes no deberían tener que comparar frases traducidas. El mismo servicio de reserva puede alimentar una view HTML y una API JSON; ambas deben pasar la identidad autenticada y filtrar recursos, sin duplicar una versión menos protegida de la regla.
Para escrituras reintentables, una clave de idempotencia identifica la misma intención del cliente. Guarda clave, carga normalizada y resultado; si se repite la clave con otra carga, rechaza la ambigüedad. La unicidad de una sala/intervalo evita solapes, pero no devuelve por sí sola el resultado de la primera petición perdida.
#Una dependencia lenta consume capacidad
Si una view síncrona espera diez segundos a una API, mantiene ocupado un worker durante ese tiempo. Con cuatro workers y muchas peticiones lentas, otras operaciones pueden quedarse sin capacidad aunque la CPU esté libre. Cada llamada necesita límites de conexión/lectura, un presupuesto total y tratamiento explícito del fallo.
Un timeout de lectura de un cliente HTTP suele limitar una espera de red, no necesariamente toda la secuencia de redirecciones, reintentos y lectura de cuerpo. Un reintento también consume el presupuesto del usuario. No reintentes automáticamente un cobro o una creación externa sin conocer su idempotencia; una respuesta perdida no demuestra que la operación no ocurriera.
Evita una llamada externa dentro de una transacción con filas bloqueadas. Primero valida lo necesario, registra intención cuando proceda y deja el trabajo externo fuera del bloqueo. Si necesitas combinar resultado externo y datos locales, define estados como pendiente, confirmado, fallido o necesita_revision; no prometas atomicidad entre dos sistemas que no la comparten.
#Elegir petición, async o trabajo durable
| Trabajo | Dónde encaja al principio | Motivo |
|---|---|---|
| Leer y dibujar 20 registros | View síncrona | Camino corto y fácil de medir |
| Esperar varias APIs breves independientes | Async con clientes compatibles, si la carga lo justifica | Solapa esperas; no elimina límites |
| Generar informe de varios minutos | Job persistente y worker | Debe sobrevivir al cierre del navegador |
| CPU intensiva | Proceso/worker con capacidad limitada | No se acelera por escribir async def |
| Aviso tras una reserva | Outbox si su entrega importa | Conserva la intención junto al commit |
Django 5.2 tiene views y consultas asíncronas, pero sus transacciones no funcionan como una unidad async nativa. Mantén el bloque transaccional en una función síncrona y llama mediante sync_to_async si estás dentro de una view async. asgiref, instalado con Django, proporciona ese adaptador. No desactives las comprobaciones de seguridad async para poder llamar al ORM bloqueante desde el event loop.
WSGI es la interfaz síncrona con el servidor; ASGI permite una pila asíncrona y conexiones duraderas. Cambiar a ASGI no vuelve asíncronas las bibliotecas bloqueantes ni crea durabilidad. asyncio.create_task() lanzado desde una petición no sustituye un worker: la tarea puede desaparecer al reiniciar el proceso. El curso de asincronía desarrolla cancelación, límites y composición antes de usar esas piezas en una aplicación.
#Un job necesita más que un booleano
Una tarea durable registra identidad, organización, estado, entrada/versiones, intentos y resultado o error. El cliente consulta su estado usando permisos normales. El worker reclama trabajo durante un tiempo limitado, registra progreso y puede reintentar con un presupuesto. Tras una caída debe ser posible distinguir «no iniciado» de «quizá ejecutó un efecto externo».
Para un volumen pequeño puedes comenzar con una tabla y un comando de gestión que la procese, con reclamación transaccional y leases. Cuando una cola aporte ventajas, Streams de Redis explica entrega, confirmación y recuperación. No es obligatorio instalar un framework de tareas antes de entender esas garantías; al elegir uno, verifica qué persiste y cuándo considera confirmado un trabajo.
El patrón outbox guarda la tarea o evento junto a los datos de negocio. El worker todavía necesita idempotencia: puede repetir una entrega tras caer entre enviar y registrar el éxito.
#Caché: una copia con una antigüedad aceptable
Una caché almacena un resultado reconstruible para evitar repetir trabajo. Antes de añadirla, define cuánto puede quedar obsoleto y quién puede leerlo. Un contador de artículos publicados puede tolerar 30 segundos; la autorización para entrar en una organización o la disponibilidad al reservar requieren otras garantías.
En la web de artículos, esta función usa la API de caché de Django:
from django.core.cache import cache
from articulos.models import Articulo
def numero_publicados():
clave = "articulos:publicados:v1"
valor = cache.get(clave)
if valor is None:
valor = Articulo.objects.filter(publicado=True).count()
cache.set(clave, valor, timeout=30)
return valor
None representa fallo de caché; no uses if not valor, porque cero es un resultado válido. La configuración por defecto usa memoria local por proceso: dos workers no comparten esa caché. Un backend Redis puede compartirla, pero añade una dependencia de red que puede fallar. Aprende primero su diseño de caché y configura timeouts y comportamiento degradado.
Invalida después del commit para no borrar/publicar antes de que existan los datos. Aun así hay carreras: un lector puede haber leído el valor viejo, llegar después de la invalidación y volver a escribirlo en caché. Un TTL limita la duración de esa copia, pero no proporciona consistencia inmediata. Si esa inconsistencia cambia una decisión crítica, usa la fuente de verdad o diseña versionado adecuado.
Una clave privada debe incluir organización y demás dimensiones de acceso relevantes; cachear panel:inicio para todos puede filtrar datos entre usuarios. También las caches HTTP/CDN necesitan políticas de Cache-Control y Vary adecuadas. No envíes una página autenticada a una caché pública sin comprender cómo se particiona.
Comprobación: describe qué respuesta recibe un cliente si el proveedor externo tarda demasiado, qué queda guardado si se cierra el navegador y cómo se rechaza la segunda entrega del mismo trabajo. Después identifica un dato que sí tolera caché y otro que no debe decidirse desde una copia obsoleta.