El contrato de la v1 de Tienda Aroma está completo: recursos, URIs, métodos, códigos, representaciones, colecciones y una política de versionado. Pero un contrato que solo existe en la cabeza del equipo que lo diseñó no es un contrato: es un acuerdo tácito esperando a ser incumplido. Esta lección trata la pieza que convierte el diseño en algo utilizable por otros. Veremos por qué la documentación es parte del producto y no un extra, qué tipos de documento sirven a qué lector, qué debe incluir la referencia de cada endpoint, y qué cambia radicalmente cuando el contrato se escribe en un formato legible por máquinas como OpenAPI. Cerramos el módulo 2 haciendo balance de todo el contrato diseñado y preparando el salto al módulo 3.
Contenido
- La documentación es parte del producto
- Tipos de documentación y a quién sirve cada uno
- Anatomía de la referencia de un endpoint
- Documentación dirigida por contrato: OpenAPI
- Un fragmento real:
GET /cafesen OpenAPI - Qué desbloquea una especificación legible por máquinas
- Documentación como código
- Buenas prácticas de redacción
- Mantener la documentación viva
- Balance del contrato de Tienda Aroma
- La documentación es parte del producto
Una API no tiene interfaz visual. No hay botones que explorar ni menús que insinúen lo que se puede hacer. Para un desarrollador que la consume, la documentación es literalmente el producto: si no está documentado, no existe.
Piénsalo desde el otro lado. Cuando el equipo de RápidoEnvíos se sienta a integrar Tienda Aroma, su experiencia consiste en: leer, probar un curl, leer otra vez, escribir código, encontrar un error no documentado, escribir un correo, esperar dos días. Cada uno de esos pasos es coste. Lo que la buena documentación reduce no es "molestias": es tiempo hasta la primera llamada correcta, la métrica que decide si tu API se adopta o se abandona.
Consecuencias prácticas de tomárselo en serio:
- Se planifica y se estima como cualquier otra funcionalidad. Un endpoint sin documentar no está terminado.
- Se prueba: los ejemplos se ejecutan, no se copian de memoria.
- Tiene responsable y se revisa en las pull requests.
- Reduce el soporte: cada pregunta que llega por correo es una pregunta que la documentación no respondió, y la respuesta correcta no es contestar el correo, sino arreglar la documentación.
- Tipos de documentación y a quién sirve cada uno
El error más común es escribir un único documento gigante. Hay lectores distintos, en momentos distintos, con necesidades incompatibles.
| Tipo | Lector | Cuándo lo lee | Pregunta que responde |
|---|---|---|---|
| Inicio rápido | Desarrollador nuevo | Primeros 15 minutos | ¿Cómo hago mi primera llamada? |
| Guía de conceptos | Integrador | Al empezar el diseño | ¿Cómo funciona este dominio? |
| Tutoriales | Integrador | Al implementar un caso de uso | ¿Cómo hago un flujo completo? |
| Referencia | Todos | Constantemente, mientras programan | ¿Qué parámetros acepta este endpoint? |
| Changelog | Integrador ya activo | Al actualizar, o cuando algo falla | ¿Qué ha cambiado? |
| Ejemplos ejecutables | Todos | Al probar | ¿Puedo ver esto funcionando ya? |
Inicio rápido
El objetivo es una sola cosa: una llamada correcta en menos de cinco minutos. Nada de arquitectura, nada de teoría.
## Tu primera llamada a la API de Tienda Aroma 1. Consigue tu clave en el panel de desarrollador. 2. Ejecuta:
curl -H "Authorization: Bearer TU_TOKEN"
"https://api.tiendaaroma.example/v1/cafes?limite=3"
{ "datos": [ { "id": "caf_001", "nombre": "Etiopía Yirgacheffe", "precioEuros": 14.50 } ], "total": 137 }
Guía de conceptos
Explica el modelo mental, que ninguna referencia transmite. Para Tienda Aroma: qué es un carrito y en qué se diferencia de un pedido, la máquina de estados de los pedidos, el ciclo de moderación de reseñas, cómo funcionan los webhooks firmados, qué significa que los identificadores sean opacos. Sin esto, el integrador deduce el modelo por prueba y error, y deduce mal.
Tutoriales
Recorren un caso de uso completo de principio a fin: "Del carrito al pedido pagado", con las siete llamadas encadenadas, sus respuestas reales y los errores probables en cada paso. Es lo que más se agradece y lo que menos se escribe.
Changelog
Una entrada por cambio, con fecha, tipo (añadido / cambiado / deprecado / eliminado / corregido) y enlace a la guía de migración cuando toque:
## 2026-04-02
### Añadido
- `GET /v1/cafes` acepta el filtro `disponible` (booleano).
- Los cafés incluyen `puntuacionMedia` y `numeroResenas`.
### Deprecado
- Campo `precio` en la representación de café. Usa `precioEuros`.
Retirada prevista: 2027-04-02. Ver [guía de migración](./migracion-precio).Es el documento más barato de mantener y el que más confianza genera: demuestra que la API está viva y que los cambios se anuncian.
- Anatomía de la referencia de un endpoint
La referencia es lo que se consulta a diario. Cada endpoint necesita todos estos elementos; si falta uno, alguien acabará preguntándolo por correo.
| Elemento | Detalle |
|---|---|
| Método y ruta | GET /v1/cafes/{cafeId} |
| Descripción | Una frase que diga qué hace y para qué sirve |
| Permisos | Qué token o rol hace falta |
| Parámetros de ruta | Nombre, tipo, formato, ejemplo |
| Parámetros de query | Nombre, tipo, obligatoriedad, valor por defecto, valores admitidos, máximos |
| Cabeceras | Las que acepta o exige (Idempotency-Key, Accept-Language…) |
| Cuerpo de la petición | Esquema completo, campos obligatorios, validaciones |
| Respuesta de éxito | Código, cabeceras relevantes y ejemplo completo |
| Respuestas de error | Todos los códigos posibles con su codigo de error |
| Límites | Rate limiting, tamaños máximos, coste |
| Idempotencia | Si lo es, y cómo se garantiza |
| Ejemplos | curl completo, copiable y funcional |
Ejemplo abreviado de cómo queda para Tienda Aroma:
### POST /v1/pedidos/{pedidoId}/pago
Paga un pedido pendiente. Crea el recurso de pago asociado al pedido y,
si se completa, cambia su estado a `pagado` y emite el evento `pedido.pagado`
hacia RápidoEnvíos.
**Permisos:** el cliente propietario del pedido, o un token del panel interno.
**Idempotencia:** obligatoria. Debes enviar `Idempotency-Key` con un UUID
único por intento de pago. Los reintentos con la misma clave y el mismo
cuerpo devuelven la respuesta original con `Idempotent-Replay: true`.
**Parámetros de ruta**
| Nombre | Tipo | Descripción |
|---|---|---|
| `pedidoId` | string | Identificador del pedido. Ej.: `ped_5001` |
**Cuerpo**
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `metodo` | string | Sí | `tarjeta` o `transferencia` |
| `tokenTarjeta` | string | Si `metodo` es `tarjeta` | Token de la pasarela |
**Respuestas**
| Código | Cuándo | `codigo` de error |
|---|---|---|
| `201` | Pago realizado (cabecera `Location`) | — |
| `400` | Datos inválidos o falta `Idempotency-Key` | `datos_invalidos`, `clave_idempotencia_requerida` |
| `401` | Sin autenticación válida | `no_autenticado` |
| `403` | El pedido no es tuyo | `permisos_insuficientes` |
| `404` | El pedido no existe | `pedido_no_encontrado` |
| `409` | El pedido ya está pagado o hay un pago en curso | `pedido_ya_pagado`, `operacion_en_curso` |
| `422` | `Idempotency-Key` reutilizada con otro cuerpo | `clave_idempotencia_reutilizada` |
| `502` | La pasarela de pago no responde correctamente | `servicio_no_disponible` |
**Ejemplo**
curl -i -X POST "https://api.tiendaaroma.example/v1/pedidos/ped_5001/pago"
-H "Authorization: Bearer $AROMA_TOKEN"
-H "Idempotency-Key: 5f3b9c2a-1d7e-4a44-9f30-8b1c2d3e4f50"
-H "Content-Type: application/json"
-d '{ "metodo": "tarjeta", "tokenTarjeta": "tok_visa_4242" }'
La columna de errores es la que más se omite y la que más incidencias evita: sin ella, el integrador descubre el 409 el día que un usuario pulsa dos veces.
- Documentación dirigida por contrato: OpenAPI
Todo lo anterior se puede escribir a mano en Markdown. Funciona, y para una API pequeña puede bastar. Pero hay una alternativa que cambia las reglas del juego: escribir el contrato en un formato que las máquinas entiendan.
OpenAPI (antes Swagger) es una especificación —hoy en su versión 3.1— para describir una API HTTP en YAML o JSON: sus rutas, métodos, parámetros, esquemas de datos, respuestas, errores y seguridad. No es documentación sobre la API: es la API descrita formalmente, y la documentación legible es solo uno de sus productos.
graph TD
O["<b>openapi.yaml</b><br/>el contrato"] --> D["Documentación<br/>de referencia navegable"]
O --> C["Clientes generados<br/>(JS, Java, Python…)"]
O --> M["Servidores simulados<br/>(mocks)"]
O --> T["Pruebas de contrato<br/>automáticas"]
O --> V["Validación de<br/>peticiones y respuestas"]
O --> G["Configuración del<br/>gateway y del portal"]
La diferencia con la documentación escrita a mano es de naturaleza, no de grado: un documento en Markdown describe el contrato y puede mentir; una especificación OpenAPI es el contrato y se puede verificar contra la implementación de forma automática.
Esta lección se queda en el porqué y en un fragmento ilustrativo. Swagger y OpenAPI a fondo —editores, generadores, interfaz interactiva, buenas prácticas de escritura— son la lección 05-02, y el uso del contrato para mocks y pruebas automatizadas es 05-04.
- Un fragmento real:
GET /cafes en OpenAPI
GET /cafes en OpenAPIEste es el aspecto que tiene el contrato de la colección de cafés que diseñamos en 02-06, escrito en OpenAPI 3.1:
openapi: 3.1.0
info:
title: API de Tienda Aroma
version: 1.7.0
description: |
API REST de la tienda de café de especialidad Tienda Aroma.
Todos los importes están en euros con dos decimales y todas las
fechas en ISO-8601 UTC.
servers:
- url: https://api.tiendaaroma.example/v1
description: Producción
paths:
/cafes:
get:
summary: Lista el catálogo de cafés
description: |
Devuelve los cafés del catálogo, filtrados, ordenados y paginados.
La paginación es obligatoria: si no se indica `limite`, se aplican 20.
operationId: obtenerCafes
tags: [Cafés]
parameters:
- name: origen
in: query
description: Filtra por país de origen. Admite varios valores separados por comas.
schema: { type: string }
example: Colombia
- name: tueste
in: query
description: Filtra por nivel de tueste. Admite varios valores separados por comas.
schema:
type: string
example: claro,medio
- name: precioMin
in: query
description: Precio mínimo en euros, inclusive.
schema: { type: number, minimum: 0 }
- name: precioMax
in: query
description: Precio máximo en euros, inclusive.
schema: { type: number, minimum: 0 }
- name: q
in: query
description: Búsqueda de texto en nombre, origen y notas de cata.
schema: { type: string, minLength: 2, maxLength: 100 }
- name: ordenar
in: query
description: |
Campo de ordenación. Prefija con `-` para orden descendente.
El orden se desempata siempre por `id` ascendente.
schema:
type: string
enum: [nombre, -nombre, precioEuros, -precioEuros, stock, -stock, fechaCreacion, -fechaCreacion]
default: nombre
- $ref: '#/components/parameters/limite'
- $ref: '#/components/parameters/desplazamiento'
responses:
'200':
description: Lista de cafés.
headers:
Link:
description: Enlaces de paginación (RFC 8288) con rel next, prev, first y last.
schema: { type: string }
content:
application/json:
schema:
type: object
required: [datos, total]
properties:
datos:
type: array
items: { $ref: '#/components/schemas/Cafe' }
total:
type: integer
description: Número total de elementos que cumplen el filtro.
example:
datos:
- id: caf_001
nombre: Etiopía Yirgacheffe
origen: Etiopía
tueste: claro
precioEuros: 14.50
stock: 120
notasCata: [cítrico, floral, té negro]
fechaCreacion: '2026-01-15T08:30:00Z'
total: 137
'400':
description: Parámetro de consulta inválido.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
example:
error:
codigo: parametro_invalido
mensaje: "El parámetro 'limite' no puede superar 100."
detalles: []
components:
parameters:
limite:
name: limite
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
desplazamiento:
name: desplazamiento
in: query
schema: { type: integer, minimum: 0, maximum: 10000, default: 0 }
schemas:
Cafe:
type: object
required: [id, nombre, origen, tueste, precioEuros, stock]
properties:
id:
type: string
pattern: '^caf_[a-zA-Z0-9]+$'
description: Identificador opaco. No lo parsees.
nombre: { type: string, maxLength: 120 }
origen: { type: string }
tueste:
type: string
enum: [claro, medio, oscuro]
description: Pueden añadirse valores nuevos sin previo aviso.
precioEuros: { type: number, minimum: 0, multipleOf: 0.01 }
stock: { type: integer, minimum: 0 }
notasCata:
type: array
items: { type: string }
fechaCreacion: { type: string, format: date-time }
Error:
type: object
required: [error]
properties:
error:
type: object
required: [codigo, mensaje, detalles]
properties:
codigo: { type: string, example: cafe_no_encontrado }
mensaje: { type: string }
detalles: { type: array, items: { type: object } }Fíjate en cuánto contrato de este módulo está aquí codificado, y de forma verificable:
- El
limitepor defecto 20 y máximo 100, y el tope dedesplazamiento(02-06). - El desempate por
iden la ordenación, documentado en la descripción. - El envoltorio
datos/total(02-05) y el formato de error concodigo/mensaje/detalles(02-04). - El patrón del identificador y el aviso de opacidad (02-02).
- Los importes con
multipleOf: 0.01, es decir, dos decimales (02-05). - El aviso de que el enumerado
tuestepuede crecer (02-07). - La cabecera
Linkcomo parte declarada de la respuesta.
Los $ref a components evitan repetir limite, desplazamiento, Cafe y Error en cada endpoint: se escriben una vez y se referencian, que es exactamente la consistencia que pedía 02-01, ahora garantizada por construcción.
- Qué desbloquea una especificación legible por máquinas
| Producto | Qué es | Beneficio | Se ve en |
|---|---|---|---|
| Referencia navegable | Documentación HTML generada | Nunca se desincroniza del contrato | 05-02 |
| Interfaz interactiva | "Pruébalo" desde el navegador | Primera llamada sin escribir código | 05-02 |
| Clientes generados | SDK en varios lenguajes | El consumidor no escribe código HTTP | 05-02 |
| Servidores simulados | Mock que responde según el contrato | La SPA avanza sin esperar al servidor | 05-04 |
| Pruebas de contrato | Verifican implementación contra especificación | Detectan la deriva automáticamente | 05-04 |
| Validación en ejecución | Middleware que valida peticiones y respuestas | Errores coherentes sin escribirlos a mano | 03-04 |
| Linters de estilo | Comprueban las convenciones de la guía | La guía de estilo deja de ser voluntaria | 05-05 |
| Configuración de gateway | Rutas, límites y seguridad importados | Menos configuración duplicada | 05-06 |
El más importante para un proyecto API-first como el nuestro es el mock: con el openapi.yaml terminado, la SPA y Aroma Móvil pueden empezar a integrar el día uno contra un servidor simulado, mientras el equipo de servidor implementa. Eso es lo que hace que "diseñar antes de programar" no sea tiempo perdido, sino paralelismo ganado.
- Documentación como código
La documentación se trata exactamente igual que el código:
- Vive en el repositorio, junto a la implementación.
openapi.yamlen la raíz, guías endocs/. - Se versiona con Git, así que el histórico responde a "¿desde cuándo dice esto?".
- Se revisa en pull request: un cambio de API que no toca el contrato ni el changelog no se aprueba.
- Se valida en integración continua: la especificación se comprueba sintácticamente, se pasa un linter de estilo y se ejecutan las pruebas de contrato.
- Se publica automáticamente al fusionar en la rama principal.
Estructura típica del repositorio de Tienda Aroma:
tienda-aroma-api/ ├── openapi.yaml # el contrato ├── CHANGELOG.md # cambios por fecha ├── docs/ │ ├── guia-estilo.md # las convenciones de 02-01 │ ├── inicio-rapido.md │ ├── conceptos/ │ │ ├── pedidos-estados.md │ │ ├── idempotencia.md │ │ └── webhooks.md │ ├── tutoriales/ │ │ └── del-carrito-al-pedido-pagado.md │ └── migraciones/ │ └── migracion-precio.md └── src/ # la implementación (módulo 3)
El detalle que hace que esto funcione de verdad: la puerta de calidad en CI. Si el pipeline falla cuando la implementación no cumple el contrato, la documentación deja de depender de la buena voluntad. Se monta en 05-05.
- Buenas prácticas de redacción
Ejemplos reales y copiables. Nada de <TU_ID_AQUÍ> mezclado con datos falsos. Usa los identificadores del dominio (caf_001, ped_5001, cli_842) de forma consistente en toda la documentación: quien lee un tutorial reconoce los mismos datos en la referencia.
# ✗ Inútil: no se puede ejecutar y no dice qué devuelve
GET /cafes?params
# ✓ Copiable, ejecutable, con respuesta esperada
curl -H "Authorization: Bearer $AROMA_TOKEN" \
"https://api.tiendaaroma.example/v1/cafes?tueste=medio&limite=2"curl completo, siempre. Con la cabecera de autenticación, la URL entrecomillada (por el &, como vimos en 01-03) y el cuerpo entero. Es el mínimo común denominador: funciona en cualquier sistema y no supone ningún lenguaje.
Documenta los errores tanto como los éxitos. Es la diferencia más visible entre una documentación profesional y una amateur.
Explica el porqué, no solo el qué. "Idempotency-Key es obligatoria porque un reintento tras un timeout podría cobrar dos veces" enseña; "Idempotency-Key: cadena, obligatoria" solo informa.
Nada de "TBD", "por documentar" o "pendiente". Un hueco declarado es peor que la ausencia: el lector pierde el tiempo confiando en que aparecerá.
Coherencia con la guía de estilo. Si la guía dice camelCase, ningún ejemplo lleva snake_case. La documentación es donde las incoherencias se ven, y donde destruyen la confianza más rápido.
Escribe para quien no conoce tu dominio. El primer uso de "carrito", "línea" o "moderación" merece una definición. El equipo de RápidoEnvíos sabe de logística, no de café de especialidad.
Cuidado con las capturas de pantalla. Envejecen mal y no se pueden buscar ni copiar. Prefiere bloques de código.
- Mantener la documentación viva
El enemigo tiene nombre: la deriva (drift), la distancia que se abre entre lo que la documentación dice y lo que la API hace. Una API mal documentada es un problema; una API mal documentada que parece bien documentada es peor, porque el integrador confía y falla.
Generada frente a escrita a mano
| Enfoque | Cómo funciona | A favor | En contra |
|---|---|---|---|
| Contrato primero | Se escribe openapi.yaml y de él salen documentación, mocks y validación |
Coherente con API-first; permite mocks antes de implementar | Requiere disciplina para que el código siga el contrato |
| Código primero | Se anotan los controladores y se genera la especificación | Difícil que se desincronice de la implementación | Documenta lo que hay, no lo que se acordó; llega tarde |
| Mixto | Contrato escrito a mano + pruebas que verifican la implementación | Lo mejor de ambos | Hay que montar las pruebas de contrato |
Tienda Aroma usa el enfoque mixto: openapi.yaml escrito a mano —es la fuente de verdad, coherente con el API-first de 02-01— y pruebas de contrato en CI que fallan si la implementación se desvía. Las guías y tutoriales se escriben a mano siempre: ningún generador explica por qué un carrito no es un pedido.
Detectar la deriva
| Técnica | Qué detecta | Dónde se trata |
|---|---|---|
| Pruebas de contrato en CI | Respuestas que no cumplen el esquema | 05-04 |
| Validación de respuestas en preproducción | Campos nuevos no documentados | 03-04 |
| Ejecutar los ejemplos de la documentación como pruebas | Ejemplos obsoletos | 05-04 |
| Linter de OpenAPI | Convenciones incumplidas, descripciones ausentes | 05-05 |
| Revisión obligatoria en cada PR que toque la API | Cambios sin documentar | Proceso |
| Métricas de uso frente a endpoints documentados | Endpoints "fantasma" no documentados | 04-07 |
Señales de alarma
- El changelog lleva meses sin entradas, pero la API ha cambiado.
- Los ejemplos usan campos que ya no existen.
- Hay endpoints en producción que no aparecen en la especificación.
- Las respuestas de error reales no coinciden con las documentadas.
- El equipo responde por chat preguntas que la documentación debería responder.
- Balance del contrato de Tienda Aroma
Esto es lo que hemos diseñado a lo largo del módulo 2, y es exactamente lo que el módulo 3 va a implementar.
Método y principios (02-01). Enfoque API-first, consumidores identificados (SPA, Aroma Móvil, panel interno, RápidoEnvíos), recursos extraídos del dominio y una guía de estilo escrita.
Recursos y URIs (02-02). Mapa completo de 24 URIs sobre https://api.tiendaaroma.example/v1, plural en minúsculas, kebab-case, anidamiento máximo de dos niveles, singleton en singular, identificadores opacos con prefijo y acciones no CRUD modeladas como subrecursos con POST (/pago, /anulacion, /aprobacion, /rechazo).
Métodos (02-03). GET, POST, PUT, PATCH, DELETE, HEAD y OPTIONS asignados recurso a recurso; PATCH con JSON Merge Patch; borrado lógico invisible desde fuera; Idempotency-Key obligatoria en POST /pedidos y POST /pedidos/{id}/pago.
Códigos (02-04). Los códigos que se usan de verdad, con 201 + Location, 409 para los conflictos de estado, la distinción 401/403 y 404/410; formato de error propio {"error": {"codigo", "mensaje", "detalles"}} frente a problem+json; catálogo de 28 códigos de error de negocio.
Representaciones (02-05). camelCase, ISO-8601 UTC, euros con dos decimales, enumerados snake_case ampliables, null frente a ausente, envoltorio datos/total, criterios de incrustar frente a enlazar, _links de hipermedia selectiva, expandir y campos, y negociación con Accept, Accept-Language, Accept-Encoding y Vary.
Colecciones (02-06). Filtros explícitos, rangos Min/Max y Desde/Hasta, multivalor con comas, ordenar con desempate por id, offset para /cafes y cursor para /pedidos, total en el cuerpo y navegación en la cabecera Link, ?q= para búsqueda, y límites por defecto y máximos.
Evolución (02-07). Versión en la ruta, dos versiones vivas como máximo, 12 meses de deprecación, cabeceras Deprecation y Sunset y apagado con 410 Gone.
Documentación (02-08). openapi.yaml como fuente de verdad, guías y tutoriales a mano, changelog por fecha, todo en el repositorio y validado en CI.
Errores Comunes y Consejos
- Dejar la documentación para el final. Nunca llega ese final. Documenta el endpoint en la misma pull request que lo crea.
- Documentar solo el camino feliz. Los errores son la mitad del trabajo del integrador:
409y422sin documentar generan incidencias que nadie sabe explicar. - Ejemplos que no se pueden ejecutar. Prueba cada
curlde la documentación. Si un ejemplo falla, has perdido la confianza del lector para todo lo demás. - Confundir referencia con guía. La referencia dice qué acepta un endpoint; nunca explica cómo encadenar seis llamadas para pagar un pedido. Hacen falta las dos.
- Generar la documentación del código y darla por buena. Documenta lo que hay, incluidos los bugs, y no dice nada de conceptos ni de intención.
- No documentar los límites. Rate limits, tamaños máximos,
limitemáximo y profundidad de expansión son parte del contrato: sin ellos, el integrador los descubre con un400en producción. - Olvidar el changelog. Es el documento más barato y el que más agradece un consumidor externo.
- Consejo: mide el "tiempo hasta la primera llamada correcta". Siéntate con alguien que no conozca la API, dale la documentación y cronometra en silencio. Descubrirás más en veinte minutos que en tres reuniones.
- Consejo: trata cada pregunta de soporte como un fallo de documentación. La respuesta no es contestar el correo, es arreglar el documento y luego contestar con el enlace.
Ejercicios
Ejercicio 1: escribir la referencia de un endpoint
Escribe la documentación de referencia completa de POST /v1/cafes/{cafeId}/resenas con todos los elementos de la sección 3. Usa el contrato diseñado en este módulo: el cuerpo lleva puntuacion (entero de 1 a 5) y comentario (texto de hasta 5.000 caracteres); la reseña se crea en estado pendiente_moderacion; solo pueden reseñar los clientes que hayan comprado ese café.
Ejercicio 2: completar el contrato OpenAPI
Amplía el fragmento de la sección 5 añadiendo la operación GET /cafes/{cafeId}: parámetro de ruta, respuesta 200 con el esquema Cafe reutilizado, respuesta 404 con el esquema Error y ejemplo del error cafe_no_encontrado, y respuesta 304 para la caché condicional. Reutiliza los components existentes.
Ejercicio 3: detectar deriva
Un desarrollador llega a Tienda Aroma y encuentra esto. Identifica todos los problemas de documentación y propón el remedio concreto y el proceso que evitaría que vuelva a ocurrir.
- La referencia dice que
GET /v1/cafesdevuelve un array; la API devuelve{"datos": [...], "total": n}. - No hay ninguna mención al parámetro
expandir, que sin embargo funciona. - El ejemplo de
POST /v1/pedidosno incluye la cabeceraIdempotency-Key, que es obligatoria. - La tabla de errores de
POST /v1/pedidos/{id}/pagosolo lista400y500. - El changelog termina hace ocho meses.
- Hay un endpoint
/v1/promocionesen producción que no aparece en ningún sitio.
Soluciones
Solución 1
### POST /v1/cafes/{cafeId}/resenas
Crea una reseña sobre un café. La reseña se crea en estado
`pendiente_moderacion` y no aparece en las listas públicas hasta que un
moderador la apruebe con `POST /v1/resenas/{resenaId}/aprobacion`.
**Permisos:** cliente autenticado que haya comprado el café en un pedido
en estado `enviado`. En caso contrario se devuelve `403`.
**Idempotencia:** no obligatoria. `Idempotency-Key` es opcional y se
recomienda para evitar reseñas duplicadas por doble envío del formulario.
**Parámetros de ruta**
| Nombre | Tipo | Descripción |
|---|---|---|
| `cafeId` | string | Identificador del café. Ej.: `caf_001` |
**Cuerpo**
| Campo | Tipo | Obligatorio | Validación |
|---|---|---|---|
| `puntuacion` | integer | Sí | Entre 1 y 5, ambos incluidos |
| `comentario` | string | Sí | Entre 10 y 5.000 caracteres |
**Respuestas**
| Código | Cuándo | `codigo` de error |
|---|---|---|
| `201` | Reseña creada (cabecera `Location`) | — |
| `400` | Validación fallida | `datos_invalidos` |
| `401` | Sin autenticación | `no_autenticado` |
| `403` | El cliente no ha comprado este café | `permisos_insuficientes` |
| `404` | El café no existe | `cafe_no_encontrado` |
| `409` | El cliente ya ha reseñado este café | `resena_duplicada` |
| `410` | El café está descatalogado | `cafe_descatalogado` |
| `429` | Límite de peticiones superado | `limite_peticiones` |
**Límites:** máximo 5 reseñas por cliente y día.
**Ejemplo**
curl -i -X POST "https://api.tiendaaroma.example/v1/cafes/caf_001/resenas"
-H "Authorization: Bearer $AROMA_TOKEN"
-H "Content-Type: application/json"
-d '{ "puntuacion": 5, "comentario": "Cítrico y floral, espectacular en V60." }'
HTTP/1.1 201 Created Location: https://api.tiendaaroma.example/v1/resenas/res_101
{ "id": "res_101", "cafeId": "caf_001", "clienteId": "cli_842", "puntuacion": 5, "comentario": "Cítrico y floral, espectacular en V60.", "estado": "pendiente_moderacion", "fechaCreacion": "2026-03-14T10:30:00Z", "_links": { "self": { "href": "/v1/resenas/res_101" }, "cafe": { "href": "/v1/cafes/caf_001" } } }
Observa que ha aparecido un código de error nuevo, resena_duplicada (409): documentar obliga a cerrar decisiones que el diseño había dejado abiertas. Ese es uno de los grandes beneficios de escribir la referencia antes de implementar.
Solución 2
/cafes/{cafeId}:
get:
summary: Obtiene un café concreto
operationId: obtenerCafePorId
tags: [Cafés]
parameters:
- name: cafeId
in: path
required: true
description: Identificador opaco del café.
schema:
type: string
pattern: '^caf_[a-zA-Z0-9]+$'
example: caf_001
- name: If-None-Match
in: header
required: false
description: ETag de una copia previa, para caché condicional.
schema: { type: string }
example: '"a1b2c3d4"'
responses:
'200':
description: El café solicitado.
headers:
ETag:
description: Identificador de versión de la representación.
schema: { type: string }
content:
application/json:
schema: { $ref: '#/components/schemas/Cafe' }
example:
id: caf_001
nombre: Etiopía Yirgacheffe
origen: Etiopía
tueste: claro
precioEuros: 14.50
stock: 120
notasCata: [cítrico, floral, té negro]
fechaCreacion: '2026-01-15T08:30:00Z'
'304':
description: |
La representación no ha cambiado desde la versión indicada
en `If-None-Match`. Sin cuerpo.
headers:
ETag:
schema: { type: string }
'404':
description: No existe ningún café con ese identificador.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
example:
error:
codigo: cafe_no_encontrado
mensaje: "No existe ningún café con el identificador 'caf_999'."
detalles: []
'410':
description: El café existió y se ha descatalogado definitivamente.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
example:
error:
codigo: cafe_descatalogado
mensaje: "El café 'caf_001' se retiró del catálogo el 2026-02-01."
detalles: []Nótese que Cafe y Error se reutilizan con $ref: al añadir mañana un campo al esquema Cafe, aparece automáticamente en las dos operaciones y en la documentación generada. Eso es consistencia garantizada por construcción, no por revisión humana.
Solución 3
| Problema | Gravedad | Remedio | Prevención |
|---|---|---|---|
| La referencia dice array y la API devuelve envoltorio | Crítica: todo integrador nuevo escribe código que falla en la primera llamada | Corregir el esquema en openapi.yaml y publicar |
Pruebas de contrato en CI (05-04): una respuesta que no cumple el esquema debe romper el build |
expandir no documentado |
Alta: funcionalidad invisible que además nadie garantiza mantener | Documentarlo con sus reglas (un nivel, máximo 3, 400 si es desconocido) |
Revisión obligatoria en PR: ningún parámetro nuevo se fusiona sin contrato |
Falta Idempotency-Key en el ejemplo |
Alta: el ejemplo copiado devuelve 400 |
Corregir el ejemplo y marcar la cabecera como obligatoria | Ejecutar los ejemplos como pruebas en CI |
| Tabla de errores incompleta | Alta: el 409 pedido_ya_pagado aparece en producción sin previo aviso |
Completar con 401, 403, 404, 409, 422 y 502 |
Plantilla de endpoint con la tabla de errores como campo obligatorio |
| Changelog abandonado | Media: se pierde la confianza y las deprecaciones no se enteran | Reconstruirlo a partir del histórico de Git y retomarlo | Comprobación en CI: si cambia openapi.yaml, debe cambiar CHANGELOG.md |
Endpoint fantasma /v1/promociones |
Crítica: superficie no documentada, no versionada y probablemente no auditada en seguridad | Decidir: documentarlo o retirarlo. No hay tercera opción | Comparar rutas reales (métricas de 04-07) con las de la especificación y alertar de las diferencias |
El proceso que lo evita todo, en una frase: la especificación es la fuente de verdad, vive en el repositorio, se valida en cada pull request y el pipeline falla si la implementación no la cumple. Sin puerta automática, la deriva es cuestión de tiempo.
Conclusión
La documentación no es lo que se escribe después de programar: es la cara visible de un producto que no tiene interfaz. Ahora sabes que hacen falta documentos distintos para lectores distintos —inicio rápido, conceptos, tutoriales, referencia, changelog—, qué debe contener la referencia de cada endpoint (incluidos todos sus errores, que es lo que más se omite), y por qué escribir el contrato en OpenAPI cambia la naturaleza del asunto: deja de ser un texto que describe la API y pasa a ser el contrato mismo, del que salen la documentación navegable, los clientes, los mocks, las pruebas y la configuración del gateway. Con documentación como código, revisada en pull request y validada en CI, la deriva deja de depender de la buena voluntad.
Con esto cerramos el módulo 2. Has diseñado, pieza a pieza y sobre el papel, el contrato completo de la API de Tienda Aroma: su método de trabajo y su guía de estilo, sus 24 URIs con las acciones modeladas como subrecursos, sus métodos con la idempotencia del pago resuelta, su tabla de códigos y su catálogo de errores de negocio, sus representaciones con hipermedia selectiva, sus colecciones filtradas y paginadas, su política de versionado y deprecación, y su documentación. Nada de esto ha necesitado todavía una línea de servidor, y esa era exactamente la idea: en API-first, el contrato va delante. Ahora toca cumplirlo. En el módulo 3, Desarrollo de APIs RESTful, montaremos el entorno con Node.js 20, levantaremos el servidor con Express y convertiremos cada decisión de este módulo en código: las rutas del mapa de URIs, la validación que devuelve datos_invalidos con sus detalles, la capa de persistencia que traduce céntimos a euros, la autenticación que distingue 401 de 403, el middleware de errores que emite el formato que hemos fijado y las pruebas que verifican que la implementación respeta el contrato. Empezamos por el principio: 03-01, Configuración del entorno de desarrollo.
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
