1. Limitaciones de la colección y mapa de contrato
La colección ya almacena solicitudes ejecutables. Ahora falta el segundo artefacto: un registro breve de qué hemos aprendido sobre el contrato mientras ejecutábamos esas solicitudes.
Collection es súper útil, pero tiene un pequeño problema «humano»: almacena las solicitudes como artefactos ejecutables, no como un registro comprensible de hechos. Dentro de una semana abrirá Postman, verá 12 solicitudes y pensará: «Bueno… ¿y cuál es importante?». El mapa de contrato es una fijación textual corta: qué escenarios hay, cuáles son sus method/path/params/status y qué forma de respuesta necesitamos realmente para ReadLater Starter.
Es importante captar la escala correcta. El mapa de contrato no es una «documentación completa del proveedor de 40 páginas» y mucho menos un «copia-pega de todo el JSON». Es más bien el pasaporte de dos o tres escenarios principales. Cuando empiece a escribir código, el mapa será su ancla: no tendrá que «recordar por sensaciones» dónde está el parámetro query, qué estado se considera éxito y cómo se llama el campo con el identificador externo.
Puede imaginarlo así: collection es un conjunto de botones que se pueden pulsar. El mapa de contrato es una breve instrucción de qué ocurrirá al pulsar cada botón y qué debe notarse en el resultado. Y sí, si usted ha llamado a la solicitud test 1 y la ha dejado en el historial, todavía no tiene un mapa; tiene arqueología.
2. Plantilla del mapa de contrato
Para que el mapa de contrato no se convierta en una redacción del tipo «cómo pasé el verano en Postman», necesita una plantilla. Buena noticia: la plantilla es muy corta y casi completamente está formada por palabras ya conocidas: method, path, query, status, headers, body. Mala noticia (es broma): habrá que anotar disciplinadamente lo mismo para cada escenario, como un ingeniero adulto, no como un romántico que dice «ya me lo recordaré».
A continuación se muestra un formato cómodo para el mapa de contrato. Lo hago deliberadamente en dos niveles: primero los hechos de transporte, luego los hechos sobre la forma de los datos. Así no caerá en la trampa favorita de los principiantes: «vi el JSON y olvidé que también había un código de estado y un Content-Type».
Miniplantilla
## Scenario: <Nombre del escenario>
Transport:
- Method: <GET/POST/...>
- Path: <p.ej., /search o /books/{externalId}>
- Query params: <cuáles hay y cuáles son obligatorios>
- Success status: <p.ej., 200>
- Response Content-Type: <p.ej., application/json>
Body shape:
- Top-level: <object | array>
- Required fields we use: <lo que importa para ReadLater>
- Optional fields we use: <lo que puede faltar>
- Notes: <1-2 observaciones sobre rarezas/matices>
Si le gusta fijar esto como datos estructurados, puede tener en mente este «modelo mínimo» — solo para no olvidar la composición de los campos.
// Instantánea mínima del contrato: suficiente para navegar por los escenarios
record ContractSnapshot(String scenario, String method, String path, int successStatus) {}
// Ejemplo: fijamos el escenario de búsqueda como un hecho (qué llamamos y qué estado consideramos éxito)
ContractSnapshot search = new ContractSnapshot(
"Search books by query",
"GET",
"/search",
200
);
Observe un detalle útil: es mejor registrar la path como una plantilla, no como una URL concreta con un valor específico. Es decir, /books/{externalId}, no /books/OL12345M. También puede conservar un ejemplo concreto, pero por separado, como «ejemplo de ejecución».
3. Escenario «Search»: búsqueda
La búsqueda es el escenario con el que normalmente comienza la vida de cualquier «catálogo»: el usuario aún no conoce el identificador externo del libro, solo sabe de clean code, Harry Potter o «algo sobre microservicios, pero sin microservicios, por favor». Por lo tanto, para ReadLater Starter la búsqueda es la entrada básica: obtenemos una lista de candidatos y seleccionamos qué libro ver en detalle a continuación.
Cuando fija el contrato de búsqueda, su tarea no es «copiar los 200 campos que el proveedor haya tenido a bien enviar», sino comprender la forma de la respuesta y elegir aquellos campos que realmente necesita nuestro proyecto. Para un curso puente esto es especialmente importante: no estamos construyendo un producto a nivel de Amazon, estamos construyendo un artefacto didáctico comprensible. Por lo tanto, tomamos el mínimo: id externo, título, autor.
Ejemplo de transporte
Supongamos que el escenario tiene este aspecto:
Method:
GET
Path:
/search
Query:
q — parámetro obligatorio de la consulta de búsqueda
Success status:
200
Response Content-Type:
application/json
Ejemplo de URL que puede guardar como «ejecución de referencia» en Postman:
GET https://catalog.example/search?q=clean+code
Aquí es importante fijar una idea: q es el mango de control del escenario. Por lo tanto, en el mapa de contrato es útil anotar honestamente: «sin q la búsqueda no tiene sentido».
Ejemplo de forma de respuesta
{
"items": [
{
"externalId": "OL12345M",
"title": "Clean Code",
"author": "Robert C. Martin"
}
],
"count": 1
}
Incluso si los nombres de su proveedor son diferentes (docs, results, total, id), el principio es el mismo: tenemos una lista y alguna metainformación sobre la cantidad. Para el mapa de contrato es importante registrar precisamente la forma: objeto de nivel superior, dentro un array de elementos, y por separado un número count/total.
También aquí es útil notar y escribir en Notes: count y items.size() no tienen por qué coincidir. El proveedor puede devolver count = 1000, pero items solo los primeros 10 resultados. Esto no es un «error de la API», es una práctica habitual.
Campos para ReadLater Starter
Para la búsqueda normalmente bastan tres cosas: externalId, title, author. Y aquí surge el primer dolor real del contrato: el autor puede ser una cadena, un array de cadenas, un objeto, o simplemente no estar donde usted espera. Y eso es normal. En el mapa de contrato conviene fijar no solo el nombre del campo, sino también su «ruta» en el JSON y una observación sobre su obligatoriedad.
import java.util.List;
// Descripción de un campo que «tomamos» de la respuesta.
record FieldNote(String jsonPath, boolean optional) {}
// Miniconjunto de lo que necesitamos críticamente de la respuesta de búsqueda.
List<FieldNote> searchFields = List.of(
new FieldNote("items[].externalId", false), // id externo — base para la solicitud de detalles
new FieldNote("items[].title", false), // título que mostramos al usuario
new FieldNote("items[].author", true) // a veces puede faltar/ser un array
);
La gracia está en que no finge que el mundo es perfecto. Si ha notado que author a veces falta o tiene otro aspecto, no está «criticando la API», simplemente está registrando la realidad del contrato. Eso es pensamiento backend: no ofenderse con la red y los servicios externos, sino trabajar con los hechos.
4. Escenario «Details»: detalles
El escenario de detalles suele ser más simple en cuanto a forma (un objeto en lugar de una lista), pero más traicionero en contenido: en los detalles el proveedor suele enviar muchos más campos, más anidamiento y más «opcionalidad». Y el principiante en ese momento tiene muchas ganas de «copiarlo todo». Es un deseo comprensible, pero suele terminar con que el mapa de contrato se hincha hasta el tamaño de una novela y su utilidad cae al de un marcapáginas.
El sentido del escenario details para ReadLater Starter en esta etapa es simple: a partir del identificador externo del libro obtener una «ficha» que se pueda mostrar al usuario y usar como fuente de datos para añadir a la lista de lectura. Por lo tanto, fijamos la parte de transporte y un conjunto mínimo de campos para la ficha: id externo, título, autor y, si tenemos suerte, una breve descripción.
Ejemplo de transporte
Aspecto típico:
Method: GET Path: /books/{externalId} Path variable: externalId — obligatoria
Success status: 200 Response Content-Type: application/json
Ejemplo de ejecución de referencia:
GET https://catalog.example/books/OL12345M
Y aquí hay una observación contractual importante: externalId vive en la ruta, no en query. Es decir, no es una «búsqueda», es «dame un recurso concreto por su id». Vale la pena anotarlo en el mapa al menos con una frase, para no confundirlo después durante la implementación.
Ejemplo de forma de respuesta
{
"externalId": "OL12345M",
"title": "Clean Code",
"author": "Robert C. Martin",
"description": "A Handbook of Agile Software Craftsmanship"
}
En el mundo real, description puede ser null, puede ser un objeto anidado o simplemente faltar. Y eso, de nuevo, no es motivo para dramatizar. Simplemente registramos que el campo puede ser opcional, y precisamente por eso no debe ser el único pilar de su lógica.
Para el mapa de contrato es útil dividir claramente los campos en «obligatorios para nuestro proyecto» y «bonificaciones agradables». No es una lista de «lo que devuelve el proveedor», sino una lista de «lo que necesitamos para hacer la siguiente parte del proyecto».
Por ejemplo, en las notas de details puede escribir honestamente una idea como: «externalId a veces no llega en el cuerpo de la respuesta, pero siempre se conoce a partir de la solicitud». Esta es una fijación de ingeniería muy útil: no pierde una hora preguntándose «¿por qué externalId es null?», sino que lo añade tranquilamente a la ficha normalizada desde el contexto de la solicitud.
Y sí, si en la respuesta author llega como un array, también lo registramos como observación. No es un «proveedor malo», es simplemente la realidad de los contratos de transporte.
5. Conjunto smoke mínimo
Si el mapa de contrato es un «pasaporte», entonces el conjunto smoke es una «prueba rápida de que el pasaporte no es falso». Y la palabra smoke aquí no se refiere a que todo se haya quemado (aunque a veces ocurre), sino a una vieja idea de ingeniería: ejecuta lo más básico, y si funciona, significa que el sistema al menos está vivo. Para un proyecto educativo, el conjunto smoke es especialmente importante: permite no depender de una memoria perfecta y no convertir cada nuevo intento en una «investigación desde cero».
A nivel de esta lección, el conjunto smoke es exactamente una línea base manual mínima: un par de solicitudes guardadas que se pueden ejecutar y verificar visualmente el status, el Content-Type y la forma de la respuesta. Sin scripts ni intentos de automatizar todo de inmediato.
Ejemplo de conjunto smoke mínimo
A continuación se muestra una tabla práctica que puede insertar directamente en su mapa de contrato o en el README junto a la colección de Postman:
| Nombre de la solicitud (en la colección) | Method | URL (ejemplo) | Esperamos | Qué comprobamos visualmente |
|---|---|---|---|---|
| Search books by query | GET | /search?q=clean+code | 200 + JSON | Hay un objeto de nivel superior, hay un array de resultados (items/results), dentro al menos un elemento tiene title y algún id |
| Get book details by external id | GET | /books/OL12345M | 200 + JSON | Hay un objeto de nivel superior, hay title, hay author (o una estructura comprensible de autores), el formato de respuesta no se ha convertido en HTML |
Aquí hay un detalle sutil. Para la solicitud de details necesita un externalId concreto que considere un «ejemplo». Puede tomarlo del resultado de la búsqueda y, una vez, seleccionarlo manualmente e insertarlo en la solicitud guardada. No es un encadenamiento automático de solicitudes; simplemente elegimos un ejemplo estable para poder ejecutar el smoke sin tener que hacer una búsqueda.
Y otra disciplina simple: para el conjunto smoke es mejor elegir solicitudes que no dependan de datos raros o inestables. Si busca asdkjashdkjashd, se está creando una situación en la que mañana count=0 y no entenderá: ¿cambió el contrato o simplemente buscó una tontería? La consulta clean code en un contexto educativo suele vivir más tranquila.
6. Almacenamiento del resultado
Existe una trampa clásica de cualquier herramienta: «si vive solo dentro de la herramienta, vive poco tiempo». Hoy lo recuerda todo, mañana Postman se actualiza, la colección termina en otro espacio de trabajo, y dentro de un mes querrá ver un diff en Git y de repente descubrirá que «el contrato no está escrito en ninguna parte con palabras». Por lo tanto, un buen final del día son dos artefactos: la propia colección y el mapa de contrato textual en el repositorio junto al código de ReadLater Starter.
Una variante cómoda de la estructura del repositorio se ve así: la colección vive en postman/, y el mapa de contrato en docs/ (o también en postman/, si quiere mantener todos los artefactos de Postman en un mismo lugar). No es «más correcto según los cánones», sino simplemente cómodo para la persona: el código por un lado, los artefactos de integración por otro.
Ejemplo de archivo docs/catalog-contract.md
# External Catalog Contract
## Scenario: Search books by query
Transport: GET /search?q=<query>, success=200, response=application/json
Body shape: object with array results + count
We use fields: externalId, title, author
Notes: count may be > returned items count
## Scenario: Get book details by external id
Transport: GET /books/{externalId}, success=200, response=application/json
Body shape: object
We use fields: title, author, (optional) description
Notes: externalId may be missing in response body; take from request path
Sí, parece un poco aburrido. Y en eso reside su fuerza. Una buena fijación de ingeniería casi siempre es más aburrida que una caótica «historia de aventuras». Pero ese aburrimiento ahorra horas de vida cuando más adelante implemente el cliente y empiece a encontrar discrepancias entre «cómo me parecía» y «cómo es realmente».
No se trata de burocracia. Cuando la colección, el mapa de contrato y dos solicitudes smoke manuales están junto al proyecto, el contrato deja de vivir solo en la memoria y en el botón Send. Sobre esa base ya se pueden añadir tranquilamente variables y enlaces entre solicitudes, porque los escenarios básicos están fijados.
7. Errores típicos al trabajar con el mapa de contrato
Error nº1: fijar solo el body y olvidarse del method/path/status/headers.
El principiante suele hacer una «captura de pantalla del JSON» y cree que el contrato está fijado. Luego resulta que ese JSON lo obtuvo solo con GET, solo con el query correcto y solo con Accept: application/json, y no lo anotó en ninguna parte. Como resultado, tiene un bonito cuerpo de respuesta, pero no tiene contrato. El contrato siempre empieza por los hechos de transporte.
Error nº2: mezclar la plantilla del escenario y el ejemplo concreto en una misma cosa.
Si ha anotado GET /books/OL12345M como «ruta» y olvidó que en realidad es una plantilla /books/{externalId}, se ha puesto una trampa a sí mismo. El mapa de contrato debe separar la «forma» del «ejemplo de ejecución». La forma es para el código, el ejemplo para la comprobación smoke y como referencia visual.
Error nº3: intentar «guardar todo lo que llegó» y convertir el mapa de contrato en un volcado de JSON.
Es psicológicamente comprensible: da miedo perder información. Pero desde el punto de vista de la ingeniería es perjudicial: se ahoga en detalles y pierde lo principal. El mapa sirve para destacar los campos clave y la forma de la respuesta. Todo lo demás vive en Postman, donde siempre puede abrir el cuerpo de la respuesta y verlo de nuevo.
Error nº4: no marcar los campos opcionales y hacer como si todo estuviera siempre presente.
Las API externas casi nunca son perfectas: en algún sitio falta el autor, en otro la descripción es null, en otro el campo inesperadamente es un array. Si no registra la observación «puede faltar», se está programando un futuro NullPointerException en su cabeza. Incluso si ahora no escribimos código, un mapa de contrato bien hecho ya debe ser honesto sobre la opcionalidad.
Error nº5: hacer que el conjunto smoke sea «creativo» en lugar de reproducible.
El conjunto smoke no es un lugar para experimentos. Debe ser simple y repetible. Cuando cada vez busca una cadena diferente o toma un id aleatorio nuevo, no puede distinguir entre «el contrato cambió» y «los datos del catálogo son otros». Smoke se trata de estabilidad y una señal rápida.
GO TO FULL VERSION