Saltar al contenido principal
Jordi Izquierdo Montalbán
Lo que practico

Fundamentos

APIs e interfaces

Diseño el contrato primero: REST, WebSocket o streaming según el caso.

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

Según quién inicia la conversación y si la respuesta llega de una vez o poco a poco.
EstiloModeloPunto fuertePunto débil
REST sobre HTTPRecursos con URL y verbosUniversal, cacheable, fácil de depurarVarias llamadas para pantallas complejas; sobrecarga o carencia de datos
GraphQLUn endpoint y consultas tipadasEl cliente pide justo lo que necesitaCaché más difícil, coste de consultas a controlar, complejidad en el servidor
gRPCLlamadas binarias con contrato Protocol BuffersRápido, tipado fuerte, streamingPoco amigable desde el navegador, menos legible a mano
WebSocketCanal bidireccional permanenteTiempo real en ambos sentidosEstado por conexión, más difícil de escalar y de cachear
SSEFlujo unidireccional servidor a cliente sobre HTTPSencillo, reconexión automáticaSolo en un sentido, conexiones abiertas
WebhookEl servidor llama a una URL del clienteEvita el sondeoEl receptor debe estar disponible y ser seguro
MensajeríaColas y temas con un brokerDesacoplamiento temporal, absorbe picosConsistencia 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

Las comprobaciones se hacen en orden: lo más barato y lo más crítico, primero.

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

NivelQué añade
0Un solo endpoint y un solo verbo, como un túnel (estilo RPC sobre HTTP)
1Recursos: cada cosa tiene su propia URL
2Verbos HTTP y códigos de estado con su significado
3Hipermedia: 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

VerboUsoSeguroIdempotente
GETLeerSíSí
HEADLeer solo cabecerasSíSí
POSTCrear o ejecutar una acciónNoNo
PUTReemplazar un recurso completoNoSí
PATCHModificar parcialmenteNoDepende de cómo se defina
DELETEEliminarNoSí

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ódigoCuándo
200 OKLectura o actualización correcta con cuerpo
201 CreatedRecurso creado; cabecera Location con su URL
202 AcceptedAceptado para proceso asíncrono
204 No ContentÉxito sin cuerpo (por ejemplo, un borrado)
304 Not ModifiedEl cliente ya tiene la versión vigente
400 Bad RequestPetición mal formada
401 UnauthorizedFalta autenticación válida
403 ForbiddenAutenticado, pero sin permiso
404 Not FoundEl recurso no existe (o no se quiere revelar que existe)
409 ConflictChoca con el estado actual (duplicado, versión obsoleta)
412 Precondition FailedFalló una condición como If-Match
415 Unsupported Media TypeFormato del cuerpo no admitido
422 Unprocessable ContentSintaxis correcta pero reglas de validación incumplidas
429 Too Many RequestsLímite de uso superado
500 Internal Server ErrorFallo inesperado del servidor
503 Service UnavailableTemporalmente 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.

El cursor marca desde dónde seguir, así que no se pierden ni repiten filas.
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=60
GET /productos/7 HTTP/1.1
If-None-Match: "v3-a1b2c3"
 
HTTP/1.1 304 Not Modified
  • ETag identifica la versión; If-None-Match evita retransmitir lo que el cliente ya tiene.
  • Cache-Control define quién puede cachear y durante cuánto (public, private, no-store, max-age).
  • Control de concurrencia optimista: el cliente envía If-Match con 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/88

GraphQL 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

AspectoSSEWebSocket
DirecciónServidor a clienteAmbas
ProtocoloHTTP normalProtocolo propio tras un saludo HTTP
ReconexiónAutomática, con identificador del último eventoLa resuelve la aplicación
Uso típicoProgreso, notificaciones, respuestas en flujoChat, 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.

El emisor llama al receptor; la fiabilidad se construye con firma, reintentos e idempotencia.

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.

El contrato es el centro: de él salen documentación, mocks, validación y clientes.
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 respuestaNo (si los clientes ignoran lo desconocido)
Añadir un endpoint nuevoNo
Añadir un parámetro opcionalNo
Hacer obligatorio un parámetroSí
Quitar o renombrar un campoSí
Cambiar el tipo o el significado de un campoSí
Añadir un valor a un enumeradoPuede romper a clientes con tratamiento estricto
Endurecer una validaciónSí
Cambiar un código de estado o un formato de errorSí

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

EstrategiaEjemploComentario
En la ruta/v2/pedidosExplícita y fácil de enrutar; la más extendida
En una cabeceraCabecera de versión o de fechaURL limpia, más difícil de probar a mano
Por tipo de contenidoTipo de medio con versiónPurista, poco práctico
Evolución sin versionesSolo cambios compatiblesIdeal 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

Cambiar un contrato en cuatro fases: añadir, deprecar, medir, retirar.
  1. Se anuncia la deprecación con fecha de retirada, documentación y guía de migración.
  2. 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"
  1. Se mide quién sigue usando lo antiguo y se les contacta.
  2. Se retira solo cuando el plazo se ha cumplido y el uso es residual.

Autenticación y autorización

MecanismoÚsalo paraObservaciones
API keyIdentificar una aplicaciónNo identifica a un usuario; se envía por cabecera, nunca en la URL; rotable y revocable
OAuth 2.0Delegar acceso limitado a un terceroDefine flujos y alcances (scopes); es autorización, no autenticación
OpenID ConnectSaber quién es el usuarioCapa de identidad sobre OAuth 2.0 que añade un token de identidad
JWTToken firmado y autocontenidoSe valida sin consultar; difícil de revocar antes de caducar, así que vida corta
mTLSServicio a servicioAmbos extremos se autentican con certificados
El cliente nunca ve la contraseña; recibe un token de acceso de vida corta.

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

  1. Entender a los consumidores y los casos de uso, no solo los datos que hay en la base.
  2. Escribir el contrato (OpenAPI, AsyncAPI, proto) y revisarlo con quienes lo van a usar.
  3. Levantar un mock para que cliente y servidor avancen en paralelo.
  4. Implementar validando contra el contrato en la frontera.
  5. Añadir seguridad y límites desde el primer endpoint, no al final.
  6. Probar el contrato de forma automática, incluidas las respuestas de error.
  7. Publicar con documentación y ejemplos.
  8. 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.