La API como producto y como contrato
Una API es una promesa pública: qué se puede pedir, con qué forma y qué se recibirá a cambio. A diferencia del código interno, no se puede refactorizar sin avisar: en cuanto alguien la consume, cada cambio es una negociación. Por eso el diseño de APIs se parece más a diseñar un producto que a escribir funciones: hay usuarios (los desarrolladores que la integran), una experiencia (coherencia, mensajes de error, documentación) y un ciclo de vida (nacimiento, evolución, retirada).
Principios que sostienen todo lo demás:
- Coherencia: lo mismo se hace siempre igual (nombres, formatos de fecha, paginación, errores).
- Previsibilidad: quien conoce un endpoint adivina los demás.
- Mínima sorpresa: nada de efectos secundarios ocultos en una lectura.
- Evolucionabilidad: el diseño asume que cambiará y deja sitio para ello.
- Seguridad por defecto: lo no permitido explícitamente está denegado.
Elegir el estilo de comunicación
Petición y respuesta
Tiempo real y eventos
| Estilo | Modelo | Punto fuerte | Punto débil |
|---|---|---|---|
| REST sobre HTTP | Recursos con URL y verbos | Universal, cacheable, fácil de depurar | Varias llamadas para pantallas complejas; sobrecarga o carencia de datos |
| GraphQL | Un endpoint y consultas tipadas | El cliente pide justo lo que necesita | Caché más difícil, coste de consultas a controlar, complejidad en el servidor |
| gRPC | Llamadas binarias con contrato Protocol Buffers | Rápido, tipado fuerte, streaming | Poco amigable desde el navegador, menos legible a mano |
| WebSocket | Canal bidireccional permanente | Tiempo real en ambos sentidos | Estado por conexión, más difícil de escalar y de cachear |
| SSE | Flujo unidireccional servidor a cliente sobre HTTP | Sencillo, reconexión automática | Solo en un sentido, conexiones abiertas |
| Webhook | El servidor llama a una URL del cliente | Evita el sondeo | El receptor debe estar disponible y ser seguro |
| Mensajería | Colas y temas con un broker | Desacoplamiento temporal, absorbe picos | Consistencia eventual, orden y duplicados que gestionar |
Regla práctica: REST para lo público y de propósito general, gRPC entre servicios propios, GraphQL cuando muchos clientes con necesidades distintas consumen un grafo de datos, eventos y webhooks para notificar, WebSocket o SSE para empujar datos en vivo. Se pueden combinar: lo importante es que cada API tenga un criterio claro.
Recorrido de una petición
El orden importa: se comprueba primero lo más barato y lo más crítico. Un límite de uso rechaza el abuso antes de gastar CPU en validar; la autenticación se resuelve antes de tocar los datos; la validación del esquema va antes de la lógica de negocio.
Diseño REST con rigor
Modelo de madurez de Richardson
| Nivel | Qué añade |
|---|---|
| 0 | Un solo endpoint y un solo verbo, como un túnel (estilo RPC sobre HTTP) |
| 1 | Recursos: cada cosa tiene su propia URL |
| 2 | Verbos HTTP y códigos de estado con su significado |
| 3 | Hipermedia: las respuestas incluyen enlaces a las acciones posibles |
La mayoría de las API útiles viven en el nivel 2. El nivel 3 (HATEOAS) es valioso cuando se quiere que los clientes descubran acciones sin codificar rutas, pero aporta poco si el cliente ya conoce el contrato.
Recursos y URL
- Sustantivos en plural, no verbos:
/pedidos,/pedidos/42,/pedidos/42/lineas. - Jerarquía solo cuando expresa pertenencia real; evitar anidar más de dos niveles.
- Para acciones que no encajan en CRUD, modelarlas como un recurso (
POST /pedidos/42/cancelaciones) mejor que como un verbo en la URL. - Un único estilo de mayúsculas y separadores en todo el contrato.
Verbos, seguridad e idempotencia
| Verbo | Uso | Seguro | Idempotente |
|---|---|---|---|
| GET | Leer | Sí | Sí |
| HEAD | Leer solo cabeceras | Sí | Sí |
| POST | Crear o ejecutar una acción | No | No |
| PUT | Reemplazar un recurso completo | No | Sí |
| PATCH | Modificar parcialmente | No | Depende de cómo se defina |
| DELETE | Eliminar | No | Sí |
Seguro significa que no cambia el estado; idempotente que repetir la misma petición deja el sistema igual que hacerla una vez. Las redes fallan y los clientes reintentan, así que la idempotencia es una propiedad de fiabilidad, no un detalle académico.
Para operaciones que por naturaleza no lo son (un pago, una creación), se usa una clave de idempotencia enviada por el cliente:
POST /pagos HTTP/1.1
Host: api.ejemplo.com
Content-Type: application/json
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
{ "importe": 4990, "moneda": "EUR", "pedido": "ped_42" }El servidor guarda el resultado asociado a esa clave y, si llega de nuevo, devuelve la misma respuesta sin repetir el cobro.
Códigos de estado que importan
| Código | Cuándo |
|---|---|
| 200 OK | Lectura o actualización correcta con cuerpo |
| 201 Created | Recurso creado; cabecera Location con su URL |
| 202 Accepted | Aceptado para proceso asíncrono |
| 204 No Content | Éxito sin cuerpo (por ejemplo, un borrado) |
| 304 Not Modified | El cliente ya tiene la versión vigente |
| 400 Bad Request | Petición mal formada |
| 401 Unauthorized | Falta autenticación válida |
| 403 Forbidden | Autenticado, pero sin permiso |
| 404 Not Found | El recurso no existe (o no se quiere revelar que existe) |
| 409 Conflict | Choca con el estado actual (duplicado, versión obsoleta) |
| 412 Precondition Failed | Falló una condición como If-Match |
| 415 Unsupported Media Type | Formato del cuerpo no admitido |
| 422 Unprocessable Content | Sintaxis correcta pero reglas de validación incumplidas |
| 429 Too Many Requests | Límite de uso superado |
| 500 Internal Server Error | Fallo inesperado del servidor |
| 503 Service Unavailable | Temporalmente no disponible; puede llevar Retry-After |
Un error nunca debe viajar en un 200. El código de estado es la primera pista que lee cualquier cliente, caché o monitor.
Errores con Problem Details (RFC 9457)
Un formato de error estable y estándar evita que cada endpoint invente el suyo. El tipo de contenido es application/problem+json:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.ejemplo.com/errores/validacion",
"title": "La petición no cumple las reglas de validación",
"status": 422,
"detail": "El campo email no tiene un formato válido.",
"instance": "/usuarios",
"errores": [
{ "campo": "email", "codigo": "formato_invalido" }
]
}El cliente trata el error por type (estable y documentado), muestra title o detail y puede ampliar con extensiones propias. No se incluyen trazas, nombres de tablas ni rutas internas.
Colecciones: paginación, filtrado y orden
Paginación por desplazamiento (offset): simple, permite saltar a una página, pero es costosa con desplazamientos grandes y inestable si se insertan o borran filas mientras se pagina (se repiten o se pierden elementos).
Paginación por cursor: el servidor devuelve un marcador opaco de dónde continuar. Es estable, escala y es la opción recomendada para colecciones grandes o que cambian.
Cuando no llega cursor siguiente, se ha terminado la colección.
GET /pedidos?limite=2&estado=pagado&orden=-creado HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{
"datos": [
{ "id": "ped_42", "total": 4990, "estado": "pagado" },
{ "id": "ped_41", "total": 1250, "estado": "pagado" }
],
"siguiente": "eyJpZCI6InBlZF80MSJ9"
}Buenas prácticas:
- El cursor es opaco: el cliente no lo construye ni lo interpreta.
- Siempre hay un límite por defecto y un máximo.
- Filtros como parámetros simples (
estado=pagado); orden con un parámetro explícito y una lista blanca de campos permitidos. - Campos opcionales (
campos=id,total) y expansión de relaciones (incluir=cliente) con cuidado para no crear un GraphQL accidental.
Caché y concurrencia con cabeceras
Las cabeceras HTTP ya resuelven problemas que a menudo se reimplementan mal.
GET /productos/7 HTTP/1.1
HTTP/1.1 200 OK
ETag: "v3-a1b2c3"
Cache-Control: private, max-age=60GET /productos/7 HTTP/1.1
If-None-Match: "v3-a1b2c3"
HTTP/1.1 304 Not ModifiedETagidentifica la versión;If-None-Matchevita retransmitir lo que el cliente ya tiene.Cache-Controldefine quién puede cachear y durante cuánto (public,private,no-store,max-age).- Control de concurrencia optimista: el cliente envía
If-Matchcon el ETag que leyó al actualizar; si alguien lo modificó, el servidor responde 412 y evita pisar cambios ajenos.
PUT /productos/7 HTTP/1.1
If-Match: "v3-a1b2c3"
Content-Type: application/json
{ "nombre": "Cuaderno A5", "precio": 590 }Operaciones largas
Si una operación tarda, se responde 202 con la URL de un recurso de estado que el cliente consulta (o se notifica por webhook):
POST /informes HTTP/1.1
HTTP/1.1 202 Accepted
Location: /informes/trabajos/88GraphQL en la práctica
Un único endpoint y un esquema tipado. El cliente describe la forma exacta de la respuesta:
query {
pedido(id: "ped_42") {
total
cliente { nombre }
lineas { producto { nombre } cantidad }
}
}Lo que hay que diseñar con cuidado:
- Problema N+1: resolver cada campo con una consulta independiente; se mitiga agrupando peticiones (batching con un cargador por petición).
- Coste y profundidad de consultas: límites de profundidad, complejidad y paginación obligatoria para que una consulta no tumbe el servicio.
- Errores: conviven errores de transporte y errores parciales dentro de la respuesta; se documentan los códigos de dominio.
- Evolución: se añaden campos sin romper, se marcan los viejos como obsoletos y se retiran más tarde; no hay versiones numeradas.
- Seguridad: la autorización se aplica por campo y objeto, no solo en la puerta de entrada.
gRPC y contratos binarios
El contrato se escribe en Protocol Buffers y genera clientes y servidores en varios lenguajes:
syntax = "proto3";
service Pedidos {
rpc Obtener (ObtenerPedidoRequest) returns (Pedido);
rpc Seguir (ObtenerPedidoRequest) returns (stream EventoPedido);
}
message ObtenerPedidoRequest { string id = 1; }
message Pedido { string id = 1; int64 total = 2; string estado = 3; }Reglas de compatibilidad: nunca reutilizar ni cambiar el número de un campo, reservar los retirados, y añadir solo campos nuevos. Admite llamadas unarias y flujos en ambos sentidos, con plazos (deadlines) explícitos en cada llamada.
Tiempo real: WebSocket y SSE
| Aspecto | SSE | WebSocket |
|---|---|---|
| Dirección | Servidor a cliente | Ambas |
| Protocolo | HTTP normal | Protocolo propio tras un saludo HTTP |
| Reconexión | Automática, con identificador del último evento | La resuelve la aplicación |
| Uso típico | Progreso, notificaciones, respuestas en flujo | Chat, edición colaborativa, juegos |
Ambos exigen pensar en: autenticación al abrir la conexión, renovación del token en conexiones largas, límites de conexiones por usuario, latidos (heartbeats) para detectar conexiones muertas y qué pasa con los mensajes perdidos durante una desconexión.
Webhooks fiables
Un webhook invierte la dirección: el emisor llama al receptor cuando ocurre algo.
Del lado del emisor:
- Firmar cada entrega con un secreto compartido y una marca de tiempo para evitar suplantaciones y repeticiones.
- Reintentar con espera creciente ante errores y documentar la política.
- Incluir un identificador único de evento.
Del lado del receptor:
- Verificar la firma sobre el cuerpo en bruto antes de procesar.
- Responder 2xx con rapidez y procesar en segundo plano.
- Ser idempotente: la entrega es «al menos una vez», así que puede llegar duplicado y fuera de orden.
POST /hooks/pagos HTTP/1.1
Content-Type: application/json
X-Firma: t=1700000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{ "id": "evt_9001", "tipo": "pago.completado", "datos": { "pedido": "ped_42" } }Mensajería y eventos
Cuando la comunicación es asíncrona, el contrato es el mensaje:
- Sobre del evento: identificador único, tipo, versión del esquema, marca de tiempo y origen.
- Hechos, no órdenes: un evento cuenta algo que ya ocurrió (
pedido.confirmado). - Esquema versionado y compatible hacia atrás; los consumidores ignoran los campos que no conocen.
- Orden y duplicados: se asume entrega al menos una vez, y el orden solo se garantiza dentro de una clave de partición.
- Cola de mensajes fallidos: los mensajes que no se pueden procesar tras varios intentos se apartan para inspección, no se pierden ni bloquean el resto.
Contract-first: OpenAPI y AsyncAPI
El contrato se escribe antes del código y es la fuente de verdad.
En paralelo
openapi: 3.1.0
info:
title: API de pedidos
version: 1.0.0
paths:
/pedidos/{id}:
get:
summary: Obtener un pedido
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
"200":
description: El pedido
content:
application/json:
schema:
$ref: "#/components/schemas/Pedido"
"404":
description: No existe
components:
schemas:
Pedido:
type: object
required: [id, total, estado]
properties:
id: { type: string }
total: { type: integer, description: Importe en céntimos }
estado: { type: string, enum: [pendiente, pagado, enviado] }Qué se obtiene de un buen contrato:
- Documentación siempre al día y navegable.
- Mocks para que el cliente avance sin esperar al servidor.
- Validación de peticiones y respuestas contra el esquema.
- Clientes y esqueletos generados en varios lenguajes.
- Pruebas de contrato que fallan en integración continua si la implementación se desvía, y linting del contrato con reglas de estilo propias.
AsyncAPI cumple el mismo papel para canales y mensajes (colas, temas, WebSocket): describe qué se publica, dónde y con qué esquema.
Versionado, compatibilidad y deprecación
Qué rompe y qué no
| Cambio | ¿Rompe? |
|---|---|
| Añadir un campo opcional a una respuesta | No (si los clientes ignoran lo desconocido) |
| Añadir un endpoint nuevo | No |
| Añadir un parámetro opcional | No |
| Hacer obligatorio un parámetro | Sí |
| Quitar o renombrar un campo | Sí |
| Cambiar el tipo o el significado de un campo | Sí |
| Añadir un valor a un enumerado | Puede romper a clientes con tratamiento estricto |
| Endurecer una validación | Sí |
| Cambiar un código de estado o un formato de error | Sí |
El principio de robustez para clientes: ser estrictos al enviar y tolerantes al recibir (ignorar campos desconocidos, tratar enumerados nuevos con un valor por defecto).
Estrategias de versionado
| Estrategia | Ejemplo | Comentario |
|---|---|---|
| En la ruta | /v2/pedidos | Explícita y fácil de enrutar; la más extendida |
| En una cabecera | Cabecera de versión o de fecha | URL limpia, más difícil de probar a mano |
| Por tipo de contenido | Tipo de medio con versión | Purista, poco práctico |
| Evolución sin versiones | Solo cambios compatibles | Ideal si se puede mantener la disciplina |
La opción más sana es evitar la versión mayor el máximo tiempo posible con cambios compatibles, y reservar la ruptura para cuando no haya alternativa.
Ciclo de deprecación
- Se anuncia la deprecación con fecha de retirada, documentación y guía de migración.
- Se avisa en las propias respuestas con cabeceras:
HTTP/1.1 200 OK
Deprecation: true
Sunset: Wed, 31 Dec 2026 23:59:59 GMT
Link: </v2/pedidos>; rel="successor-version"- Se mide quién sigue usando lo antiguo y se les contacta.
- Se retira solo cuando el plazo se ha cumplido y el uso es residual.
Autenticación y autorización
| Mecanismo | Úsalo para | Observaciones |
|---|---|---|
| API key | Identificar una aplicación | No identifica a un usuario; se envía por cabecera, nunca en la URL; rotable y revocable |
| OAuth 2.0 | Delegar acceso limitado a un tercero | Define flujos y alcances (scopes); es autorización, no autenticación |
| OpenID Connect | Saber quién es el usuario | Capa de identidad sobre OAuth 2.0 que añade un token de identidad |
| JWT | Token firmado y autocontenido | Se valida sin consultar; difícil de revocar antes de caducar, así que vida corta |
| mTLS | Servicio a servicio | Ambos extremos se autentican con certificados |
El token de refresco permite renovarlo sin volver a pedir credenciales.
Reglas de oro:
- Flujo de código de autorización con PKCE para aplicaciones públicas (navegador, móvil).
- Tokens de acceso de vida corta y renovación con token de refresco.
- Validar siempre firma, emisor, destinatario y caducidad de un JWT; no aceptar algoritmos arbitrarios.
- Autorizar en el servidor, objeto por objeto: que el token sea válido no implica que el usuario pueda ver ese recurso. La falta de este control (acceso a objetos ajenos cambiando un identificador) es de las vulnerabilidades más frecuentes en APIs.
- La identidad sale del token, no del cuerpo de la petición.
- Mínimo privilegio en los alcances.
Límites de uso y protección
- Rate limiting por clave, usuario o IP, con algoritmos como cubo de fichas (token bucket) o ventana deslizante.
- Comunicar el límite con cabeceras y un 429 acompañado de
Retry-After. - Cuotas por plan, distintas de los límites de ráfaga.
- Tamaño máximo de cuerpo, de página y de consulta.
- Tiempos máximos (timeouts) y cancelación de peticiones que ya no interesan.
- Validación estricta del esquema en la frontera: se rechaza lo que no se esperaba, no se ignora en silencio.
- CORS restringido a los orígenes necesarios.
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/problem+json
{ "type": "https://api.ejemplo.com/errores/limite", "title": "Demasiadas peticiones", "status": 429 }Observabilidad y operación
- Identificador de correlación en cada petición, devuelto en la respuesta y propagado a los servicios internos.
- Métricas por endpoint: tasa de peticiones, tasa de errores y latencia en percentiles.
- Registros estructurados sin datos sensibles ni secretos.
- Comprobaciones de salud distintas para «vivo» y «listo para recibir tráfico».
- Política clara de reintentos: el cliente reintenta solo operaciones idempotentes o con clave de idempotencia, con espera creciente y aleatoria.
Experiencia de desarrollador
La documentación forma parte de la API. Una buena referencia incluye:
- Una guía de inicio que lleve a la primera llamada correcta en minutos.
- Ejemplos reales de petición y respuesta, también de errores.
- Una referencia completa generada del contrato.
- Un registro de cambios con las deprecaciones y las fechas.
- Un entorno de pruebas (sandbox) con datos ficticios.
- Errores accionables: el mensaje explica qué falló y cómo corregirlo.
Flujo de trabajo de principio a fin
- Entender a los consumidores y los casos de uso, no solo los datos que hay en la base.
- Escribir el contrato (OpenAPI, AsyncAPI, proto) y revisarlo con quienes lo van a usar.
- Levantar un mock para que cliente y servidor avancen en paralelo.
- Implementar validando contra el contrato en la frontera.
- Añadir seguridad y límites desde el primer endpoint, no al final.
- Probar el contrato de forma automática, incluidas las respuestas de error.
- Publicar con documentación y ejemplos.
- Medir el uso y evolucionar con cambios compatibles; deprecar con aviso.
Lista de comprobación de una API
- Recursos con sustantivos, nombres y formatos coherentes.
- Verbos y códigos de estado usados con su significado.
- Errores con formato estándar y estable, sin información interna.
- Colecciones paginadas, con límite máximo y orden definido.
- Operaciones no idempotentes protegidas con clave de idempotencia.
- Cabeceras de caché y control de concurrencia donde aporten.
- Autenticación, autorización por objeto y límites de uso.
- Contrato publicado, versionado y probado automáticamente.
- Política de compatibilidad y de deprecación escrita.
- Identificador de correlación y métricas por endpoint.
Antipatrones habituales
- 200 con el error dentro del cuerpo: rompe cachés, monitores y clientes genéricos.
- Verbos en las URL (
/crearPedido) y un único endpoint para todo. - Exponer el modelo de la base de datos tal cual: cualquier cambio interno se convierte en rotura pública.
- Colecciones sin paginar que crecen hasta tumbar el servicio.
- Identidad en el cuerpo: aceptar el identificador de usuario que envía el cliente en lugar del que dice el token.
- Cambiar el contrato sin versionar ni avisar.
- Enumerados y formatos inconsistentes entre endpoints.
- Mensajes de error con trazas, consultas SQL o rutas del servidor.
- API charlatana: obligar a decenas de llamadas para componer una pantalla.
- Secretos en la URL, que acaban en registros y en el historial.