La API de Tienda Aroma funciona. Tiene contrato, capas, validación, persistencia, autenticación, errores unificados y pruebas que lo protegen todo. Y sin embargo, si mañana se la entregas a un equipo externo, ocurrirán cosas: preguntarán por qué limite acepta 100 pero expandir no tiene tope; descubrirán que la pantalla de "mis pedidos" de la app móvil necesita cinco llamadas; alguien mandará "rol": "administrador" en el registro para ver qué pasa; y otro se quejará de que datos_invalidos le dice que algo falla pero no qué escribir para arreglarlo. Ninguno de esos problemas es un bug. Todos son decisiones de diseño, y ninguna prueba automatizada los detecta.
Esta lección es distinta de las anteriores: no añade código nuevo al proyecto, añade criterio. En 02-01 vimos los principios de diseño antes de construir nada; ahora que ya sabes construir, los revisitamos desde el otro lado, con la experiencia de haber implementado cada pieza. Al terminar tendrás una lista de revisión aplicable a cualquier API, un catálogo de antipatrones para reconocerlos en el trabajo real, y una estrategia para cuando descubras —porque ocurrirá— que ya te has equivocado.
Contenido
- Correcta frente a excelente
- La consistencia como valor supremo
- Cómo se garantiza la consistencia en la práctica
- Linting de la especificación con Spectral
- Diseñar para el consumidor: la pantalla "mis pedidos"
- Demasiado fina, demasiado gruesa: la granularidad
- Previsibilidad y el principio de mínima sorpresa
- Valores por defecto sensatos y seguros
- El principio de robustez y sus límites
- Idempotencia y reintentos como contrato explícito
- Errores accionables
- Compatibilidad hacia delante por diseño
- Salud, metadatos y raíz descubrible
- Paginación obligatoria y límites por defecto
- Zonas horarias, unidades y localización
- Antipatrones a evitar
- La lista de revisión de diseño de Tienda Aroma
- Deuda de diseño: qué hacer cuando ya te has equivocado
- Correcta frente a excelente
Una API correcta cumple su especificación: los códigos son los que dice el contrato, los datos que entran salen bien, los errores no filtran nada. Es lo que has construido en el módulo 3 y es una condición necesaria.
Una API excelente añade algo que no aparece en ninguna especificación: el coste de usarla es bajo. Se mide en una unidad incómoda de cuantificar pero fácil de reconocer: cuánto tarda un desarrollador que no la conoce en integrar su primer caso de uso completo, y cuántas veces tiene que abrir la documentación después de la primera semana.
| Dimensión | API correcta | API excelente |
|---|---|---|
| Corrección | Hace lo que dice | Hace lo que dice |
| Aprendizaje | Se aprende leyendo la documentación entera | Se adivina; la documentación confirma |
| Consistencia | Cada endpoint es correcto por separado | Todos siguen las mismas reglas |
| Errores | Indican que algo ha fallado | Indican qué hacer a continuación |
| Casos de uso | Cada recurso es accesible | Los flujos reales necesitan pocas llamadas |
| Evolución | Cambiar rompe clientes | Cambiar es rutina |
| Defectos | Se detectan en producción | Se detectan en la revisión de diseño |
La diferencia práctica es económica. Una API interna que consumen tres equipos, con 40 desarrolladores integrándose a lo largo de dos años, multiplica cada pequeña fricción por cientos de horas. Una decisión de nomenclatura que se toma en cinco minutos se paga durante años.
- La consistencia como valor supremo
Si tuvieras que elegir una sola propiedad de diseño y sacrificar todas las demás, elige la consistencia. La razón es cognitiva: un consumidor aprende una API construyendo un modelo mental, y ese modelo es una máquina de extrapolar. Si GET /v1/cafes?limite=20 funciona, asume que GET /v1/pedidos?limite=20 funciona. Si esa extrapolación acierta el 100 % de las veces, deja de leer la documentación y va rápido. Si acierta el 90 %, no puede fiarse de ninguna: tiene que verificar las diez, y va más lento que si la API fuera uniformemente mediocre.
Una API consistentemente imperfecta es más usable que una API inconsistentemente perfecta. Es contraintuitivo y es cierto. Si Tienda Aroma hubiera decidido snake_case en el JSON, sería una decisión peor que camelCase para consumidores JavaScript, pero aplicada en los 24 recursos costaría exactamente un párrafo de documentación. Mezclar los dos convenios cuesta una consulta a la documentación por cada campo, para siempre.
Las dimensiones donde la consistencia se rompe con más facilidad, en orden de frecuencia real:
| Dimensión | Regla de Tienda Aroma | Síntoma de ruptura |
|---|---|---|
| Nomenclatura de campos | camelCase siempre |
precioEuros junto a fecha_creacion |
| Nombres de recursos | Plural, sustantivo, minúscula | /cafes junto a /getPedido |
| Formato de colección | {"datos": [...], "total": n} |
Un endpoint que devuelve un array pelado |
| Formato de error | {"error": {codigo, mensaje, detalles}} |
Un {"mensaje": "..."} suelto |
| Códigos de estado | Árbol de decisión de 02-04 | Un 200 donde debería haber 201 |
| Paginación | limite/desplazamiento, cursor en /pedidos |
page/per_page en un recurso nuevo |
| Fechas | ISO-8601 UTC con Z |
Un 1734567890 epoch en un campo |
| Dinero | Euros con dos decimales fuera | Un campo en céntimos que se escapa |
| Identificadores | string con prefijo caf_, ped_ |
Un entero desnudo en un recurso nuevo |
| Cabeceras propias | Prefijo Aroma- |
Un X-Total-Count heredado de un ejemplo |
Fíjate en el patrón: casi todas las rupturas ocurren al añadir algo nuevo, meses después, cuando quien lo añade no participó en las decisiones originales y copia el estilo de un ejemplo de Internet. La consistencia no es un acto de diseño, es un proceso de mantenimiento.
- Cómo se garantiza la consistencia en la práctica
Tres mecanismos, de menos a más automático. Los tres son necesarios; ninguno sustituye a los otros.
La guía de estilo viva
En 02-01 escribimos la guía de estilo de Tienda Aroma. El adjetivo importante es viva: un documento que se escribe una vez y se archiva no sirve para nada. Una guía viva tiene tres propiedades:
- Vive en el repositorio, no en un wiki corporativo. Se versiona con el código, se revisa por pull request y se puede enlazar a un commit concreto.
- Cada regla es normativa y comprobable. "Usa nombres claros" no es una regla, es un deseo. "Los nombres de recurso son sustantivos en plural, en minúsculas, sin guiones bajos" sí lo es: dos personas la aplican igual.
- Registra las decisiones con su motivo. Cuando dentro de un año alguien pregunte por qué el dinero viaja en euros con dos decimales y no en céntimos, la respuesta debe estar escrita. Si no, la decisión se revierte por desconocimiento.
Un formato práctico es el ADR (Architecture Decision Record): un fichero corto por decisión, con contexto, decisión y consecuencias.
<!-- docs/decisiones/0007-dinero-en-euros-con-dos-decimales.md -->
# 0007. El dinero viaja en euros con dos decimales
- Estado: aceptado
- Fecha: 2026-03-14
## Contexto
Internamente almacenamos `precio_centimos` como entero para evitar los errores
de coma flotante. Hacia fuera había dos opciones: exponer céntimos enteros
(`1450`) o euros con dos decimales (`14.50`).
## Decisión
Se expone `precioEuros: 14.50`. La conversión vive en `src/servicios/mapeadores.js`
y en ningún otro sitio.
## Consecuencias
- (+) La SPA y la app móvil muestran el valor sin conversión ni riesgo de dividir mal.
- (+) La documentación es autoexplicativa: nadie confunde 1450 con 1450 €.
- (-) Un consumidor descuidado puede sumar en coma flotante y acumular error.
Se mitiga documentándolo y devolviendo `totalEuros` ya calculado por el servidor.
- El campo se llama `precioEuros`, con la unidad en el nombre, precisamente por (-).Este fichero, de veinte líneas, ahorra una discusión de una hora cada vez que entra alguien nuevo.
La revisión de diseño
Es una revisión que ocurre antes de escribir código, sobre la especificación, no sobre la implementación. Su objetivo es que ningún endpoint público nazca sin que al menos otra persona haya mirado su forma. La conversación es corta si el diseño es bueno y larga si no lo es, que es exactamente lo que quieres: el momento barato de cambiar /v1/pedidos/{id}/cancelar por /v1/pedidos/{id}/anulacion es cuando solo existe en un YAML.
Un guion de quince minutos que funciona:
- ¿Qué caso de uso real habilita este endpoint? (Si no hay respuesta concreta, no se construye.)
- ¿Es un recurso o es un verbo disfrazado?
- ¿Los nombres siguen la guía de estilo? ¿Se parecen a los que ya existen?
- ¿Qué códigos de estado devuelve y cuáles faltan?
- ¿Qué pasa si se llama dos veces? ¿Es idempotente? ¿Debe serlo?
- ¿Quién puede llamarlo? ¿Qué ve un cliente que no es el propietario?
- ¿Se puede añadir un campo dentro de seis meses sin romper a nadie?
- ¿Está paginado si devuelve una colección?
El linting automático
Lo que se puede comprobar con una máquina no debe consumir tiempo humano en la revisión. Ahí entra Spectral.
- Linting de la especificación con Spectral
Spectral es un linter para ficheros OpenAPI y AsyncAPI. Se ejecuta sobre openapi.yaml —el que empezamos en 02-08— y aplica reglas escritas por ti. Convierte la guía de estilo, que es prosa, en comprobaciones que fallan en la integración continua.
# Instalación como dependencia de desarrollo del proyecto
npm install --save-dev @stoplight/spectral-cli
# Ejecución sobre el contrato
npx spectral lint openapi.yamlEl fichero de reglas se llama .spectral.yaml y vive en la raíz del proyecto:
# .spectral.yaml — reglas de estilo de la API de Tienda Aroma
extends: ["spectral:oas"] # hereda las reglas base de OpenAPI (estructura válida)
rules:
# --- Reglas heredadas que ajustamos ---
operation-tag-defined: error # toda operación debe tener una etiqueta declarada
info-contact: error # el contrato debe decir a quién escribir
# --- Reglas propias de Tienda Aroma ---
aroma-rutas-en-minuscula-y-plural:
description: Las rutas usan sustantivos en plural y minúsculas, sin guiones bajos ni camelCase.
message: "{{property}} no cumple el convenio de rutas de Tienda Aroma."
severity: error
given: $.paths[*]~ # el ~ selecciona la CLAVE (la ruta), no su valor
then:
function: pattern
functionOptions:
match: "^(/[a-z0-9-]+|/\\{[a-zA-Z]+\\})+$"
aroma-sin-verbos-en-la-uri:
description: Las URIs no contienen verbos; la acción la expresa el método HTTP.
message: "La ruta {{property}} contiene un verbo: usa un sustantivo o un subrecurso."
severity: error
given: $.paths[*]~
then:
function: pattern
functionOptions:
notMatch: "(crear|obtener|listar|borrar|actualizar|get|create|delete|update|buscar)"
aroma-propiedades-en-camelcase:
description: Todas las propiedades de los esquemas van en camelCase.
severity: error
given: $.components.schemas[*].properties[*]~
then:
function: casing
functionOptions:
type: camel
aroma-colecciones-paginadas:
description: Toda operación GET que devuelve una colección declara limite y desplazamiento.
severity: warn
given: $.paths[*].get
then:
field: parameters
function: schema
functionOptions:
schema:
type: array
contains:
type: object
properties:
name: { const: limite }
aroma-toda-operacion-declara-401:
description: Las operaciones bajo /v1 deben documentar la respuesta 401.
severity: warn
given: $.paths[?(@property.match(/^\/(cafes|pedidos|clientes|resenas|carritos)/))][get,post,put,patch,delete]
then:
field: responses.401
function: truthy
aroma-cabeceras-propias-con-prefijo:
description: Las cabeceras propias llevan el prefijo Aroma-, nunca X-.
severity: error
given: $.paths[*][*].responses[*].headers[*]~
then:
function: pattern
functionOptions:
notMatch: "^[Xx]-"Repasemos las piezas menos evidentes:
extends: ["spectral:oas"]carga el conjunto de reglas oficial que verifica que el documento es un OpenAPI estructuralmente válido. Tus reglas se suman a esas.givenes una expresión JSONPath que selecciona los nodos a comprobar. El sufijo~es específico de Spectral y significa "aplica la regla a la clave del nodo, no a su valor": por eso$.paths[*]~selecciona/cafes/{id}como texto.then.functiones la comprobación.patternaceptamatch(debe cumplir) ynotMatch(no debe cumplir);casingverifica convenios de nombres;truthyexige que el campo exista y no esté vacío.severitydecide si el fallo rompe la construcción (error) o solo avisa (warn). Una regla nueva se introduce siempre comowarn, se limpian las infracciones existentes y solo entonces se sube aerror; si no, nadie puede hacer merge el día que la añades.
En la integración continua (que veremos en 05-05) esto es un paso más:
El efecto cultural es mayor que el técnico: la discusión sobre el estilo deja de ocurrir en cada pull request y pasa a ocurrir una vez, cuando se propone la regla.
- Diseñar para el consumidor: la pantalla "mis pedidos"
Aquí está el error más común del diseño de APIs, y no es un error de nomenclatura: diseñar desde el modelo de datos en lugar de desde el caso de uso. Los recursos de Tienda Aroma son un reflejo casi exacto de las tablas de SQLite, lo cual es cómodo para nosotros y a veces terrible para quien consume.
Veámoslo con un caso concreto. La app Aroma Móvil tiene una pantalla "Mis pedidos" que muestra, por cada uno de los últimos diez pedidos del cliente: fecha, estado, total, y una miniatura con el nombre del primer café de la lista.
Con la API tal y como está al terminar el módulo 3, el cliente móvil hace esto:
GET /v1/clientes/cli_842/pedidos?limite=10&ordenar=-fechaCreacion
GET /v1/cafes/caf_001
GET /v1/cafes/caf_002
GET /v1/cafes/caf_007
... (una por cada café distinto que aparezca en las líneas)Once peticiones para pintar una pantalla. En una red móvil con 150 ms de latencia por petición, si el cliente las encadena son 1,6 segundos solo de ida y vuelta. Y esto es el problema N+1, el mismo que en 03-05 atacamos dentro de la base de datos, pero ahora ocurre por encima de HTTP, donde cada salto cuesta mil veces más.
La solución no es inventar GET /v1/pantalla-mis-pedidos. Es usar el mecanismo que ya diseñamos en 02-05:
GET /v1/clientes/cli_842/pedidos?limite=10&ordenar=-fechaCreacion&expandir=lineas.cafe&campos=id,fechaCreacion,estado,totalEuros,lineasUna petición. La respuesta trae anidado lo justo:
{
"datos": [
{
"id": "ped_5001",
"fechaCreacion": "2026-08-02T09:14:22Z",
"estado": "enviado",
"totalEuros": 41.90,
"lineas": [
{
"cafeId": "caf_001",
"cantidad": 2,
"cafe": { "id": "caf_001", "nombre": "Etiopía Yirgacheffe", "tueste": "claro" }
}
],
"_links": {
"self": { "href": "/v1/pedidos/ped_5001" },
"devolver": { "href": "/v1/pedidos/ped_5001/devolucion", "method": "POST" }
}
}
],
"total": 7
}El principio general: el número de llamadas necesarias para un caso de uso real es una métrica de diseño de primer orden. Cuando diseñes un recurso, escribe al lado los dos o tres flujos que lo van a usar y cuenta las llamadas. Si un flujo frecuente necesita más de dos o tres, falta un mecanismo.
Los mecanismos disponibles, por orden de preferencia:
| Mecanismo | Cuándo | Coste |
|---|---|---|
expandir sobre relaciones |
El dato extra está a un salto | Bajo; ya implementado |
campos para adelgazar |
La respuesta es grande y el cliente usa poco | Bajo |
Subrecurso de colección (/clientes/{id}/pedidos) |
La relación es la consulta natural | Bajo |
| Recurso agregado nuevo | Un flujo crítico y muy frecuente lo justifica | Alto: recurso que mantener para siempre |
| GraphQL en paralelo | Muchos clientes con necesidades muy dispares | Muy alto (ver 01-07) |
- Demasiado fina, demasiado gruesa: la granularidad
El apartado anterior empuja hacia respuestas más gordas. Hay un límite, y pasarse tiene su propio castigo.
| API demasiado fina | API demasiado gruesa | |
|---|---|---|
| Síntoma | 11 llamadas para una pantalla | Una llamada que devuelve 400 KB |
| Coste | Latencia acumulada, batería, complejidad en el cliente | Ancho de banda, memoria, consultas SQL innecesarias |
| Caché | Cada trozo se cachea bien por separado | Todo se invalida cuando cambia cualquier parte |
| Ejemplo malo | GET /v1/pedidos/{id}/total como recurso aparte |
GET /v1/pedidos/{id}?expandir=cliente.pedidos.lineas.cafe.resenas |
| Permisos | Fáciles de acotar por recurso | Un solo endpoint mezcla datos con permisos distintos |
El equilibrio de Tienda Aroma es explícito y merece la pena enunciarlo como regla:
El recurso por defecto es fino; el consumidor lo engorda a demanda con
expandir, y el servidor limita hasta dónde.
Ese "el servidor limita" no es opcional: expandir sin tope es un vector de denegación de servicio, porque el consumidor decide cuánto trabajo hace tu base de datos. La regla concreta que aplica Tienda Aroma es profundidad máxima 2 y una lista blanca de rutas expandibles; el detalle de por qué eso es una defensa de disponibilidad y no solo de rendimiento lo veremos en 04-04.
- Previsibilidad y el principio de mínima sorpresa
El principio de mínima sorpresa dice que, ante dos diseños válidos, elijas el que el consumidor habría adivinado. Aplicado a una API, se traduce en una prueba muy concreta que puedes hacer sin herramientas: enseña la lista de endpoints a alguien que no conozca el sistema y pídele que prediga la respuesta de tres de ellos. Lo que no acierte es una sorpresa, y toda sorpresa es una consulta a la documentación repetida por cada consumidor durante toda la vida de la API.
Los ejes donde se juega la previsibilidad:
| Eje | Previsible | Sorprendente |
|---|---|---|
| Nombre del recurso | /v1/pedidos |
/v1/orden-compra-v2 |
| Nombre del campo | precioEuros (unidad en el nombre) |
precio (¿euros? ¿céntimos?) |
| Booleanos | disponible |
noDisponible, sinStock (doble negación) |
| Enumerados | pendiente_pago | pagado | enviado |
0 | 1 | 2 |
| Colección vacía | {"datos": [], "total": 0} con 200 |
404, o null, o {} |
| Campo ausente | Se omite, o null — pero siempre igual |
Unas veces null, otras ausente, otras "" |
| Borrado repetido | 204 la primera vez, 404 después |
500 |
Orden sin ordenar |
Estable y documentado (por id) |
El que decida SQLite ese día |
Dos reglas prácticas que resuelven la mayoría de los casos:
- Los nombres se eligen en el dominio del consumidor, no en el de la base de datos. La tabla puede llamarse
t_ord_hdr; el recurso se llamapedidos. - La misma pregunta se responde siempre en el mismo sitio. Si el total de una colección está en
total, está entotalen las once colecciones, no en una cabecera en algunas y en el cuerpo en otras.
- Valores por defecto sensatos y seguros
Todo parámetro opcional tiene un valor por defecto, lo declares o no. Si no lo declaras, el valor por defecto es el que resulte de tu implementación, y eso es una decisión de diseño tomada por accidente.
Un buen valor por defecto cumple dos condiciones a la vez:
- Sensato: es lo que quiere el 80 % de los consumidores, para que no tengan que escribirlo.
- Seguro: si el consumidor no sabe lo que hace, el daño está acotado. Ante la duda, el defecto es el conservador.
Los de Tienda Aroma, ya implementados, con su justificación:
| Parámetro | Defecto | Sensato porque | Seguro porque |
|---|---|---|---|
limite |
20 | Cabe en una pantalla | Sin él, GET /v1/cafes volcaría la tabla entera |
limite máximo |
100 | Suficiente para un lote | Acota el trabajo por petición |
desplazamiento máximo |
10 000 | Nadie pagina en serio más allá | Evita OFFSET gigantes que barren la tabla |
ordenar |
id ascendente |
Orden estable y reproducible | Sin orden explícito la paginación duplica y salta filas |
campos |
Todos los públicos | Lo esperable | La lista blanca del mapeador impide filtrar internos |
expandir |
Ninguno | La respuesta base es barata | El coste extra es siempre una elección explícita |
Accept-Language |
es |
Idioma principal de la tienda | Determinista |
| Visibilidad de un recurso nuevo | Privado | — | Se publica al añadirlo al contrato, no al desplegarlo |
La cuarta fila merece un comentario, porque es un error clásico que ya evitamos en 03-05 casi sin darnos cuenta: la paginación sin orden explícito no es determinista. Si el motor devuelve las filas en el orden que le conviene, el desplazamiento=20 puede repetir filas que ya viste en el 0 y saltarse otras. Por eso el desempate por id no es un detalle estético, es corrección.
- El principio de robustez y sus límites
El principio de robustez (o ley de Postel) dice: sé conservador en lo que envías, liberal en lo que aceptas. Nació con TCP y se ha aplicado durante décadas al diseño de protocolos. Hoy se acepta con matices importantes.
La primera mitad es incondicionalmente buena. Ser conservador en lo que envías significa: fechas siempre en el mismo formato, campos siempre del mismo tipo, colecciones siempre con la misma envoltura, errores siempre con la misma forma. Nunca hay razón para relajarla.
La segunda mitad es peligrosa. Aceptar liberalmente lo que llega parece amable, pero tiene un coste diferido brutal:
- Si aceptas
precioEuros: "14.50"(cadena) además del número, ese comportamiento se convierte en contrato de facto en cuanto un consumidor lo use. Ya no puedes quitarlo. - Si ignoras silenciosamente los campos que no conoces, un consumidor que escriba
precioEuro(sin s) creerá que ha actualizado el precio y no lo habrá hecho. El fallo se manifiesta en otro sitio, días después. - Cada tolerancia es una rama del código que hay que probar y mantener para siempre.
Por eso Tienda Aroma es estricta en la entrada: los esquemas de Zod llevan .strict(), un campo desconocido produce 400 datos_invalidos en lugar de ignorarse, y los tipos no se coaccionan. Es menos amable en el primer minuto de integración y mucho más amable en los dos años siguientes.
Dónde sí conviene ser tolerante, con criterio:
| Situación | Tolerar | Motivo |
|---|---|---|
| Espacios alrededor de un texto | Sí, con .trim() |
Error humano trivial, sin ambigüedad |
| Mayúsculas en un correo | Sí, normalizando a minúsculas | El correo no distingue caja en el dominio |
?tueste=Claro frente a claro |
No | Enseña un convenio y luego lo contradice |
| Campo desconocido en el cuerpo | No, nunca | Silencia errores y habilita mass assignment (04-02) |
| Fecha en otro formato | No | La ambigüedad 03/04 es irresoluble |
| Campo desconocido en la respuesta que recibe un cliente | Sí, siempre | Es el tolerant reader: ver apartado 12 |
La asimetría de la última fila es la clave y suele confundirse: estricto al recibir peticiones, tolerante al leer respuestas de otros. Son papeles distintos.
- Idempotencia y reintentos como contrato explícito
En 02-03 estudiamos la idempotencia como propiedad de los métodos HTTP y en 03-03 la implementamos con Idempotency-Key. Lo que falta es la parte de diseño: la idempotencia no es una característica técnica que se activa, es una promesa documentada sin la cual el consumidor no puede reintentar con seguridad.
El razonamiento del consumidor ante un timeout es siempre el mismo, y es un dilema real:
graph TD
A[POST /v1/pedidos] --> B{Llega respuesta?}
B -->|Si, 201| C[Pedido creado. Fin]
B -->|Timeout / red caida| D{Se creo el pedido?}
D -->|No lo se| E{La API promete idempotencia?}
E -->|Si, documentada| F[Reintento con la misma Idempotency-Key]
F --> G[Misma respuesta 201, un solo pedido]
E -->|No lo dice| H[No reintentar y arriesgar perder el pedido]
E -->|No lo dice| I[Reintentar y arriesgar cobrar dos veces]
Sin la promesa escrita, el consumidor elige entre dos malas opciones. Con ella, el caso deja de ser un problema.
Lo que hay que documentar, endpoint por endpoint, es una tabla como esta —que además es exactamente lo que un consumidor busca cuando algo falla en producción:
| Operación | ¿Idempotente? | Mecanismo | Qué hacer ante un timeout |
|---|---|---|---|
GET (cualquiera) |
Sí, por definición | — | Reintentar libremente |
PUT /v1/cafes/{id} |
Sí, por definición | — | Reintentar; el estado final es el mismo |
DELETE /v1/resenas/{id} |
Sí | Segunda llamada → 404 |
Reintentar; 404 significa "ya no está" |
POST /v1/pedidos |
Sí, con clave | Idempotency-Key obligatoria, 24 h |
Reintentar con la misma clave |
POST /v1/pedidos/{id}/pago |
Sí, con clave | Idempotency-Key obligatoria, 24 h |
Reintentar con la misma clave |
POST /v1/cafes/{id}/resenas |
No | — | Consultar antes de reintentar |
PATCH con merge-patch+json |
Depende del cuerpo | — | Reintentar solo si el parche es absoluto |
La fila del PATCH es la más sutil: {"stock": 100} es idempotente porque fija un valor absoluto; un hipotético {"stockIncremento": 10} no lo sería. Es una razón más para preferir parches absolutos.
Y hay un detalle de diseño que se olvida siempre: qué pasa si se reutiliza la clave con un cuerpo distinto. Tienda Aroma responde 409 clave_idempotencia_reutilizada, y eso es lo correcto, porque casi siempre indica un bug del cliente (una clave generada una vez por sesión en lugar de una por operación) y silenciarlo lo haría indetectable.
- Errores accionables
La pregunta que hay que hacerse ante cada mensaje de error es una sola: ¿qué hace el desarrollador que lo lee, inmediatamente después de leerlo? Si la respuesta es "abrir la documentación", "preguntar en el chat de soporte" o "probar cosas", el error no es accionable.
{
"error": {
"codigo": "datos_invalidos",
"mensaje": "La petición contiene datos no válidos.",
"detalles": [
{ "campo": "tueste", "mensaje": "Debe ser uno de: claro, medio, oscuro. Se recibió: 'tostado'." },
{ "campo": "precioEuros", "mensaje": "Debe ser un número mayor que 0 con dos decimales como máximo. Se recibió: -3." },
{ "campo": "notasCatas", "mensaje": "Campo no reconocido. ¿Querías decir 'notasCata'?" }
]
}
}Las cuatro propiedades de un detalle accionable:
| Propiedad | En el ejemplo | Sin ella |
|---|---|---|
| Señala dónde | "campo": "tueste" |
El desarrollador busca a ojo entre 12 campos |
| Dice qué se esperaba | "uno de: claro, medio, oscuro" | Tiene que abrir la documentación |
| Dice qué se recibió | "Se recibió: 'tostado'" | No sabe si el problema es su código o su dato |
| Sugiere la corrección | "¿Querías decir 'notasCata'?" | Pierde diez minutos con una errata |
Y las tres reglas complementarias, todas ya implementadas:
- Todos los fallos a la vez, no el primero. Un consumidor que corrige de uno en uno hace seis viajes para arreglar seis erratas.
- Código estable y legible por máquina (
stock_insuficiente), separado del mensaje legible por humanos. El código es contrato; el mensaje se puede reescribir o traducir. - Nunca filtrar el interior al construir el mensaje: ni SQL, ni rutas de fichero, ni el nombre de la columna. Eso lo cerramos en 03-07 y es tan cierto aquí.
Un último matiz de diseño: los mensajes de error de una API se escriben para desarrolladores, no para usuarios finales. "Se requiere el campo clienteId" es correcto en la API; "Por favor, indica a quién enviamos el pedido" es trabajo de la SPA. Confundir las dos audiencias produce mensajes inútiles para ambas.
- Compatibilidad hacia delante por diseño
En 02-07 vimos el versionado como estrategia. Aquí va la otra mitad: cuanto mejor diseñes, menos veces necesitarás una versión nueva. Una /v2 es un fracaso caro; el objetivo es que /v1 viva años.
Tres técnicas que se aplican en el momento del diseño, no después.
Campos opcionales desde el principio. Añadir un campo opcional a una respuesta es compatible; añadirlo obligatorio a una petición no lo es. Por eso, cuando dudes entre exigir un campo o darle un valor por defecto, el defecto es más barato de mantener.
Enumerados extensibles. estado hoy vale pendiente_pago | pagado | enviado. Mañana habrá devuelto y anulado. Si el consumidor escribió un switch sin rama por defecto, tu añadido rompe su aplicación. Por eso el contrato debe decir explícitamente, con estas palabras: «el conjunto de valores de este enumerado puede crecer; los clientes deben tratar los valores desconocidos sin fallar». Y la documentación debe mostrar cómo:
// Cliente TOLERANTE: los valores nuevos no rompen la pantalla.
const ETIQUETAS = {
pendiente_pago: 'Pendiente de pago',
pagado: 'Pagado',
enviado: 'Enviado',
};
function etiquetaDeEstado(estado) {
// Si el servidor añade 'devuelto', mostramos algo razonable en vez de romper.
return ETIQUETAS[estado] ?? 'Estado desconocido';
}Tolerant reader. Es el patrón que convierte al consumidor en resistente al cambio. Un lector tolerante:
- lee solo los campos que necesita e ignora los que no conoce (nunca falla porque llegue un campo nuevo);
- no depende del orden de las claves de un objeto ni de los elementos de un array salvo que el contrato lo garantice;
- no valida la respuesta contra un esquema cerrado que rechace propiedades adicionales;
- no reconstruye las URLs: sigue los
_linksque le da el servidor (ahí HATEOAS deja de ser teoría, como vimos en 01-05).
// Lector TOLERANTE de una respuesta de Tienda Aroma.
function leerCafe(json) {
return {
id: json.id,
nombre: json.nombre,
precio: json.precioEuros,
// Si mañana llegan 'altitudMetros' o 'variedad', simplemente no se leen.
};
}
// Lector FRÁGIL: rompe el día que la API añade un campo. No lo hagas.
function leerCafeFragil(json) {
const claves = Object.keys(json);
if (claves.length !== 8) throw new Error('respuesta inesperada'); // ← bomba de relojería
return json;
}La consecuencia para ti como diseñador de la API es doble: documenta que el cliente debe ser tolerante y, sobre todo, no publiques un esquema con additionalProperties: false en las respuestas, porque estarías prometiendo que nunca añadirás un campo. En las peticiones, al contrario, esa restricción es exactamente lo que quieres.
- Salud, metadatos y raíz descubrible
Dos endpoints que no forman parte del dominio y que casi siempre se olvidan hasta que hacen falta.
GET /salud ya existe en src/app.js, deliberadamente fuera de /v1: no es parte del contrato de negocio, es infraestructura, y no debe versionarse con él. Su papel completo (liveness frente a readiness, y qué debe y qué no debe comprobar) lo desarrollamos en 04-07, porque pertenece a la observabilidad.
GET /v1, la raíz descubrible, es el punto de entrada que permite a un cliente empezar sin más conocimiento que una URL:
{
"nombre": "API de Tienda Aroma",
"version": "1.0.0",
"documentacion": "https://api.tiendaaroma.example/docs",
"_links": {
"self": { "href": "/v1" },
"cafes": { "href": "/v1/cafes" },
"pedidos": { "href": "/v1/pedidos" },
"clientes": { "href": "/v1/clientes" },
"resenas": { "href": "/v1/resenas" },
"carritos": { "href": "/v1/carritos" },
"sesiones": { "href": "/v1/sesiones", "method": "POST" }
}
}Es coherente con el nivel 3 de Richardson (01-05) y con los _links que ya devuelven todos los recursos. Cuesta veinte líneas y da tres cosas: un sitio al que apuntar en la documentación, un punto de comprobación trivial para un consumidor nuevo, y un lugar natural donde anunciar la versión y el enlace a la documentación.
- Paginación obligatoria y límites por defecto
Vale la pena enunciarlo como regla absoluta porque las excepciones envejecen mal:
Ninguna colección se devuelve sin paginar. Nunca. Ni siquiera las que hoy tienen cuatro elementos.
El argumento es de crecimiento: cuando /v1/cafes tenía 12 registros, devolverlos todos parecía razonable. Con 4.000 referencias y treinta consumidores móviles, esa decisión es una caída del servicio. Y no puedes añadir la paginación después sin romper a los clientes que asumían recibirlo todo: el cambio de [...] a {"datos": [...], "total": n} es incompatible, y limitar a 20 lo que antes venía completo es peor, porque no rompe visiblemente sino que hace que los consumidores empiecen a perder datos en silencio.
De ahí que la envoltura {"datos": [...], "total": n} esté desde el primer día en las once colecciones de Tienda Aroma, incluso en las que devuelven tres elementos. El coste de tenerla es cero; el coste de añadirla tarde es una versión nueva.
- Zonas horarias, unidades y localización
Tres fuentes de bugs sutiles que se deciden una vez y se aplican en toda la API.
Fechas. ISO-8601, siempre en UTC, siempre con la Z explícita: "2026-08-02T09:14:22Z".
| Formato | Problema |
|---|---|
1754126062 (epoch) |
Ilegible; ambigüedad segundos/milisegundos |
02/08/2026 |
¿2 de agosto o 8 de febrero? |
2026-08-02T09:14:22 |
Sin zona: se interpreta distinto en cada cliente |
2026-08-02T11:14:22+02:00 |
Válido, pero mezcla dos cosas y complica comparar |
2026-08-02T09:14:22Z |
Sin ambigüedad, ordenable como texto |
La conversión a la zona del usuario es responsabilidad del cliente, que es el único que sabe dónde está. Ojo con una excepción real: una fecha civil sin hora (un cumpleaños, la fecha de caducidad de un lote) es "2026-08-02" a secas, no un instante; convertirla a UTC la desplaza un día en media Europa.
Unidades en el nombre. Es la práctica más barata y rentable de esta lección: precioEuros, pesoGramos, duracionSegundos, altitudMetros. Un campo peso obliga a mirar la documentación cada vez; pesoGramos no. Y el nombre viaja con el dato: aparece en los logs, en los volcados y en el código del cliente.
Dinero. La regla de Tienda Aroma: céntimos enteros por dentro (precio_centimos), euros con dos decimales por fuera (precioEuros). Nunca coma flotante en la base de datos ni en los cálculos, porque 0.1 + 0.2 !== 0.3. Y si algún día la tienda vende fuera de la zona euro, harán falta moneda: "EUR" junto al importe y, mejor aún, un objeto {"cantidad": 14.50, "moneda": "EUR"}; diseñarlo ahora cuesta poco y evita una migración incompatible.
Localización. El idioma del contenido se negocia con Accept-Language (02-05) y se declara con Vary: Accept-Language para que las cachés no mezclen idiomas —algo que se vuelve crítico en 04-06—. Lo que no se traduce nunca es el contrato: los nombres de campo, los valores de los enumerados y los códigos de error son identificadores, no texto para humanos. estado: "enviado" es un símbolo estable; la palabra "Enviado" que ve el usuario la pone la SPA.
- Antipatrones a evitar
| Antipatrón | Ejemplo | Por qué es malo | Alternativa |
|---|---|---|---|
| Verbos en la URI | POST /v1/crearPedido, GET /v1/pedidos/obtenerTodos |
Duplica lo que ya dice el método; multiplica endpoints; rompe la caché y los proxies | POST /v1/pedidos, GET /v1/pedidos |
200 con exito: false |
200 OK + {"exito": false, "error": "sin stock"} |
Los clientes HTTP, proxies, cachés y monitorizaciones creen que todo va bien; obliga a inspeccionar el cuerpo siempre | 409 + {"error": {"codigo": "stock_insuficiente"}} |
| Exponer el esquema de la BD | {"t_ord_id": 5001, "fk_cli": 842, "flg_del": 0} |
Ata el contrato a la tabla: no puedes refactorizar; filtra información interna | Mapeador explícito con lista blanca (03-03) |
| Endpoint "todo en uno" | POST /v1/api con {"accion": "crear_pedido", ...} |
Es RPC sobre HTTP: un solo código de estado, sin caché, sin permisos por recurso | Recursos y métodos HTTP |
| Parámetros mágicos | ?modo=2, ?tipo=A, ?flags=15 |
Nadie recuerda qué significa 2; imposible de leer en un log | ?estado=pagado, ?incluirAnulados=true |
| Respuesta que cambia de forma | datos es un objeto si hay uno y un array si hay varios |
El cliente necesita un if en cada consumo; rompe el tipado |
Siempre array en colecciones, aunque tenga un elemento |
| Filtrar identificadores internos | Devolver id autoincremental, hashContrasena, activo, version |
Permite enumerar recursos ajenos y filtra datos sensibles | Ids opacos con prefijo; lista blanca en el mapeador |
| Anidamiento profundo | /v1/clientes/842/pedidos/5001/lineas/3/cafe/resenas/101 |
URLs impredecibles; el mismo recurso accesible por N rutas | Máximo un nivel; el resto por id: /v1/resenas/res_101 |
GET que modifica |
GET /v1/pedidos/ped_5001/anular |
Un rastreador o un prefetch del navegador anula pedidos | POST /v1/pedidos/ped_5001/anulacion |
| Colección sin paginar | GET /v1/pedidos devuelve los 400.000 |
Caída garantizada; imposible de arreglar sin romper | Paginación desde el día uno |
| Números como enumerados | "estado": 2 |
Ilegible; el 2 acaba significando otra cosa | "estado": "pagado" |
| Nulos con significado | precioEuros: -1 para "no disponible" |
Un cliente descuidado suma −1 al carrito | disponible: false |
La fila del GET que modifica no es teórica: es uno de los incidentes más repetidos de la historia de la web. Un rastreador que sigue enlaces, o el prefetch de un navegador, ejecuta acciones destructivas porque alguien decidió que un enlace era más cómodo que un formulario. La safety del GET que vimos en 02-03 es una promesa que hacen los intermediarios de toda la red, no una recomendación.
- La lista de revisión de diseño de Tienda Aroma
Esta lista se aplica a cada endpoint nuevo antes de escribir su implementación. Es accionable: cada línea se responde sí o no.
Recurso y URI
- [ ] El nombre es un sustantivo en plural, minúsculas, sin verbos.
- [ ] La ruta tiene como máximo un nivel de anidamiento.
- [ ] El identificador es opaco y con prefijo (
caf_,ped_,cli_). - [ ] La URI es estable: no contiene nada que vaya a cambiar (estado, categoría, año).
Métodos y semántica
- [ ] El método coincide con la semántica:
GETes seguro,PUT/DELETEidempotentes. - [ ] Si es
POSTy no es idempotente por naturaleza, se ha decidido si exigeIdempotency-Key. - [ ] Hay
405con cabeceraAllowpara los métodos no soportados de esa ruta.
Peticiones
- [ ] El cuerpo tiene esquema Zod con
.strict(); los campos desconocidos dan400. - [ ] Cada parámetro de query está en la lista blanca; uno desconocido da
400 parametro_invalido. - [ ] Los opcionales tienen defecto documentado, sensato y seguro.
- [ ] El tamaño del cuerpo está acotado (100 kB globales).
Respuestas
- [ ] El código de estado sale del árbol de decisión de 02-04.
- [ ] Si es una colección: envoltura
{"datos", "total"}, paginada, conLink. - [ ] Los campos son
camelCase, con unidad en el nombre cuando aplique. - [ ] Las fechas son ISO-8601 UTC con
Z. - [ ] Pasa por el mapeador: ninguna columna interna llega al JSON.
- [ ]
_links.selfsiempre; enlaces de acción solo si la acción es posible ahora. - [ ] Si crea un recurso:
201conLocation.
Errores
- [ ] Todos los códigos usados existen en el catálogo, o se ha decidido ampliarlo y documentarlo.
- [ ]
datos_invalidosdevuelve todos los fallos, con campo, esperado y recibido. - [ ] Ningún mensaje filtra SQL, rutas de fichero, versiones ni la existencia de recursos ajenos.
Seguridad y permisos
- [ ] Está decidido qué roles pueden llamarlo (
cliente,empleado,administrador,socio). - [ ] Un cliente no puede acceder a datos de otro ni distinguir "no existe" de "no es tuyo".
- [ ] Ningún campo sensible entra por asignación masiva (
rol,activo,saldo).
Evolución
- [ ] Se puede añadir un campo a la respuesta sin romper a nadie.
- [ ] Los enumerados están documentados como extensibles.
- [ ] Está en
openapi.yamlynpx spectral lintpasa sin errores.
Pruebas
- [ ] Hay prueba de integración del camino feliz y de al menos dos errores.
- [ ] Hay prueba de permisos: el acceso ajeno se rechaza.
- Deuda de diseño: qué hacer cuando ya te has equivocado
Vas a equivocarte. La pregunta útil no es cómo evitarlo, sino qué hacer después. El primer paso es clasificar el error, porque el tratamiento depende del tipo:
| Tipo de error | Ejemplo en Tienda Aroma | Coste de arreglarlo | Tratamiento |
|---|---|---|---|
| Cosmético, sin consumidores | Un campo mal nombrado en un endpoint que aún no usa nadie | Nulo | Arréglalo hoy |
| Aditivo | Falta expandir en un recurso |
Bajo | Añádelo; es compatible |
| Ampliación de tolerancia | limite máximo de 100 a 200 |
Bajo | Amplía; nadie se rompe |
| Cambio de forma | total pasa de cabecera a cuerpo |
Alto | Convivencia temporal y Deprecation |
| Cambio de semántica | estado: "pagado" pasa a significar otra cosa |
Muy alto | Campo nuevo; el viejo se congela |
| Error estructural | El recurso equivocado, RPC disfrazado | Máximo | /v2 para ese recurso, o rediseño con doble escritura |
Las cinco reglas que hacen manejable la deuda de diseño:
- Reconócela por escrito. Un fichero
docs/deuda-de-diseno.mdcon "sabemos quePOST /v1/carritos/{id}/lineas/{cafeId}debería serPUTy por qué no lo cambiamos" evita que cada persona nueva reabra la discusión y, sobre todo, evita que el error se copie en el siguiente recurso. - Deja de sangrar. Lo primero no es arreglar lo viejo, es que lo nuevo no repita el error. Una regla de Spectral impide que el patrón se propague aunque no puedas limpiar el pasado.
- Convive antes de romper. El campo nuevo y el viejo se devuelven a la vez; el viejo se marca
deprecateden OpenAPI y con las cabecerasDeprecationySunsetde 02-07. - Mide antes de retirar. Si no sabes cuántos consumidores usan el campo viejo, no puedes retirarlo. Instrumentarlo es una necesidad de diseño, no solo de operación; en 04-07 verás cómo se cuenta.
- Agrupa los cambios incompatibles. Si tienes que romper, rompe una vez: acumula los cambios incompatibles y sácalos juntos en
/v2. Tres versiones en un año destruyen la confianza más que un error de diseño.
Errores Comunes y Consejos
Confundir consistencia con rigidez. La consistencia es sobre la forma, no sobre las capacidades. Un recurso puede tener parámetros propios que ningún otro tiene; lo que no puede es llamarlos con otro convenio.
Diseñar para el consumidor que tienes hoy. La pantalla "mis pedidos" de Aroma Móvil es un caso de uso, no el caso de uso. Optimizar la API hasta convertirla en el backend de una pantalla concreta la vuelve inútil para el siguiente cliente. La prueba: si el nombre de un endpoint contiene el de una pantalla, has cruzado la línea.
Añadir un endpoint agregado a la primera queja. Antes de crear /v1/resumen-cliente, comprueba si expandir y campos resuelven el caso. Cada recurso agregado hay que mantenerlo, versionarlo, documentarlo y probarlo para siempre.
Creer que la guía de estilo se cumple sola. Sin Spectral en la integración continua, la guía se erosiona en tres meses. Automatiza lo automatizable el mismo día que escribes la regla.
Poner todas las reglas de Spectral en error de golpe. Bloqueas a todo el equipo. Entra en warn, limpia y sube.
Tratar openapi.yaml como documentación. Es el contrato. Si el código y el YAML difieren, hay un bug en algún sitio; cuál de los dos es lo veremos en 05-04, con las pruebas de contrato.
Consejo: escribe la petición y la respuesta de ejemplo antes que el código. Cinco minutos escribiendo el JSON que quieres recibir detectan más problemas de diseño que dos horas implementando.
Consejo: lee tu propia API como si fuera ajena. Cierra el editor, abre solo la documentación e intenta resolver un caso de uso completo. Todo lo que te obligue a mirar el código es un fallo de diseño.
Ejercicios
Ejercicio 1: auditoría de antipatrones
Un equipo propone estos cinco endpoints para el módulo de fidelización de Tienda Aroma. Identifica los antipatrones de cada uno y propón la alternativa correcta.
1. POST /v1/clientes/cli_842/calcularPuntos
2. GET /v1/puntos?cliente=842&modo=3
3. GET /v1/clientes/cli_842/puntos → 200 {"exito": true, "datos": {...}}
200 {"exito": false, "error": "sin programa"}
4. GET /v1/clientes/cli_842/pedidos/ped_5001/lineas/1/cafe/puntos
5. GET /v1/promociones → devuelve las 1.200 promociones históricasEjercicio 2: reducir las llamadas de una pantalla
La pantalla "Detalle de pedido" de Aroma Móvil muestra: datos del pedido, nombre y foto de cada café de las líneas, dirección de envío del cliente y estado del envío. Hoy necesita: 1 llamada al pedido + 1 por café (hasta 5) + 1 al cliente + 1 al envío = hasta 8 llamadas.
Diseña la petición única que resuelve la pantalla usando solo los mecanismos que ya existen en la API, y justifica qué límite pondrías a expandir para que este flujo no se convierta en un problema.
Ejercicio 3: escribir una regla de Spectral
Escribe una regla de Spectral llamada aroma-fechas-con-sufijo-iso que avise cuando una propiedad de un esquema tenga formato date-time y su nombre no empiece por fecha. Justifica por qué la severidad debe ser warn y no error en el momento de introducirla.
Soluciones
Solución 1
| Nº | Antipatrones | Alternativa |
|---|---|---|
| 1 | Verbo en la URI (calcularPuntos); además un POST que solo lee |
GET /v1/clientes/cli_842/puntos |
| 2 | Identificador sin prefijo (842); parámetro mágico (modo=3); filtro por cliente en una colección global cuando existe el subrecurso |
GET /v1/clientes/cli_842/puntos?incluirCaducados=true |
| 3 | 200 con exito:false; envoltura datos/exito distinta del resto de la API |
200 con el recurso, o 404 {"error":{"codigo":"programa_no_encontrado"}} |
| 4 | Anidamiento profundo (seis niveles); el mismo dato accesible por varias rutas | GET /v1/cafes/caf_001/puntos, o un campo puntos en la representación del café |
| 5 | Colección sin paginar | GET /v1/promociones?limite=20&desplazamiento=0 con total y Link |
Y un antipatrón transversal: la colección /v1/puntos del caso 2 sugiere que "punto" es un recurso de primer nivel cuando en realidad es un atributo de la relación cliente-programa. Si no existe un GET /v1/puntos/pnt_1 que devuelva un punto individual, probablemente no debería existir la colección.
Solución 2
GET /v1/pedidos/ped_5001?expandir=lineas.cafe,cliente,envio HTTP/1.1
Host: api.tiendaaroma.example
Authorization: Bearer <token>
Accept: application/jsonY para no traer de más, se combina con campos:
GET /v1/pedidos/ped_5001?expandir=lineas.cafe,cliente,envio&campos=id,estado,totalEuros,lineas,cliente,envioDe ocho llamadas a una. Sobre el límite de expandir, tres restricciones que hay que imponer a la vez:
- Profundidad máxima 2.
lineas.cafees válido;lineas.cafe.resenas.autorno. Cada nivel multiplica las consultas. - Lista blanca de rutas expandibles por recurso, declarada en el contrato. No vale cualquier combinación: solo las que tienen una consulta eficiente detrás.
- Prohibido expandir colecciones no acotadas.
expandir=clientetrae un objeto; un hipotéticoexpandir=cliente.pedidostraería una colección entera dentro de otra. Si se permite, se pagina o se limita a los N primeros.
Sin esas tres reglas, el consumidor decide cuánto trabajo hace tu base de datos, que es justo lo que hay que evitar (04-04).
Solución 3
aroma-fechas-con-sufijo-iso:
description: Las propiedades date-time deben nombrarse empezando por 'fecha'.
message: "La propiedad {{property}} es date-time pero no empieza por 'fecha'."
severity: warn
given: $.components.schemas[*].properties[?(@.format == 'date-time')]~
then:
function: pattern
functionOptions:
match: "^fecha[A-Z]?"El given combina dos cosas: el filtro JSONPath [?(@.format == 'date-time')] selecciona solo las propiedades con ese formato, y el ~ final hace que la comprobación se aplique al nombre de la propiedad en vez de a su definición.
Por qué warn y no error al introducirla: la especificación actual ya tiene propiedades que la incumplen (por ejemplo un creadoEn heredado). Si la regla entra como error, la integración continua se pone en rojo y bloquea a todo el equipo por un asunto de estilo, con lo que la reacción probable será desactivarla. El procedimiento correcto es entrar como warn, corregir las infracciones en un pull request específico —renombrando con periodo de convivencia si el campo ya es público— y solo entonces subirla a error para que nadie pueda reintroducir el problema.
Conclusión
Lo que separa una API correcta de una excelente no es una técnica, es un conjunto de decisiones tomadas con criterio y sostenidas en el tiempo. La consistencia por encima de todo, porque es lo que permite al consumidor extrapolar y dejar de leer la documentación; el diseño desde el caso de uso y no desde el modelo de datos, que convierte once llamadas en una sin inventar recursos artificiales; la previsibilidad, los defectos sensatos y seguros, la estrictez en la entrada y la tolerancia en la lectura; la idempotencia como promesa documentada y no como detalle de implementación; los errores que dicen qué hacer a continuación; y la compatibilidad hacia delante diseñada desde el principio, para que /v1 viva años. Tienes además el catálogo de antipatrones para reconocerlos en cualquier API, la lista de revisión que se aplica a cada endpoint nuevo, Spectral para que la guía de estilo se cumpla sola, y una estrategia para la deuda de diseño que ya existe.
Todo esto mejora la API para quien la usa bien. La siguiente lección se ocupa de quien la usa mal: en 04-02, Seguridad en APIs RESTful, recorreremos el OWASP API Security Top 10 sobre Tienda Aroma —con el GET /v1/pedidos/ped_5001 de otro cliente como ejemplo de BOLA, el "rol": "administrador" en el registro como asignación masiva y los endpoints de prueba olvidados como inventario descontrolado—, veremos por qué el transporte se cifra siempre, qué inyecciones siguen siendo posibles después de las sentencias preparadas de 03-05, añadiremos helmet a src/app.js con su posición exacta en la cadena de middlewares, y terminaremos con un modelo de amenazas ligero que dice, activo por activo, dónde está implementada cada defensa.
Curso de REST API: Principios de Diseño y Desarrollo de APIs RESTful
Módulo 1: Introducción a las APIs RESTful
- ¿Qué es una API?
- Historia y evolución de las APIs
- Fundamentos de HTTP para APIs
- Principios básicos de REST
- Modelo de madurez de Richardson y HATEOAS
- REST vs. SOAP
- REST frente a GraphQL, gRPC y webhooks
Módulo 2: Diseño de APIs RESTful
- Principios de diseño de APIs RESTful
- Recursos y URIs
- Métodos HTTP
- Códigos de estado HTTP
- Representaciones, cabeceras y negociación de contenido
- Filtrado, ordenación, paginación y búsqueda
- Versionado de APIs
- Documentación de APIs
Módulo 3: Desarrollo de APIs RESTful
- Configuración del entorno de desarrollo
- Creación de un servidor básico
- Manejo de peticiones y respuestas
- Validación de datos de entrada
- Persistencia y capa de acceso a datos
- Autenticación y autorización
- Manejo de errores
- Pruebas y validación
Módulo 4: Buenas Prácticas y Seguridad
- Buenas prácticas en el diseño de APIs
- Seguridad en APIs RESTful
- OAuth 2.0 y OpenID Connect en la práctica
- Rate limiting y throttling
- CORS y políticas de seguridad
- Caché HTTP y rendimiento
- Observabilidad: logs, métricas y trazas
Módulo 5: Herramientas y Frameworks
- Postman para pruebas de APIs
- Swagger y OpenAPI para documentación
- Frameworks populares para APIs RESTful
- Contratos, mocks y pruebas automatizadas de API
- Integración continua y despliegue
- API gateways y portales de desarrollador
