Durant quatre mòduls hem escrit l'API de la Botiga Aroma: la vam dissenyar al mòdul 2, la vam construir al 3 i la vam endurir al 4. I en tot aquest temps l'hem provada de dues maneres: amb curl a mà, escrivint capçaleres llarguíssimes a la terminal, i amb les proves automàtiques de Supertest de 03-08, que són excel·lents per al codi però no serveixen per explorar.

Falta una tercera manera de treballar, la que ocupa el dia a dia real de qui desenvolupa o consumeix una API: obrir un client, llançar una petició, mirar la resposta, canviar un paràmetre, tornar-la a llançar. I, quan alguna cosa funciona, desar-la per no haver-la de tornar a escriure mai més ni dependre que algú recordi la sintaxi exacta.

Aquesta lliçó obre el mòdul 5 amb aquesta eina. Construirem la col·lecció «Botiga Aroma v1»: un fitxer versionable, amb carpetes per recurs, entorns per a local, proves i producció, autenticació que es renova sola, assercions que comproven el contracte i peticions encadenades. I acabarem executant-la sencera des de la terminal amb Newman, que és exactament el que 05-05 connectarà a la integració contínua.

Totes les dades, dominis i credencials d'aquesta lliçó són ficticis. Cap cadena que sembli un token no és un secret real, i cap exemple no s'ha de copiar amb valors reals a dins.

Contingut

  1. Per què cal un client HTTP a més de les proves automàtiques
  2. Què és Postman i quines alternatives hi ha
  3. Instal·lació i primer contacte: GET /v1/cafes
  4. Llegir la resposta: cos, capçaleres i temps
  5. Col·leccions i carpetes: l'estructura de «Botiga Aroma v1»
  6. Les peticions reals del contracte
  7. Variables: de col·lecció, d'entorn i globals
  8. Entorns: local, proves i producció
  9. Secrets: què no s'exporta mai
  10. Autenticació: Bearer Token i herència de carpeta
  11. Scripts pre-request: el que passa abans d'enviar
  12. Scripts post-response: les assercions
  13. Encadenar peticions: desar l'id i fer-lo servir després
  14. Generar una Idempotency-Key diferent a cada enviament
  15. Assercions sobre l'esquema JSON
  16. Executar la col·lecció sencera amb el Collection Runner
  17. Fitxers de dades CSV i JSON
  18. Newman: la col·lecció des de la terminal
  19. Importar openapi.yaml i exportar la col·lecció
  20. Documentació i compartició amb l'equip
  21. El servidor mock de Postman
  22. Bones pràctiques i què va al repositori

  1. Per què cal un client HTTP a més de les proves automàtiques

Les proves de 03-08 i un client com Postman responen a preguntes diferents, i confondre-les porta a equips que tenen una cosa i troben a faltar l'altra.

Proves Supertest (03-08) Client HTTP (Postman)
Pregunta que respon Continua funcionant el que ja funcionava? Què passa si faig això?
Quan es fa servir A cada git push, sense humans Mentre desenvolupes, depures o explores
Contra què s'executa L'objecte app en memòria, sense xarxa Un servidor real, amb xarxa, TLS i proxies
Qui l'escriu Qui desenvolupa l'API També qui la consumeix
Què detecta bé Regressions lògiques Problemes de xarxa, CORS, capçaleres, desplegament
Què no detecta Que el desplegament estigui mal configurat Regressions, perquè ningú no l'executa a mà

La frase clau és l'última fila. Supertest no veuria mai que el balancejador de producció està eliminant la capçalera Aroma-Traca-Id, perquè mai no hi ha balancejador: crida app directament. I Postman no detectaria una regressió si ningú no prem el botó. Per això la meta d'aquesta lliçó no és «aprendre a prémer Send», sinó convertir l'exploració manual en un artefacte repetible i executable que, a 05-05, es premerà sol.

  1. Què és Postman i quines alternatives hi ha

Postman és un client HTTP amb interfície gràfica que desa les peticions en col·leccions: fitxers JSON amb les URL, capçaleres, cossos, variables i scripts. Aquest detall —que una col·lecció és un fitxer— és el que la converteix en una cosa més que una eina personal: es versiona a Git, es revisa en un pull request i s'executa a la integració contínua.

Aquestes són les alternatives serioses que et trobaràs en equips reals:

Eina Model Punt fort Punt feble Format de la col·lecció
Postman App d'escriptori, compte al núvol Ecosistema complet: runner, mocks, monitors, documentació Empeny cap al núvol; pesada; funcions clau al pla de pagament JSON propi (v2.1)
Insomnia App d'escriptori Lleugera, bona per a GraphQL i gRPC Ecosistema menor JSON/YAML propi
Bruno App d'escriptori, offline first Desa cada petició com a fitxer de text pla (.bru) al teu repositori; diffs llegibles Jove, menys integracions Fitxers .bru en carpetes
Hoppscotch Web (i autoallotjable) Zero instal·lació, s'obre al navegador Depèn del navegador per a CORS i certificats JSON propi
REST Client (VS Code) Extensió de l'editor Peticions en un .http al costat del codi; sense sortir de l'editor Sense runner ni informes Fitxer .http
curl Terminal És a tot arreu; és la llengua franca per compartir una fallada Verbós; sense estat entre crides Una ordre
HTTPie Terminal Sintaxi molt més llegible que curl; acoloreix JSON S'ha d'instal·lar Una ordre

Comparació pràctica de la mateixa petició en les tres formes de la terminal:

# curl: universal, verbós. Així s'enganxa una fallada en un tiquet.
curl -i -X POST http://localhost:3000/v1/comandes \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7f3c1a90-2d64-4e11-9c88-1b2f4a6d0e55" \
  -d '{"clientId":"cli_842","linies":[{"cafeId":"caf_001","quantitat":2}]}'

# HTTPie: el mateix, molt més curt de llegir
http POST localhost:3000/v1/comandes \
  "Authorization:Bearer $TOKEN" \
  "Idempotency-Key:7f3c1a90-2d64-4e11-9c88-1b2f4a6d0e55" \
  clientId=cli_842 \
  linies:='[{"cafeId":"caf_001","quantitat":2}]'

I el mateix cas com a fitxer .http de l'extensió REST Client, que té l'avantatge de viure dins del repositori al costat del codi que prova:

### Inici de sessió de la clienta Marta
# @name login
POST http://localhost:3000/v1/sessions
Content-Type: application/json

{ "email": "[email protected]", "contrasenya": "{{clauDeProva}}" }

### Crear comanda reutilitzant el token de la resposta anterior
POST http://localhost:3000/v1/comandes
Authorization: Bearer {{login.response.body.token}}
Content-Type: application/json
Idempotency-Key: 7f3c1a90-2d64-4e11-9c88-1b2f4a6d0e55

{ "clientId": "cli_842", "linies": [{ "cafeId": "caf_001", "quantitat": 2 }] }

Criteri d'elecció. Si el teu equip ja viu a Postman, queda't a Postman: el guany de canviar poques vegades compensa. Si t'importa per damunt de tot que les peticions es revisin com a codi als pull requests, Bruno o REST Client són millors perquè desen text pla llegible. Tot el que és conceptual d'aquesta lliçó —variables, entorns, encadenat, assercions, execució a CI— existeix a les quatre eines gràfiques amb un altre nom; el que canvia és la sintaxi.

