System Design
Diseño de APIs y contratos
Explica cómo diseñar contratos de API claros, versionables y seguros con recursos, operaciones, errores, idempotencia, compatibilidad y ownership.
- Última actualización
- Actualizada
- Nivel
- Aplicación
System Design
Explica cómo diseñar contratos de API claros, versionables y seguros con recursos, operaciones, errores, idempotencia, compatibilidad y ownership.
Una API es un contrato de comportamiento entre un proveedor y sus consumidores. Incluye operaciones, datos, errores, seguridad, compatibilidad y expectativas operativas; el protocolo es solo una parte.
Diseñar una API consiste en expresar capacidades del sistema de forma comprensible, estable y verificable. El contrato debe permitir que un consumidor use la capacidad sin conocer detalles internos de persistencia o implementación.
Una API improvisada suele copiar tablas, exponer campos internos, usar errores inconsistentes y romper clientes con cada cambio. El diseño contractual reduce ambigüedad y permite evolucionar proveedor y consumidores de forma coordinada.
necesidad del consumidor
→ operación del dominio
→ entrada válida
→ autorización
→ resultado o error observable
→ garantías operativas
→ evolución compatibleDefine primero:
El estilo se elige después de conocer estas necesidades.
Debe expresar intención. POST /orders/{id}/cancel puede comunicar mejor una transición que un PATCH genérico que permita cualquier estado.
Incluye campos, formatos, límites, opcionalidad y reglas. La validación sintáctica no sustituye reglas de negocio.
Debe indicar datos garantizados, estados parciales y campos que pueden faltar.
Un error útil posee código estable, mensaje comprensible, detalles accionables, correlación y semántica clara.
Autenticación responde quién es; autorización decide si puede realizar la operación sobre ese recurso.
Timeouts, cuotas, tamaños máximos, orden, consistencia e idempotencia forman parte del contrato.
POST /v1/orders
Idempotency-Key: 9b8c4e6a
Authorization: Bearer <token>
Content-Type: application/json
{
"branchId": "br_123",
"customerId": "cus_456",
"items": [
{ "productId": "prd_1", "quantity": 2 }
]
}Respuesta:
HTTP/1.1 201 Created
Location: /v1/orders/ord_789
{
"id": "ord_789",
"status": "confirmed",
"total": { "amount": "42.00", "currency": "COP" },
"version": 1
}Location permite consultar el recurso creado.version puede ayudar a controlar actualizaciones concurrentes.{
"type": "https://example.com/problems/insufficient-stock",
"title": "Insufficient stock",
"status": 409,
"code": "ORDER_STOCK_INSUFFICIENT",
"detail": "Two units are unavailable for product prd_1",
"traceId": "tr_abc",
"fields": [
{ "path": "items[0].quantity", "reason": "available=1" }
]
}Los códigos HTTP clasifican el resultado; el código de dominio permite a clientes tomar decisiones estables.
Una operación idempotente produce el mismo efecto observable al repetirse con la misma intención. GET, PUT o DELETE pueden diseñarse así, pero el verbo no garantiza una implementación correcta.
Para POST de pagos o pedidos:
Simple, pero puede duplicar u omitir elementos si el conjunto cambia y se degrada con offsets altos.
Usa una posición estable basada en orden. Es más adecuada para grandes volúmenes y datos cambiantes.
El contrato debe definir orden determinista, límites y comportamiento cuando el cursor expira.
No expongas consultas arbitrarias sin límites. Define campos filtrables, operadores, orden permitido y coste máximo. Los filtros también deben respetar autorización.
Para evitar sobrescrituras perdidas puede usarse una versión o ETag:
PATCH /v1/orders/ord_789
If-Match: "7"Si la versión cambió, responde conflicto y obliga a refrescar. Esto no soluciona toda concurrencia, pero hace visible una garantía.
Una operación rápida puede devolver resultado final. Un proceso largo puede responder 202 Accepted con un recurso de operación:
{
"operationId": "op_123",
"status": "pending",
"statusUrl": "/v1/operations/op_123"
}No devuelvas éxito final cuando el trabajo apenas fue encolado.
Aprovecha semántica HTTP, caché y recursos. Funciona bien para APIs públicas y CRUD con comportamiento explícito.
Permite seleccionar campos y un esquema tipado. Introduce control de complejidad, autorización por campo, N+1 y caché más delicada.
Expresa operaciones y contratos fuertes, con serialización eficiente. Es útil entre servicios controlados, pero puede acoplar ciclos de despliegue y no siempre es amigable para navegador o terceros.
Permiten notificar a consumidores externos. Requieren firma, retries, idempotencia, replay y registro de entregas.
Comunican hechos a múltiples consumidores de forma asíncrona. No son un reemplazo automático de consultas o comandos.
Prefiere cambios aditivos:
Cambios peligrosos:
El versionado no elimina la necesidad de migración ni soporte temporal.
Los tests de contrato verifican que el proveedor continúe satisfaciendo expectativas reales de consumidores. No sustituyen pruebas funcionales ni revisión semántica, pero detectan incompatibilidades temprano.
El cliente no sabe si la operación ocurrió. Una clave de idempotencia y consulta de estado permiten resolver incertidumbre.
Puede duplicar efectos si la operación no es idempotente.
Clientes robustos suelen ignorar campos nuevos; servidores deben decidir si rechazan entradas desconocidas para evitar errores silenciosos.
Define si es inmediata, asíncrona, reversible o lógica, y qué ocurre con recursos relacionados.
Un recurso antes visible puede dejar de serlo; cachés y enlaces no deben saltarse la validación.
200 aunque falle el negocio.Si dos módulos se despliegan juntos, comparten ciclo de vida y no necesitan aislamiento, una llamada local puede ser más simple. Una frontera remota introduce latencia, fallos parciales, seguridad y versionado.
202 Accepted es más correcto que 201 Created?Eventos e integración entre sistemas desarrolla contratos asíncronos, entrega, orden, duplicados y recuperación.