Farem servir Postman perquè és l'estàndard de facto i perquè Newman ens dona l'execució a CI que necessita 05-05.

  1. Instal·lació i primer contacte: GET /v1/cafes

Postman es descarrega del seu lloc oficial per a Windows, macOS i Linux; també n'existeix una versió web, encara que per cridar localhost necessita el Postman Agent instal·lat, així que per al nostre cas convé l'aplicació d'escriptori.

Abans de res, aixeca el projecte dels mòduls 3 i 4:

cd botiga-aroma-api
npm run bd:reiniciar   # migra i sembra: caf_001, caf_002, cli_842, com_5001...
npm run dev            # node --watch src/servidor.js → http://localhost:3000

A Postman, Ctrl/Cmd + NHTTP Request. Escriu el mètode GET i la URL:

http://localhost:3000/v1/cafes?torrefaccio=clar&limit=2&ordenar=-preuEuros

Prem Send. Fixa't que Postman ha entès la cadena de consulta i ha omplert sola la pestanya Params amb una fila per paràmetre: pots activar-los i desactivar-los amb la casella, que és la manera còmoda de provar combinacions de filtres sense editar text.

La resposta que retorna la nostra API:

{
  "dades": [
    {
      "id": "caf_001",
      "nom": "Etiòpia Yirgacheffe",
      "origen": "Etiòpia",
      "torrefaccio": "clar",
      "preuEuros": 14.50,
      "estoc": 120,
      "notesTast": ["cítric", "floral", "te negre"],
      "dataCreacio": "2026-01-15T08:30:00Z",
      "_links": {
        "self": { "href": "/v1/cafes/caf_001" },
        "ressenyes": { "href": "/v1/cafes/caf_001/ressenyes" }
      }
    }
  ],
  "total": 1
}

  1. Llegir la resposta: cos, capçaleres i temps

El cos és el primer que es mira i el menys interessant per al que hem construït. A la part inferior de Postman hi ha tres dades que resumeixen mig mòdul 4:

  • Status: 200 OK.
  • Time: el temps total de la petició. Compte: inclou la resolució DNS, la connexió i el TLS, així que sempre serà més gran que la latència que mesura prom-client a /metriques (04-07). Passa-hi el ratolí per sobre per veure'n el desglossament per fases, que és la manera més ràpida de descobrir que «l'API va lenta» és en realitat «l'encaixada TLS triga 300 ms».
  • Size: la mida. Si vas activar compression a 04-06, hi veuràs la mida comprimida.

La pestanya Headers de la resposta és on es comprova la feina del mòdul 4:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
ETag: W/"a1b2c3d4e5f6"
Cache-Control: public, max-age=60
Vary: Accept-Encoding, Origin
Link: </v1/cafes?limit=2&desplacament=2&torrefaccio=clar>; rel="next",
      </v1/cafes?limit=2&desplacament=0&torrefaccio=clar>; rel="first"
Aroma-Traca-Id: 3f9a2c1e-8b47-4d2a-9e01-77c6b5d3a812
Aroma-RateLimit-Limit: 600
Aroma-RateLimit-Restants: 597
Aroma-RateLimit-Reinici: 1771065600
X-Content-Type-Options: nosniff

Quatre comprovacions que convé fer a mà la primera vegada:

  1. ETag i 304. Copia el valor de l'ETag, crea una petició idèntica amb la capçalera If-None-Match posada a aquest valor i envia-la. Ha de respondre 304 Not Modified sense cos. Postman mostra Size: 0 B al cos: aquí veus de debò l'estalvi de 04-06.
  2. Aroma-RateLimit-Restants. Prem Send deu vegades seguides i observa com baixa. És la comprovació més simple que el limitador de 04-04 està actiu en aquest entorn.
  3. Link. Copia la URL de rel="next" en una petició nova: t'ha de portar la pàgina següent sense que hagis de construir tu el desplacament. Si l'has de calcular a mà, HATEOAS no està funcionant.
  4. Aroma-Traca-Id. Copia'l i cerca'l a la sortida de pino de la teva terminal. Ha d'aparèixer a totes les línies d'aquella petició. És la correlació de 04-07 vista des de fora.

Consell. Prova també un error a propòsit: GET /v1/cafes?torrefaccio=morat. Ha de respondre 400 amb {"error":{"codi":"parametre_invalid",...}} i sense tracaId al cos, perquè només els 5xx el porten. Veure el contracte d'errors complint-se és tan important com veure el camí feliç.

  1. Col·leccions i carpetes: l'estructura de «Botiga Aroma v1»

Una petició solta es perd. El pas següent és crear la col·lecció. Al panell esquerre: Collections → + i anomena-la Botiga Aroma v1.

L'estructura que construirem reflecteix els recursos del contracte de 02-02, no la implementació:

Botiga Aroma v1/
├── 00 Sessions/
│   ├── POST Iniciar sessió (client)
│   ├── POST Iniciar sessió (administrador)
│   └── DELETE Tancar sessió
├── 01 Cafès/
│   ├── GET Llistar cafès
│   ├── GET Llistar cafès filtrats
│   ├── GET Obtenir cafè per id
│   ├── GET Obtenir cafè (If-None-Match → 304)
│   ├── POST Crear cafè
│   ├── PATCH Actualitzar cafè (merge-patch)
│   ├── DELETE Esborrar cafè
│   └── GET Ressenyes del cafè
├── 02 Comandes/
│   ├── POST Crear comanda (amb Idempotency-Key)
│   ├── GET Llistar les meves comandes
│   ├── GET Obtenir comanda
│   ├── POST Pagar comanda
│   └── POST Anul·lar comanda
├── 03 Clients/
├── 04 Ressenyes/
└── 99 Errors esperats/
    ├── GET Cafè inexistent → 404
    ├── GET Paràmetre invàlid → 400
    ├── POST Crear cafè sense token → 401
    ├── POST Crear cafè com a client → 403
    └── PUT /v1/cafes → 405 amb Allow

Tres decisions d'aquesta estructura mereixen explicació:

  • El prefix numèric (00, 01, …) no és decoratiu: el Collection Runner executa les peticions en l'ordre en què apareixen, i la carpeta 00 Sessions ha d'anar primer perquè és la que obté el token que fan servir totes les altres.
  • Una carpeta per recurs, no per cas d'ús. Els casos d'ús canvien cada trimestre; els recursos són la part estable del contracte.
  • La carpeta 99 Errors esperats és la que distingeix una col·lecció professional d'una llista de peticions. Documenta el comportament del catàleg d'errors de 02-04 i detecta la regressió més habitual: algú toca l'autorització i un 403 es converteix en 500.

  1. Les peticions reals del contracte

Aquestes són les peticions centrals tal com queden configurades. Comencem per l'inici de sessió, del qual depèn tota la resta:

POST {{urlBase}}/sessions
Content-Type: application/json

{
  "email": "[email protected]",
  "contrasenya": "{{clauClient}}"
}

Resposta esperada, 200, amb el JWT que emetem a 03-06:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.EXEMPLE.FICTICI",
  "caducaEn": 3600,
  "client": { "id": "cli_842", "nom": "Marta Garcia", "rol": "client" }
}

Llistat amb filtres, fent servir la pestanya Params per poder desactivar-los un a un:

GET {{urlBase}}/cafes?origen=Colòmbia&torrefaccio=mitja&preuMax=15&ordenar=-preuEuros&limit=20
Authorization: Bearer {{token}}

Creació d'un cafè, que exigeix rol administrador segons la matriu de permisos de 03-06:

POST {{urlBase}}/cafes
Authorization: Bearer {{tokenAdmin}}
Content-Type: application/json

{
  "nom": "Kenya Nyeri AA",
  "origen": "Kenya",
  "torrefaccio": "clar",
  "preuEuros": 16.90,
  "estoc": 40,
  "notesTast": ["grosella", "tomàquet", "cítric"]
}

Actualització parcial amb merge-patch, el format que vam fixar a 02-05. Aquí és fàcil equivocar-se: el Content-Type no és application/json, i la nostra API respon 415 amb Accept-Patch si el poses malament.

PATCH {{urlBase}}/cafes/{{cafeId}}
Authorization: Bearer {{tokenAdmin}}
Content-Type: application/merge-patch+json
If-Match: {{cafeEtag}}

{ "preuEuros": 15.90, "estoc": 55 }

I la creació de comanda, l'única —juntament amb el pagament— que exigeix Idempotency-Key:

POST {{urlBase}}/comandes
Authorization: Bearer {{token}}
Content-Type: application/json
Idempotency-Key: {{clauIdempotencia}}

{
  "clientId": "cli_842",
  "linies": [
    { "cafeId": "caf_001", "quantitat": 2 },
    { "cafeId": "caf_002", "quantitat": 1 }
  ]
}

Observa que no hi ha ni un sol valor escrit a foc: {{urlBase}}, {{token}}, {{cafeId}}, {{clauIdempotencia}}. D'això tracta l'apartat següent.

  1. Variables: de col·lecció, d'entorn i globals

Postman resol {{nom}} cercant en diversos àmbits, del més específic al més general. Entendre aquesta jerarquia evita el 80 % dels «però si jo vaig posar bé la URL».

Àmbit On viu Abast Ús correcte a la Botiga Aroma
Local (d'execució) Només durant un Runner/Newman L'execució en curs Dades del fitxer CSV
De dades Fitxer CSV/JSON del runner La iteració en curs nomCafe, preu de cada fila
D'entorn Fitxer d'entorn seleccionat Tot el que s'executi amb aquell entorn urlBase, token, clauClient
De col·lecció Dins del .json de la col·lecció Tota la col·lecció, en qualsevol entorn versio: "v1", moneda: "EUR"
Global La instal·lació de Postman Tot, totes les col·leccions Gairebé res: evita-les

Regla pràctica: si el valor canvia segons on apuntis, és d'entorn; si és igual sempre, és de col·lecció; si creus que és global, gairebé sempre t'equivoques. Les variables globals són la causa habitual de «a la meva màquina funciona»: algú té un valor a la seva instal·lació que no és al fitxer que va compartir.

Variables de col·lecció de «Botiga Aroma v1»:

{
  "variable": [
    { "key": "versio", "value": "v1" },
    { "key": "clientEmail", "value": "[email protected]" },
    { "key": "adminEmail", "value": "[email protected]" },
    { "key": "cafeIdLlavor", "value": "caf_001" },
    { "key": "comandaIdLlavor", "value": "com_5001" }
  ]
}

Els identificadors sembrats (caf_001, com_5001) van a la col·lecció perquè els garanteix el npm run sembrar de 03-05 a tots els entorns.

  1. Entorns: local, proves i producció

Un entorn és un conjunt de valors per a les mateixes claus. Es canvia amb el desplegable de la cantonada superior dreta, i tota la col·lecció apunta a un altre lloc sense tocar ni una petició.

Variable local proves producció
urlBase http://localhost:3000/v1 https://api-proves.botigaaroma.example/v1 https://api.botigaaroma.example/v1
clauClient clau-de-prova-local (secret) (no es defineix)
token (buida, l'omple l'script) (buida) (buida)
permetEscriptura true true false

Fitxer d'entorn local, exportable i versionable perquè no conté res sensible:

{
  "name": "Botiga Aroma — local",
  "values": [
    { "key": "urlBase", "value": "http://localhost:3000/v1", "type": "default", "enabled": true },
    { "key": "clauClient", "value": "clau-de-prova-local", "type": "default", "enabled": true },
    { "key": "clauAdmin", "value": "clau-admin-local", "type": "default", "enabled": true },
    { "key": "token", "value": "", "type": "secret", "enabled": true },
    { "key": "tokenAdmin", "value": "", "type": "secret", "enabled": true },
    { "key": "permetEscriptura", "value": "true", "type": "default", "enabled": true }
  ]
}

permetEscriptura no és un caprici. És el fre de mà que evita la pitjor història possible amb un client HTTP: executar la col·lecció sencera contra producció i crear quaranta cafès de prova al catàleg real. A l'script pre-request de la carpeta d'escriptura:

// Pre-request de les carpetes "01 Cafès" i "02 Comandes"
// Avorta qualsevol mètode d'escriptura si l'entorn no ho permet.
const metode = pm.request.method;
const escriu = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(metode);
const permes = pm.environment.get('permetEscriptura') === 'true';

if (escriu && !permes) {
  throw new Error(
    `Bloquejat: ${metode} no està permès a l'entorn "${pm.environment.name}".`
  );
}

  1. Secrets: què no s'exporta mai

Postman distingeix el tipus d'una variable: default (text pla) o secret (es mostra emmascarada i no s'inclou en exportar l'entorn ni en compartir-lo).

Regles per a la Botiga Aroma:

  • La contrasenya real de qualsevol compte, el client_secret d'OAuth de 04-03 i qualsevol token: sempre secret.
  • Cap token no s'escriu a mà. El token és un valor derivat: el produeix l'inici de sessió i el desa un script. Si l'enganxes a mà, en una hora caduca i el tornes a enganxar, i així fins que algú el commiteja.
  • Les claus de l'entorn «producció» no es desen al fitxer: s'omplen al moment o, encara millor, no existeix un entorn de producció amb permisos d'escriptura a la col·lecció compartida.
  • El fitxer exportat es revisa abans de pujar-lo al repositori. Un grep ràpid evita disgustos:
# Abans de commitejar qualsevol fitxer de Postman
grep -iE '"value": "(eyJ|sk_|ghp_|AKIA)' postman/*.json && echo "ALTO! hi ha un secret" || echo "net"

  1. Autenticació: Bearer Token i herència de carpeta

Posar Authorization: Bearer {{token}} a mà en trenta peticions és garantia que tres es quedaran sense. La pestanya Authorization existeix per a això i funciona per herència:

  1. A la col·lecció: Auth Type → Bearer Token, Token → {{token}}.
  2. A cada petició: Auth Type → Inherit auth from parent (el valor per defecte).
  3. A la carpeta 00 Sessions i al POST /v1/clients de registre: Auth Type → No Auth, perquè són públiques i enviar un token caducat a POST /v1/sessions és un soroll innecessari.
  4. A les peticions d'administració: Bearer Token → {{tokenAdmin}}, sobreescrivint l'herència.

La carpeta 99 Errors esperats mereix atenció: la petició «Crear cafè sense token → 401» ha d'estar en No Auth explícit, i la de «Crear cafè com a client → 403» en Bearer amb {{token}} (el de la Marta, rol client). Si totes dues hereten el mateix, una de les dues no prova el que diu que prova.

Postman també admet el flux OAuth 2.0 complet de 04-03: a Auth Type → OAuth 2.0 pots configurar Authorization Code amb PKCE, i Postman obre el navegador, fa l'intercanvi i desa el token. És la manera correcta de provar la integració de CataBox, i de comprovar de debò que un token amb només l'àmbit cafes.llegir rep 403 permisos_insuficients en intentar POST /v1/comandes.

  1. Scripts pre-request: el que passa abans d'enviar

Cada petició, carpeta i col·lecció té dos scripts en JavaScript: Pre-request (abans d'enviar) i Post-response (en rebre; en versions anteriors de Postman es deia «Tests»). S'executen en cascada: primer el de la col·lecció, després el de la carpeta i finalment el de la petició.

L'ús més útil del pre-request a la nostra API és renovar el token si ha caducat, perquè la col·lecció funcioni encara que faci dues hores que no la toques. A l'script pre-request de la col·lecció:

// Pre-request de la col·lecció "Botiga Aroma v1"
// Si no hi ha token o està a punt de caducar, inicia sessió abans de continuar.

const ara = Date.now();
const caduca = Number(pm.environment.get('tokenCaducaEn') || 0);
const margeMs = 60 * 1000; // renovem un minut abans: evita el 401 per cursa

if (caduca - margeMs > ara) {
  return; // el token continua sent vàlid, no fem res
}

pm.sendRequest({
  url: `${pm.environment.get('urlBase')}/sessions`,
  method: 'POST',
  header: { 'Content-Type': 'application/json' },
  body: {
    mode: 'raw',
    raw: JSON.stringify({
      email: pm.collectionVariables.get('clientEmail'),
      contrasenya: pm.environment.get('clauClient'),
    }),
  },
}, (error, resposta) => {
  if (error) {
    throw new Error(`No s'ha pogut renovar el token: ${error}`);
  }
  if (resposta.code !== 200) {
    throw new Error(`Inici de sessió fallit (${resposta.code}): ${resposta.text()}`);
  }
  const cos = resposta.json();
  pm.environment.set('token', cos.token);
  // caducaEn ve en segons (03-06); el convertim a marca de temps absoluta
  pm.environment.set('tokenCaducaEn', Date.now() + cos.caducaEn * 1000);
  console.log('Token renovat automàticament.');
});

Punts que convé entendre d'aquest script:

  • pm.sendRequest és asíncron amb callback. Postman espera que acabi abans d'enviar la petició principal, però qualsevol pm.environment.set que facis fora del callback s'executarà abans d'hora.
  • El marge d'un minut evita la cursa clàssica: el token és vàlid quan l'script ho comprova i ha caducat quan arriba al servidor.
  • Llançar un Error atura l'execució amb un missatge clar en lloc de deixar-te investigant per què tot retorna 401.
  • console.log escriu a la Postman Console (Ctrl/Cmd + Alt + C), que a més mostra la petició HTTP exacta que s'ha enviat, capçaleres incloses. És l'eina de depuració número u i gairebé ningú no l'obre.

  1. Scripts post-response: les assercions

Aquí és on la col·lecció deixa de ser documentació i es converteix en una prova. L'API és pm.test(nom, funcio), i a dins es fa servir pm.expect, que és Chai.

Post-response de POST /v1/cafes:

// --- Estat i capçaleres --------------------------------------------------
pm.test('Respon 201 Created', () => {
  pm.response.to.have.status(201);
});

pm.test('Retorna Location apuntant al recurs creat', () => {
  const location = pm.response.headers.get('Location');
  pm.expect(location, 'falta la capçalera Location').to.be.a('string');
  // El contracte de 02-04: /v1/cafes/{id} amb id opac amb prefix caf_
  pm.expect(location).to.match(/^\/v1\/cafes\/caf_[A-Za-z0-9]+$/);
});

pm.test('Retorna ETag per a la concurrència optimista', () => {
  pm.expect(pm.response.headers.get('ETag')).to.be.a('string');
});

pm.test('El Content-Type és JSON', () => {
  pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json');
});

// --- Cos -----------------------------------------------------------------
const cos = pm.response.json();

pm.test('El cos retorna el recurs creat, no un embolcall', () => {
  pm.expect(cos).to.have.property('id');
  pm.expect(cos).to.not.have.property('dades'); // això és per a col·leccions
});

pm.test('El preu se serialitza en euros amb dos decimals', () => {
  pm.expect(cos.preuEuros).to.be.a('number');
  // La regla de 02-05: fora euros, dins cèntims. Mai no ha de sortir 1690.
  pm.expect(cos.preuEuros).to.equal(16.90);
});

pm.test('La data és ISO-8601 en UTC amb Z', () => {
  pm.expect(cos.dataCreacio).to.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/);
});

pm.test('Inclou _links amb self', () => {
  pm.expect(cos._links.self.href).to.equal(`/v1/cafes/${cos.id}`);
});

pm.test('Respon en menys de 500 ms', () => {
  pm.expect(pm.response.responseTime).to.be.below(500);
});

Post-response de GET /v1/cafes, on el que es comprova és la forma de la col·lecció i la paginació de 02-06:

const cos = pm.response.json();

pm.test('Respon 200', () => pm.response.to.have.status(200));

pm.test('La col·lecció té la forma {dades, total}', () => {
  pm.expect(cos).to.have.all.keys('dades', 'total');
  pm.expect(cos.dades).to.be.an('array');
  pm.expect(cos.total).to.be.a('number');
});

pm.test('Respecta el límit sol·licitat', () => {
  const limit = Number(pm.request.url.query.get('limit') || 20);
  pm.expect(cos.dades.length).to.be.at.most(limit);
});

pm.test('El filtre torrefaccio=clar s\'aplica de debò', () => {
  // Una asserció d'estat sense comprovar el filtre deixa passar el pitjor bug:
  // que el filtre s'ignori silenciosament i es retorni tot el catàleg.
  cos.dades.forEach((cafe) => pm.expect(cafe.torrefaccio).to.equal('clar'));
});

pm.test('L\'ordre descendent per preu es respecta', () => {
  const preus = cos.dades.map((c) => c.preuEuros);
  const ordenats = [...preus].sort((a, b) => b - a);
  pm.expect(preus).to.eql(ordenats);
});

pm.test('Exposa les capçaleres de límit de peticions', () => {
  pm.expect(pm.response.headers.has('Aroma-RateLimit-Restants')).to.be.true;
});

pm.test('Emet Link amb rel=next quan hi ha més pàgines', () => {
  if (cos.total > cos.dades.length) {
    pm.expect(pm.response.headers.get('Link')).to.include('rel="next"');
  }
});

I l'error esperat de la carpeta 99, tan important com el camí feliç:

// Post-response de "GET Cafè inexistent → 404"
pm.test('Respon 404', () => pm.response.to.have.status(404));

pm.test('Compleix el format d\'error del catàleg', () => {
  const { error } = pm.response.json();
  pm.expect(error.codi).to.equal('cafe_no_trobat');
  pm.expect(error.missatge).to.be.a('string').and.not.empty;
  pm.expect(error.detalls).to.be.an('array');
});

pm.test('No filtra tracaId en un 4xx', () => {
  // Només els 5xx porten tracaId (03-07). Si apareix aquí, hi ha una fuita de conveni.
  pm.expect(pm.response.json().error).to.not.have.property('tracaId');
});

  1. Encadenar peticions: desar l'id i fer-lo servir després

L'encadenat és el que converteix una llista de peticions en un recorregut. La tècnica és sempre la mateixa: una petició desa un valor en una variable, la següent el consumeix.

Al post-response de POST /v1/cafes:

// Desem l'id i l'ETag per a les peticions següents de la carpeta.
if (pm.response.code === 201) {
  const cos = pm.response.json();
  pm.collectionVariables.set('cafeId', cos.id);
  pm.collectionVariables.set('cafeEtag', pm.response.headers.get('ETag'));
  pm.collectionVariables.set('cafeVersio', cos.versio);
}

Ara PATCH {{urlBase}}/cafes/{{cafeId}} amb If-Match: {{cafeEtag}} funciona sense tocar res, i el DELETE posterior també. Detall important: després del PATCH, l'ETag canvia (la versió ha pujat), així que el post-response del PATCH l'ha de tornar a desar, o el DELETE rebrà el 412 conflicte_versio de 04-06. És exactament el mateix error que cometria un client real, i descobrir-lo aquí és barat.

Aquest és el recorregut complet que executa la col·lecció:

sequenceDiagram
    participant P as Postman
    participant A as API Botiga Aroma
    P->>A: POST /v1/sessions amb correu i clau
    A-->>P: 200 amb el token
    Note over P: desa les variables token i tokenCaducaEn
    P->>A: POST /v1/cafes com a administrador
    A-->>P: 201 amb Location i ETag
    Note over P: desa les variables cafeId i cafeEtag
    P->>A: PATCH /v1/cafes/cafeId amb If-Match
    A-->>P: 200 amb un ETag nou
    Note over P: actualitza la variable cafeEtag
    P->>A: POST /v1/comandes amb Idempotency-Key
    A-->>P: 201 amb l'id de la comanda
    Note over P: desa la variable comandaId
    P->>A: POST /v1/comandes/comandaId/pagament
    A-->>P: 200 amb estat pagat
    P->>A: DELETE /v1/cafes/cafeId de neteja
    A-->>P: 204

L'última petició és la que gairebé tothom oblida: netejar el que s'ha creat. Sense ella, cada execució deixa un cafè nou a la base de dades i la col·lecció deixa de ser repetible.

Hi ha una alternativa a l'ordre fix que convé conèixer: postman.setNextRequest('nom de la petició') permet saltar en l'execució del runner, per exemple per ometre la resta d'una carpeta si l'inici de sessió ha fallat. Fes-ho servir amb moderació: col·leccions amb salts per tot arreu són il·legibles.

  1. Generar una Idempotency-Key diferent a cada enviament

POST /v1/comandes exigeix Idempotency-Key (02-03 i 04-01). Si l'escrius fixa, el segon enviament retornarà la mateixa resposta que el primer en lloc de crear una comanda nova, que és precisament el que la capçalera promet. I si canvies el cos mantenint la clau, rebràs 409 clau_idempotencia_reutilitzada.

Postman té variables dinàmiques incorporades: n'hi ha prou amb posar {{$guid}} com a valor de la capçalera. Però convé generar-la al pre-request per poder reutilitzar el mateix valor a la prova de reintent:

// Pre-request de "POST Crear comanda"
const clau = pm.variables.replaceIn('{{$guid}}'); // UUID v4 generat per Postman
pm.collectionVariables.set('clauIdempotencia', clau);
console.log('Idempotency-Key d\'aquesta execució:', clau);

I una segona petició idèntica, «Crear comanda (reintent)», que no regenera la clau i comprova el contracte d'idempotència:

// Post-response de "POST Crear comanda (reintent)"
pm.test('El reintent amb la mateixa clau no crea una comanda nova', () => {
  pm.response.to.have.status(201); // es repeteix la resposta original
  pm.expect(pm.response.json().id).to.equal(pm.collectionVariables.get('comandaId'));
});

Altres variables dinàmiques útils: {{$timestamp}} (segons Unix), {{$randomInt}}, {{$isoTimestamp}}, {{$randomEmail}}, {{$randomFullName}}. Serveixen per crear clients de prova sense col·lisions de correu.

  1. Assercions sobre l'esquema JSON

Comprovar propietat a propietat es torna insostenible. Postman inclou AJV, així que pots validar la resposta completa contra un JSON Schema:

const esquemaCafe = {
  type: 'object',
  required: ['id', 'nom', 'origen', 'torrefaccio', 'preuEuros', 'estoc', 'dataCreacio'],
  additionalProperties: true, // tolerem camps nous: 02-07, compatibilitat cap endavant
  properties: {
    id: { type: 'string', pattern: '^caf_[A-Za-z0-9]+$' },
    nom: { type: 'string', minLength: 1, maxLength: 120 },
    origen: { type: 'string' },
    torrefaccio: { type: 'string', enum: ['clar', 'mitja', 'fosc'] },
    preuEuros: { type: 'number', minimum: 0 },
    estoc: { type: 'integer', minimum: 0 },
    notesTast: { type: 'array', items: { type: 'string' } },
    dataCreacio: { type: 'string', format: 'date-time' },
    versio: { type: 'integer', minimum: 1 },
  },
};

const esquemaColleccio = {
  type: 'object',
  required: ['dades', 'total'],
  properties: {
    dades: { type: 'array', items: esquemaCafe },
    total: { type: 'integer', minimum: 0 },
  },
};

pm.test('La resposta compleix l\'esquema de col·lecció de cafès', () => {
  pm.response.to.have.jsonSchema(esquemaColleccio);
});

additionalProperties: true és deliberat: si demà afegim paisTorrefaccio al recurs, la col·lecció no s'ha de trencar. Aquesta és la regla de compatibilitat cap endavant de 02-07 aplicada a les proves.

Duplicar aquests esquemes a mà és tediós i es desincronitza. El correcte és que surtin de l'openapi.yaml, i això és exactament el que farà 05-04 amb les proves de contracte; aquí queda com la solució ràpida i suficient per a l'exploració.

  1. Executar la col·lecció sencera amb el Collection Runner

Botó dret sobre la col·lecció → Run collection. El runner executa totes les peticions en ordre i mostra un informe amb les assercions que passen i les que no.

Opcions que importen:

  • Iterations: quantes vegades es repeteix la col·lecció sencera (amb un fitxer de dades, una per fila).
  • Delay: mil·lisegons entre peticions. Amb el rate limiting de 04-04 actiu, un runner sense delay pot menjar-se la quota i provocar 429 a mitja execució. 50-100 ms sol bastar.
  • Keep variable values: si es persisteixen els valors que van escriure els scripts. Activa'l mentre depures, desactiva'l per comprovar que la col·lecció funciona des de zero.
  • Run manually / Automatically: l'execució pas a pas és utilíssima per depurar un encadenat trencat.

  1. Fitxers de dades CSV i JSON

Per provar diversos cafès sense duplicar peticions, el runner accepta un fitxer de dades; cada fila és una iteració i les seves columnes es converteixen en variables.

postman/dades-cafes.csv:

nom,origen,torrefaccio,preuEuros,estoc,esperat
Kenya Nyeri AA,Kenya,clar,16.90,40,201
Brasil Cerrado,Brasil,fosc,9.50,200,201
Cafè sense origen,,mitja,11.00,10,400
Preu negatiu,Perú,mitja,-3.00,10,400
Torrefacció invàlida,Perú,morat,11.00,10,400

El cos de la petició fa servir les columnes com a variables, i observa el detall de les cometes: {{preuEuros}} va sense cometes perquè és un número, i {{nom}} amb elles perquè és text.

{
  "nom": "{{nom}}",
  "origen": "{{origen}}",
  "torrefaccio": "{{torrefaccio}}",
  "preuEuros": {{preuEuros}},
  "estoc": {{estoc}}
}

La columna esperat permet que una sola petició validi casos vàlids i invàlids:

const esperat = Number(pm.iterationData.get('esperat'));

pm.test(`Respon ${esperat} per a "${pm.iterationData.get('nom')}"`, () => {
  pm.response.to.have.status(esperat);
});

if (esperat === 400) {
  pm.test('El 400 fa servir el codi dades_invalides amb detalls', () => {
    const { error } = pm.response.json();
    pm.expect(error.codi).to.equal('dades_invalides');
    pm.expect(error.detalls).to.be.an('array').that.is.not.empty;
    pm.expect(error.detalls[0]).to.have.property('camp');
  });
}

Això prova de cop la validació amb Zod de 03-04 i el format d'error de 03-07, amb cinc línies de CSV en lloc de cinc peticions.

  1. Newman: la col·lecció des de la terminal

Newman és l'executor de col·leccions per línia d'ordres. És la peça que fa que tot l'anterior serveixi per a alguna cosa més que per a tu.

Exporta primer els fitxers (botó dret → Export, format Collection v2.1) a la carpeta postman/ del repositori:

botiga-aroma-api/
└── postman/
    ├── botiga-aroma-v1.postman_collection.json
    ├── local.postman_environment.json
    ├── proves.postman_environment.json
    └── dades-cafes.csv

I executa:

# Execució bàsica amb l'entorn local
npx newman run postman/botiga-aroma-v1.postman_collection.json \
  -e postman/local.postman_environment.json

# Amb fitxer de dades, delay per no xocar amb el rate limiting,
# i un informe HTML a més del resum a pantalla
npx newman run postman/botiga-aroma-v1.postman_collection.json \
  -e postman/proves.postman_environment.json \
  -d postman/dades-cafes.csv \
  --delay-request 100 \
  --reporters cli,junit,htmlextra \
  --reporter-junit-export informes/newman.xml \
  --reporter-htmlextra-export informes/newman.html \
  --bail

# Només una carpeta: útil per a una prova de fum ràpida després de desplegar
npx newman run postman/botiga-aroma-v1.postman_collection.json \
  -e postman/proves.postman_environment.json \
  --folder "99 Errors esperats"
Opció Per a què serveix
-e Fitxer d'entorn
-d Fitxer de dades CSV/JSON
--folder Executa només una carpeta
--env-var clau=valor Injecta una variable sense escriure-la al fitxer: així entren els secrets a CI
--delay-request Pausa entre peticions
--bail S'atura a la primera fallada
--reporters Formats d'informe; junit és el que entenen els sistemes de CI
--insecure Accepta certificats autosignats (només per a preproducció interna)

El punt clau per a 05-05: Newman retorna un codi de sortida diferent de zero si alguna asserció falla. Això és tot el que necessita una canalització de CI per bloquejar un desplegament.

Afegeix-lo com a script al package.json:

{
  "scripts": {
    "proves:api": "newman run postman/botiga-aroma-v1.postman_collection.json -e postman/local.postman_environment.json --delay-request 50",
    "proves:fum": "newman run postman/botiga-aroma-v1.postman_collection.json -e postman/proves.postman_environment.json --folder \"00 Sessions\" --bail"
  }
}

I newman amb newman-reporter-htmlextra van a devDependencies.

  1. Importar openapi.yaml i exportar la col·lecció

No cal crear les peticions a mà si ja tens el contracte. Import → File → openapi.yaml genera una col·lecció completa amb totes les rutes, paràmetres, cossos d'exemple i carpetes per tag.

Avantatges i inconvenients, perquè no és màgia:

  • A favor: cobertura instantània de tots els endpoints, cossos preemplenats amb els example de l'especificació, i una comprovació indirecta que l'openapi.yaml descriu el que creus.
  • En contra: no porta scripts, ni encadenat, ni assercions —el que dona valor a la col·lecció—, i reimportar sobreviu malament: Postman crea una col·lecció nova en lloc de fusionar, i perds els teus scripts.

Estratègia pràctica: importa una vegada per tenir l'esquelet, afegeix-hi al damunt scripts i encadenat, i a partir d'aquí mantén la col·lecció a mà. Quan el contracte afegeixi un endpoint nou, s'importa a una col·lecció temporal i es copia la petició que falti. La sincronització de debò entre contracte i implementació no es resol aquí: es resol amb les proves de contracte de 05-04.

  1. Documentació i compartició amb l'equip

Postman genera documentació navegable a partir de la col·lecció: descripcions en Markdown de cada petició i carpeta, paràmetres, i exemples de codi en curl, JavaScript, Python o Go generats automàticament.

Dos hàbits que multipliquen el seu valor:

  1. Desar exemples de resposta. A cada petició, botó «Save as Example» després d'una resposta bona. Desa almenys el cas d'èxit i un d'error per endpoint. Els exemples són el que es veu a la documentació i, a més, el que serveix al servidor mock de l'apartat següent.
  2. Escriure la descripció de cada carpeta explicant quin rol cal, quines precondicions té i quins errors esperar. És documentació que viu on es fa servir.

Formes de compartir, de menys a més compromesa: exportar el JSON i posar-lo al repositori (la que recomanem, perquè es revisa en pull requests i no depèn de comptes); publicar la documentació com a enllaç públic; o fer servir els workspaces d'equip amb sincronització al núvol, que són còmodes però impliquen que el contingut de les teves col·leccions viu en un servidor de tercers —revisa-ho amb seguretat abans de posar-hi una API interna—.

Per a consumidors externs com CataBox, la documentació de Postman és una opció, però el destí natural és el portal de desenvolupador de 05-06, alimentat per l'openapi.yaml de 05-02.

  1. El servidor mock de Postman

Postman pot aixecar una URL pública que respon amb els exemples desats de la teva col·lecció. Serveix perquè l'equip de la SPA comenci a maquetar la pantalla del catàleg abans que existeixi l'endpoint.

https://a1b2c3d4-1111-2222-3333-444455556666.mock.pstmn.io/v1/cafes

Es crea en tres clics i té dos límits importants: respon exemples fixos, sense lògica —el filtre ?torrefaccio=clar no filtra res—, i depèn del núvol de Postman.

És una de diverses opcions, i no la millor si ja tens contracte: Prism genera el mock directament des d'openapi.yaml, sense exemples per mantenir a part. Ho veurem a fons a 05-04, juntament amb msw i nock.

Errors Comuns i Consells

  • Escriure la URL completa a cada petició. El dia que apareix un entorn de preproducció s'han d'editar quaranta peticions. {{urlBase}} des de la primera, sempre.
  • Enganxar el token a mà. Caduca en una hora i acabes amb tokens a la col·lecció exportada, és a dir, al repositori. El token s'obté amb l'inici de sessió i es desa amb un script.
  • Exportar l'entorn amb secrets a dins. Marca les variables sensibles com a secret, revisa el fitxer abans de commitejar i afegeix postman/*.local.json al .gitignore.
  • Assercions que només comproven l'estat. pm.response.to.have.status(200) passa encara que l'API retorni un array buit perquè el filtre s'ha ignorat. Comprova també la forma i el contingut.
  • Oblidar la neteja. Cada execució deixa dades. Acaba cada carpeta amb el DELETE del que ha creat, o sembra la base de dades abans amb npm run bd:reiniciar.
  • Confondre variables d'entorn amb variables de col·lecció. Si urlBase és a la col·lecció, el desplegable d'entorns no fa res i acabes apuntant sempre al mateix lloc.
  • Executar el runner sense delay contra un entorn amb rate limiting. Fallades aleatòries 429 que semblen bugs de l'API i no ho són. --delay-request 100, o una quota específica per al client de CI (04-04).
  • Content-Type: application/json al PATCH. La nostra API respon 415 format_no_suportat amb Accept-Patch. El valor correcte és application/merge-patch+json.
  • No obrir la Postman Console. Allà es veu la petició literal enviada, amb les variables ja substituïdes. La majoria dels «no entenc què passa» es resolen en deu segons mirant-la.
  • Consell: anomena les peticions pel que proven, no pel mètode. «Crear cafè com a client → 403» diu molt més que «POST cafes 2».
  • Consell: posa la col·lecció al mateix repositori que l'API. Així el pull request que canvia un endpoint canvia també la seva petició, i la revisió detecta les incoherències.

Exercicis

Exercici 1: la carpeta d'errors esperats

Construeix la carpeta 99 Errors esperats de la col·lecció «Botiga Aroma v1» amb cinc peticions que verifiquin el catàleg de 02-04, i escriu-ne les assercions. Els casos: cafè inexistent (404 cafe_no_trobat), paràmetre de consulta invàlid (400 parametre_invalid), creació sense token (401 no_autenticat amb WWW-Authenticate), creació amb rol client (403 permisos_insuficients) i PUT /v1/cafes (405 metode_no_permes amb Allow).

Indica per a cadascuna quina configuració d'Authorization necessita i escriu l'script post-response d'almenys tres d'elles.

Exercici 2: recorregut de compra encadenat

Crea una carpeta 10 Recorregut de compra que executi, en ordre i sense intervenció manual: iniciar sessió com la Marta → llistar cafès (desant l'id del primer amb estoc disponible) → crear comanda amb Idempotency-Key generada → pagar la comanda → consultar la comanda i comprovar que el seu estat és pagat.

Escriu els scripts necessaris i explica quines variables es passen entre peticions i en quin àmbit les desaries.

Exercici 3: Newman amb dades i porta de qualitat

Prepara l'execució de la col·lecció a la terminal perquè serveixi com a porta de qualitat abans d'un desplegament: un fitxer de dades amb almenys dos casos vàlids i dos d'invàlids de creació de cafè, l'ordre de Newman que l'executa contra l'entorn de proves injectant la contrasenya sense escriure-la a cap fitxer, i l'script del package.json. Explica com sabrà el sistema de CI si ha de bloquejar el desplegament.

Solucions

Solució 1

Configuració d'autenticació per petició:

Petició Authorization Motiu
Cafè inexistent → 404 Inherit ({{token}}) Cal estar autenticat per arribar al 404 i no quedar-se al 401
Paràmetre invàlid → 400 Inherit ({{token}}) Igual: la validació passa després d'autenticar
Sense token → 401 No Auth explícit Si hereta el Bearer, la prova no prova res
Com a client → 403 Bearer {{token}} (rol client) Ha d'estar autenticat però sense permís
PUT /v1/cafes → 405 Inherit El router.all respon abans de la lògica

Petició 3 — POST {{urlBase}}/cafes sense token:

pm.test('Respon 401', () => pm.response.to.have.status(401));

pm.test('Inclou WWW-Authenticate', () => {
  const capcalera = pm.response.headers.get('WWW-Authenticate');
  pm.expect(capcalera).to.include('Bearer');
});

pm.test('El codi és no_autenticat', () => {
  pm.expect(pm.response.json().error.codi).to.equal('no_autenticat');
});

pm.test('El missatge no revela si el recurs existeix', () => {
  // 04-02: un 401 no ha de filtrar informació sobre l'estat del sistema
  pm.expect(pm.response.json().error.missatge.toLowerCase()).to.not.include('cafè');
});

Petició 4 — POST {{urlBase}}/cafes amb el token de la Marta (rol client):

pm.test('Respon 403, no 401', () => {
  // Distinció clau de 02-04: 401 és "no sé qui ets", 403 és "sé qui ets i no pots"
  pm.response.to.have.status(403);
});

pm.test('El codi és permisos_insuficients', () => {
  pm.expect(pm.response.json().error.codi).to.equal('permisos_insuficients');
});

pm.test('No s\'ha creat res', () => {
  pm.expect(pm.response.headers.has('Location')).to.be.false;
});

Petició 5 — PUT {{urlBase}}/cafes:

pm.test('Respon 405', () => pm.response.to.have.status(405));

pm.test('Declara els mètodes permesos a Allow', () => {
  const allow = pm.response.headers.get('Allow');
  pm.expect(allow, 'falta la capçalera Allow, obligatòria en un 405').to.be.a('string');
  ['GET', 'POST', 'HEAD', 'OPTIONS'].forEach((metode) => {
    pm.expect(allow).to.include(metode);
  });
  pm.expect(allow).to.not.include('PUT');
});

pm.test('El codi és metode_no_permes', () => {
  pm.expect(pm.response.json().error.codi).to.equal('metode_no_permes');
});

Solució 2

Àmbits triats: totes les variables del recorregut van a pm.collectionVariables, no a pm.environment. Motiu: són valors efímers d'una execució concreta i no han d'embrutar el fitxer d'entorn que es comparteix. El token és l'excepció: va a l'entorn perquè el comparteixen totes les carpetes i el gestiona el pre-request de la col·lecció.

Petició 1 — POST {{urlBase}}/sessions (No Auth). Post-response:

pm.test('Inici de sessió correcte', () => pm.response.to.have.status(200));
const cos = pm.response.json();
pm.environment.set('token', cos.token);
pm.environment.set('tokenCaducaEn', Date.now() + cos.caducaEn * 1000);
pm.collectionVariables.set('clientId', cos.client.id);

pm.test('El rol és client', () => pm.expect(cos.client.rol).to.equal('client'));

Petició 2 — GET {{urlBase}}/cafes?disponible=true&limit=5:

const cos = pm.response.json();
pm.test('Hi ha almenys un cafè disponible', () => {
  pm.expect(cos.dades.length).to.be.above(0);
});

const cafe = cos.dades.find((c) => c.estoc >= 2);
if (!cafe) {
  throw new Error('No hi ha cap cafè amb estoc suficient: sembra la base de dades.');
}
pm.collectionVariables.set('cafeId', cafe.id);
pm.collectionVariables.set('cafePreu', cafe.preuEuros);

Petició 3 — POST {{urlBase}}/comandes. Pre-request:

pm.collectionVariables.set('clauIdempotencia', pm.variables.replaceIn('{{$guid}}'));

Cos i post-response:

{ "clientId": "{{clientId}}", "linies": [{ "cafeId": "{{cafeId}}", "quantitat": 2 }] }
pm.test('Comanda creada', () => pm.response.to.have.status(201));
const comanda = pm.response.json();
pm.collectionVariables.set('comandaId', comanda.id);

pm.test('L\'estat inicial és pendent_pagament', () => {
  pm.expect(comanda.estat).to.equal('pendent_pagament');
});

pm.test('El total coincideix amb preu × quantitat', () => {
  // Comprova de passada la conversió cèntims→euros del mapejador de 03-03
  const esperat = Number((pm.collectionVariables.get('cafePreu') * 2).toFixed(2));
  pm.expect(comanda.totalEuros).to.equal(esperat);
});

pm.test('Location apunta a la comanda creada', () => {
  pm.expect(pm.response.headers.get('Location')).to.equal(`/v1/comandes/${comanda.id}`);
});

Petició 4 — POST {{urlBase}}/comandes/{{comandaId}}/pagament amb Idempotency-Key: {{$guid}}:

pm.test('Pagament acceptat', () => pm.response.to.have.status(200));
pm.test('La comanda passa a pagat', () => {
  pm.expect(pm.response.json().estat).to.equal('pagat');
});

Petició 5 — GET {{urlBase}}/comandes/{{comandaId}}:

pm.test('L\'estat persistit és pagat', () => {
  pm.expect(pm.response.json().estat).to.equal('pagat');
});

pm.test('L\'estoc del cafè ha baixat', () => {
  pm.sendRequest({
    url: `${pm.environment.get('urlBase')}/cafes/${pm.collectionVariables.get('cafeId')}`,
    method: 'GET',
    header: { Authorization: `Bearer ${pm.environment.get('token')}` },
  }, (err, res) => {
    pm.expect(res.json().estoc).to.be.at.most(120 - 2);
  });
});

Variables que viatgen: token (entorn) → totes; clientId i cafeId (col·lecció) → petició 3; comandaId (col·lecció) → peticions 4 i 5; clauIdempotencia (col·lecció) → petició 3.

Solució 3

Fitxer postman/dades-cafes.csv:

nom,origen,torrefaccio,preuEuros,estoc,esperat,codiError
Kenya Nyeri AA,Kenya,clar,16.90,40,201,
Brasil Cerrado,Brasil,fosc,9.50,200,201,
Sense nom,,mitja,11.00,10,400,dades_invalides
Torrefacció invàlida,Perú,morat,11.00,10,400,dades_invalides

Post-response que cobreix tots dos casos:

const esperat = Number(pm.iterationData.get('esperat'));
const codiError = pm.iterationData.get('codiError');

pm.test(`"${pm.iterationData.get('nom')}" respon ${esperat}`, () => {
  pm.response.to.have.status(esperat);
});

if (esperat === 201) {
  // Desem l'id per poder netejar al final del recorregut
  const creats = JSON.parse(pm.collectionVariables.get('cafesCreats') || '[]');
  creats.push(pm.response.json().id);
  pm.collectionVariables.set('cafesCreats', JSON.stringify(creats));
} else {
  pm.test(`El codi d'error és ${codiError}`, () => {
    pm.expect(pm.response.json().error.codi).to.equal(codiError);
  });
}

Ordre amb el secret injectat des de fora:

npx newman run postman/botiga-aroma-v1.postman_collection.json \
  -e postman/proves.postman_environment.json \
  -d postman/dades-cafes.csv \
  --env-var "clauClient=$CLAU_CLIENT_PROVES" \
  --env-var "clauAdmin=$CLAU_ADMIN_PROVES" \
  --delay-request 100 \
  --reporters cli,junit \
  --reporter-junit-export informes/newman.xml

--env-var sobreescriu el valor del fitxer d'entorn. Així el fitxer versionat pot tenir clauClient buida i el valor real arriba d'una variable d'entorn que a CI prové del gestor de secrets (05-05), mai del repositori.

Script al package.json:

{
  "scripts": {
    "proves:api:proves": "newman run postman/botiga-aroma-v1.postman_collection.json -e postman/proves.postman_environment.json -d postman/dades-cafes.csv --delay-request 100 --reporters cli,junit --reporter-junit-export informes/newman.xml"
  }
}

Com bloqueja el desplegament. Newman surt amb codi 0 si totes les assercions passen i amb un codi diferent de zero si alguna falla o hi ha un error de xarxa. Qualsevol sistema de CI interpreta un codi de sortida diferent de zero com a pas fallit i atura la canalització. A més, l'informe junit permet que la interfície mostri exactament quina asserció ha fallat, sense haver de llegir el log. Si es vol que la fallada sigui immediata en lloc d'executar tota la col·lecció, s'hi afegeix --bail.

Conclusió

Has convertit l'exploració manual en un artefacte. La col·lecció «Botiga Aroma v1» ja no és una llista d'URL: té carpetes per recurs que reflecteixen el contracte de 02-02, entorns que permeten apuntar a local, proves o producció sense editar ni una petició, un fre de mà que impedeix escriure a producció per accident, i un pre-request que renova el token de l'inici de sessió de 03-06 abans que caduqui, sense que ningú enganxi credencials a mà.

Damunt d'aquesta base hi has posat el que la converteix en una prova: assercions que verifiquen el 201 i el seu Location, la forma {dades, total} de les col·leccions, que els filtres de 02-06 filtrin de debò, que el preu surti en euros i no en cèntims, que els errors del catàleg de 02-04 arribin amb el seu codi exacte i sense tracaId als 4xx, i esquemes JSON validats amb AJV que toleren camps nous. Has encadenat un recorregut complet de compra passant l'id i l'ETag d'una petició a la següent, has generat una Idempotency-Key per execució per provar el reintent de 02-03, i has multiplicat els casos amb un fitxer CSV en què cada fila declara el codi que espera. I amb Newman tot això s'executa des de la terminal, amb un codi de sortida que basta per bloquejar un desplegament: els fitxers nous del projecte són postman/botiga-aroma-v1.postman_collection.json, postman/local.postman_environment.json, postman/proves.postman_environment.json i postman/dades-cafes.csv, amb newman a devDependencies i els scripts proves:api i proves:fum al package.json.

Queda un cap solt i és l'important: la col·lecció és una segona descripció de l'API, escrita a mà, que pot desviar-se de la real sense que ningú se n'assabenti. Ja tenim una descripció millor —l'openapi.yaml que arrosseguem des de 02-08— però està a mitges: un sol endpoint, sense esquemes complets, sense seguretat declarada i sense publicar. A 05-02, Swagger i OpenAPI per a documentació, l'acabem: l'anatomia completa del document secció a secció, components amb esquemes, paràmetres i respostes reutilitzables, els securitySchemes amb els àmbits OAuth de 04-03, la diferència entre escriure'l a mà i generar-lo des del codi, Swagger UI servit a /docs dins del mateix projecte, la validació amb swagger-cli i les regles de Spectral de 04-01, i la generació de clients TypeScript per a la SPA i per a l'Aroma Mòbil a partir del contracte.

Curs de REST API: Principis de Disseny i Desenvolupament d'APIs RESTful

Mòdul 1: Introducció a les APIs RESTful

Mòdul 2: Disseny d'APIs RESTful

Mòdul 3: Desenvolupament d'APIs RESTful

Mòdul 4: Bones Pràctiques i Seguretat

Mòdul 5: Eines i Frameworks

Mòdul 6: Casos d'Estudi i Projectes

© Copyright 2026. Tots els drets reservats