A la lliçó anterior vam anar deixant números pel camí: 201 en crear amb Location, 204 sense cos, 404 davant de 410, 409 quan la transició no és possible, 415 quan el Content-Type no s'admet. Cadascun d'aquests codis és una decisió de contracte: és el primer que llegeix un client i el que determina si reintenta, si mostra un error a l'usuari, si redirigeix o si tanca la sessió. Triar malament el codi converteix en indesxifrable una API que per la resta està ben dissenyada. Aquesta lliçó recorre els codis que una API REST fa servir de debò —no la llista completa del registre d'IANA—, construeix un arbre de decisió per encertar-la sempre, i dissenya el cos de l'error de la Botiga Aroma, comparant-lo amb l'estàndard application/problem+json.

Contingut

  1. Les cinc famílies i per què importen
  2. La família 2xx: èxit
  3. La família 3xx: redirecció
  4. La família 4xx: error del client
  5. La família 5xx: error del servidor
  6. Taula mestra de la Botiga Aroma
  7. Arbre de decisió: com triar el codi correcte
  8. El cos de l'error: problem+json i el format de la Botiga Aroma
  9. Catàleg de codis d'error de negoci
  10. Antipatrons

  1. Les cinc famílies i per què importen

A 01-03 vam veure el mapa; ara entrem en el detall. La primera xifra del codi classifica la resposta:

Família Significat De qui és el problema? Ha de reintentar el client?
1xx Informativa De ningú, és protocol
2xx Èxit No
3xx Redirecció Del client, que ha de seguir una altra ruta Sí, a una altra URI
4xx Error del client Del client No, sense canviar la petició
5xx Error del servidor Del servidor Sí, amb reintents espaiats

Aquesta classificació no és decorativa: és lògica de la qual depèn programari real. Un client HTTP genèric, un balancejador o la passarel·la d'un soci decideixen basant-se només en la primera xifra. Si retornes 200 per a un error, cap reintent automàtic no es dispararà i cap alerta no saltarà. Si retornes 500 per una dada mal escrita pel client, el sistema reintentarà una petició que mai no funcionarà, i el teu equip de guàrdia rebrà un avís a les tres de la matinada per una fallada que no és seva.

De la família 1xx només mereix menció 100 Continue, que gestionen els clients HTTP de manera transparent en pujar cossos grans. No la faràs servir explícitament.

  1. La família 2xx: èxit

200 OK

L'èxit genèric, amb cos. El fan servir GET, PUT, PATCH correctes i els POST d'acció que no creen cap recurs nou.

HTTP/1.1 200 OK
Content-Type: application/json

{ "id": "caf_001", "nom": "Etiòpia Yirgacheffe", "preuEuros": 14.50 }

Recorda de 02-03: un GET de col·lecció sense resultats és 200 amb {"dades": [], "total": 0}, mai 404.

201 Created

S'ha creat un recurs. Obligatòria la capçalera Location amb la seva URI.

HTTP/1.1 201 Created
Content-Type: application/json
Location: https://api.botigaaroma.example/v1/comandes/com_5001

{
  "id": "com_5001",
  "clientId": "cli_842",
  "estat": "pendent_pagament",
  "totalEuros": 29.00,
  "dataCreacio": "2026-03-14T10:30:00Z",
  "_links": {
    "self": { "href": "/v1/comandes/com_5001" },
    "pagar": { "href": "/v1/comandes/com_5001/pagament", "method": "POST" }
  }
}

Location és el que permet al client encadenar sense construir URLs a mà, i és el mínim d'hipermèdia que tota API hauria de complir. A la Botiga Aroma la retornen POST /comandes, POST /cafes, POST /cafes/{id}/ressenyes, POST /comandes/{id}/pagament i tots els subrecursos d'acció.

202 Accepted

"He acceptat la petició, però encara no l'he processada." És el codi del processament asíncron, i el seu contracte inclou dir al client on consultar el progrés.

HTTP/1.1 202 Accepted
Content-Type: application/json
Location: https://api.botigaaroma.example/v1/comandes/com_5001/devolucio

{
  "estat": "en_revisio",
  "missatge": "La teva sol·licitud de devolució es revisarà en un termini de 24 hores.",
  "_links": { "estat": { "href": "/v1/comandes/com_5001/devolucio" } }
}

Compte amb el parany del 202: quan retornes 202 estàs dient que pot fallar després, així que necessites un recurs on el client vegi el resultat final. Un 202 sense lloc on mirar és un forat negre.

204 No Content

Èxit sense cos. El client no ha d'intentar parsejar res. Usos a la Botiga Aroma: DELETE correcte, PUT/PATCH quan el client no necessita la representació, i OPTIONS.

HTTP/1.1 204 No Content

Regla estricta: 204 significa cos de longitud zero. Retornar 204 amb un JSON a dins trenca clients que, correctament, ni llegeixen el flux.

206 Partial Content

Resposta parcial a una petició amb Range. A la Botiga Aroma apareix exactament en un lloc: la descàrrega represa de la factura en PDF.

GET /v1/comandes/com_5001/factura HTTP/1.1
Accept: application/pdf
Range: bytes=24000-48212
HTTP/1.1 206 Partial Content
Content-Type: application/pdf
Content-Range: bytes 24000-48212/48213
Content-Length: 24213

No es fa servir per paginar JSON: per a això hi ha els mecanismes de 02-06.

  1. La família 3xx: redirecció

301 Moved Permanently

El recurs ha canviat d'URI per sempre. La Botiga Aroma ho fa servir per normalitzar la barra final (/cafes//cafes) i per a les URIs heretades de l'API antiga.

HTTP/1.1 301 Moved Permanently
Location: https://api.botigaaroma.example/v1/cafes

304 Not Modified

"Allò que tens a la memòria cau encara val." Respon a peticions condicionals amb If-None-Match o If-Modified-Since, i va sense cos, que és justament l'estalvi.

GET /v1/cafes/caf_001 HTTP/1.1
If-None-Match: "a1b2c3d4"
HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4"
Cache-Control: public, max-age=300

La memòria cau condicional completa —ETags, Last-Modified, validació, revalidació— es desenvolupa a 04-06. Aquí n'hi ha prou de saber que 304 no és un error, sinó un èxit molt barat.

307 i 308 davant de 302

El problema històric: davant d'un 301 o 302, molts clients convertien un POST en un GET en seguir la redirecció, cosa que l'estàndard no pretenia però que es va consolidar com a pràctica. Per eliminar l'ambigüitat es van crear dos codis que garanteixen que el mètode i el cos es conserven:

Codi Permanència Conserva mètode i cos? Ús recomanat
301 Permanent A la pràctica, no (POST → GET) Recursos moguts, només amb GET
302 Found Temporal Ambigu Evitar en APIs
307 Temporary Redirect Temporal Manteniment, redirecció a una altra regió
308 Permanent Redirect Permanent Canvi definitiu d'URI conservant el mètode

Decisió de la Botiga Aroma: no es fa servir 302 en cap cas. Per al que és permanent, 301 si només afecta lectures i 308 si pot afectar escriptures; per al que és temporal, 307.

  1. La família 4xx: error del client

És la família més rica i la que més s'equivoca. En totes elles el cos porta el format d'error de la secció 8.

400 Bad Request

La petició està mal formada o les dades no són vàlides: JSON amb error de sintaxi, tipus incorrecte, camp obligatori absent, camp desconegut (recorda: la Botiga Aroma és estricta a l'entrada), paràmetre de query invàlid.

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": {
    "codi": "dades_invalides",
    "missatge": "El cos de la petició conté errors de validació.",
    "detalls": [
      { "camp": "preuEuros", "problema": "Ha de ser un nombre més gran que 0.", "valorRebut": -3 },
      { "camp": "torrefaccio", "problema": "Valor no permès. Valors vàlids: clar, mitja, fosc.", "valorRebut": "torradíssim" }
    ]
  }
}

Nota de disseny important: es retornen tots els errors de validació alhora, no el primer. Un formulari que falla camp a camp, en peticions successives, és una tortura per a l'usuari.

401 Unauthorized davant de 403 Forbidden

La distinció que més s'equivoca del món HTTP. La manera de recordar-la:

  • 401 = "no sé qui ets". Falta l'Authorization, el token és invàlid o ha caducat. La resposta ha d'incloure WWW-Authenticate. El client ho pot arreglar autenticant-se.
  • 403 = "sé qui ets i no pots". La identitat és vàlida, però li falten permisos. Tornar-se a autenticar no serveix de res.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api.botigaaroma.example", error="invalid_token"
Content-Type: application/json

{ "error": { "codi": "token_caducat", "missatge": "El token d'accés ha caducat.", "detalls": [] } }
HTTP/1.1 403 Forbidden
Content-Type: application/json

{ "error": { "codi": "permisos_insuficients", "missatge": "Cal el rol 'moderador' per aprovar ressenyes.", "detalls": [] } }

El nom 401 Unauthorized és un error històric de l'estàndard: s'hauria de dir Unauthenticated. Conseqüència pràctica a la SPA de la Botiga Aroma: davant d'un 401 intenta refrescar el token i reintentar una vegada; davant d'un 403 mostra directament "no tens permís" i no reintenta.

Existeix un tercer cas, incòmode però important: quan un client autenticat demana un recurs aliè (GET /v1/comandes/com_9999, que és d'una altra persona), respondre 403 confirma que aquesta comanda existeix. Per evitar aquesta fuita, la Botiga Aroma respon 404 als recursos privats d'altres clients. És una decisió de seguretat deliberada, es diu emmascarament i es tracta a 04-02.

404 Not Found davant de 410 Gone

404 Not Found 410 Gone
Significat No hi ha res aquí (potser mai no hi va haver res, potser no ho pots veure) Va existir i s'ha eliminat definitivament
Pot tornar? Potser sí No
Què fa un rastrejador Reintenta més endavant Elimina la URL del seu índex
Ús a la Botiga Aroma Id inexistent, recurs aliè Cafè descatalogat, versió d'API apagada

410 és més informatiu quan ho saps amb certesa: diu al client que deixi de demanar-ho. És especialment útil en l'apagada de versions antigues (02-07).

405 Method Not Allowed

El recurs existeix, però no admet aquest mètode. Obligatòria la capçalera Allow.

HTTP/1.1 405 Method Not Allowed
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json

{ "error": { "codi": "metode_no_permes", "missatge": "DELETE no està permès sobre /v1/cafes.", "detalls": [] } }

Distingeix-lo del 404: si DELETE /v1/cafes donés 404, el desenvolupador buscaria una errada a la URL en comptes d'adonar-se que el mètode és l'equivocat.

406 Not Acceptable i 415 Unsupported Media Type

Es confonen perquè totes dues parlen de formats, però apunten en direccions oposades:

  • 406 → el servidor no pot produir el que el client demana a Accept (sortida).
  • 415 → el servidor no entén el que el client envia a Content-Type (entrada).
GET /v1/cafes/caf_001 HTTP/1.1
Accept: application/xml
HTTP/1.1 406 Not Acceptable
Content-Type: application/json

{ "error": { "codi": "format_no_disponible", "missatge": "Només s'admet application/json.", "detalls": [] } }
PATCH /v1/cafes/caf_001 HTTP/1.1
Content-Type: application/json-patch+json
HTTP/1.1 415 Unsupported Media Type
Accept-Patch: application/merge-patch+json
Content-Type: application/json

{ "error": { "codi": "format_no_suportat", "missatge": "Aquest recurs només admet application/merge-patch+json.", "detalls": [] } }

409 Conflict

La petició és vàlida però xoca amb l'estat actual del recurs. És el codi de la lògica de negoci, i a la Botiga Aroma apareix sovint:

POST /v1/comandes HTTP/1.1
Content-Type: application/json
Idempotency-Key: 5f3b9c2a-1d7e-4a44-9f30-8b1c2d3e4f50

{ "clientId": "cli_842", "linies": [{ "cafeId": "caf_002", "quantitat": 100 }] }
HTTP/1.1 409 Conflict
Content-Type: application/json

{
  "error": {
    "codi": "estoc_insuficient",
    "missatge": "No hi ha prou unitats de 'Colòmbia Huila'.",
    "detalls": [
      { "cafeId": "caf_002", "sollicitat": 100, "disponible": 80 }
    ]
  }
}

Altres conflictes del catàleg: comanda_ja_pagada, comanda_ja_anullada, ressenya_ja_moderada, operacio_en_curs. La regla per distingir-lo del 400: si les dades són correctes i el que impedeix l'operació és l'estat del recurs, és 409.

412 Precondition Failed

Ha fallat una condició prèvia enviada pel client, típicament If-Match amb un ETag antic. És el mecanisme de concurrència optimista que evita l'actualització perduda:

PATCH /v1/cafes/caf_001 HTTP/1.1
If-Match: "a1b2c3d4"
Content-Type: application/merge-patch+json

{ "estoc": 95 }
HTTP/1.1 412 Precondition Failed
Content-Type: application/json

{ "error": { "codi": "conflicte_versio", "missatge": "El recurs ha canviat des de la teva darrera lectura.", "detalls": [] } }

Els ETags i les peticions condicionals es desenvolupen a 04-06.

422 Unprocessable Content i el debat 400 vs 422

422 (reanomenat Unprocessable Content a la RFC 9110) significa: la sintaxi és correcta, entenc el document, però els seus continguts no es poden processar. Exemple canònic: un JSON perfectament format que demana una data de lliurament al passat.

El debat fa anys que és obert i aquestes són les dues postures:

Postura Regla A favor En contra
Només 400 Tot error del client és 400; el detall va al cos Simple, no cal decidir; 422 ve de WebDAV Es perd una distinció útil per a clients automàtics
400 + 422 400 per a errors de sintaxi/format; 422 per a errors semàntics Distingeix "no t'entenc" de "t'entenc i no puc" Fronteres difuses: un enumerat invàlid és sintaxi o semàntica?

Decisió de la Botiga Aroma: 400 per a tots els errors de validació d'entrada (sintaxi, tipus, camps obligatoris, enumerats, rangs), amb el detall camp a camp a detalls. 422 es reserva per a un cas molt concret i ben delimitat: la reutilització indeguda d'una Idempotency-Key amb un cos diferent (02-03), on la petició és impecable però no es pot processar per un motiu que no és ni de format ni d'estat del recurs.

L'important no és quina de les dues postures tries, sinó escriure-la a la guia d'estil i no desviar-te'n: el que trenca els clients és que el mateix tipus de fallada retorni 400 en un endpoint i 422 en un altre.

429 Too Many Requests

S'ha superat el límit de peticions. Ha d'anar acompanyat de Retry-After i, a la Botiga Aroma, de les capçaleres de quota:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Aroma-RateLimit-Limit: 1000
Aroma-RateLimit-Restants: 0
Content-Type: application/json

{ "error": { "codi": "limit_peticions", "missatge": "Has superat el límit de 1000 peticions per hora.", "detalls": [] } }

Les polítiques, finestres i algorismes de limitació són la lliçó 04-04.

  1. La família 5xx: error del servidor

Aquí el client no ha fet res malament. Regla d'or: mai no filtris detalls interns —traces de pila, consultes SQL, rutes de fitxer, noms de servidors— perquè són informació d'or per a un atacant.

500 Internal Server Error

El calaix de sastre: excepció no controlada. Ha de portar un identificador de traça perquè el client el pugui citar en obrir una incidència:

HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{
  "error": {
    "codi": "error_intern",
    "missatge": "S'ha produït un error inesperat. Contacta amb suport citant l'identificador de traça.",
    "detalls": [],
    "tracaId": "trz_8f4a1c92"
  }
}

Aquest tracaId és la costura entre la resposta i els teus logs, i és la base de l'observabilitat (04-07).

502, 503 i 504

Codi Significat Causa típica a la Botiga Aroma Reintentar?
502 Bad Gateway Resposta invàlida d'un servei aigües amunt La passarel·la de pagament retorna brossa Sí, amb espera
503 Service Unavailable Servei no disponible temporalment Manteniment, saturació, arrencada Sí, segons Retry-After
504 Gateway Timeout Un servei aigües amunt no ha respost a temps El servei gRPC d'estoc supera el seu termini Sí, amb compte

503 és l'únic que es pot planificar, i per això porta Retry-After, que admet segons o una data HTTP:

HTTP/1.1 503 Service Unavailable
Retry-After: 120
Content-Type: application/json

{ "error": { "codi": "servei_no_disponible", "missatge": "Manteniment programat. Torna-ho a intentar d'aquí a 2 minuts.", "detalls": [] } }

Precaució amb el 504: el temps d'espera s'ha esgotat, però l'operació pot haver-se executat igualment aigües amunt. És exactament l'escenari que justifica les claus d'idempotència de 02-03.

  1. Taula mestra de la Botiga Aroma

Codi Quan es fa servir Exemple concret
200 Lectura o actualització correctes GET /v1/cafes/caf_001
201 Recurs creat (+ Location) POST /v1/comandes
202 Acceptat per processar després POST /v1/comandes/com_5001/devolucio
204 Èxit sense cos DELETE /v1/cistelles/cis_77/linies/caf_002
206 Descàrrega parcial amb Range PDF de /v1/comandes/com_5001/factura
301 URI moguda permanentment /v1/cafes//v1/cafes
304 La memòria cau del client continua vigent GET /v1/cafes/caf_001 amb If-None-Match
307 Redirecció temporal conservant el mètode Desviament durant manteniment
308 Redirecció permanent conservant el mètode Reubicació d'un endpoint d'escriptura
400 Petició o dades invàlides preuEuros: -3
401 Falta autenticació o token caducat Sense capçalera Authorization
403 Autenticat però sense permisos Client intentant aprovar una ressenya
404 No existeix (o no ho pots veure) GET /v1/cafes/caf_999
405 Mètode no permès (+ Allow) DELETE /v1/cafes
406 No es pot servir l'Accept demanat Accept: application/xml
409 Conflicte amb l'estat actual estoc_insuficient, comanda_ja_pagada
410 Va existir i s'ha eliminat per sempre Cafè descatalogat; API /v0 apagada
412 Ha fallat If-Match (ETag antic) Dues edicions simultànies d'estoc
413 Cos massa gran Imatge de cafè de 20 MB
415 Content-Type no suportat PATCH amb application/json-patch+json
422 Correcte però no processable Idempotency-Key reutilitzada amb un altre cos
429 Límit de peticions superat 1001 peticions en una hora
500 Error inesperat del servidor Excepció no controlada
502 Servei aigües amunt respon malament Passarel·la de pagament caiguda
503 No disponible temporalment (+ Retry-After) Manteniment programat
504 Timeout aigües amunt Servei d'estoc lent

  1. Arbre de decisió: com triar el codi correcte

graph TD
    A{"S'ha processat<br/>correctament?"} -->|No| B{"De qui és<br/>la fallada?"}
    A -->|"Sí, però encara no<br/>està acabat"| ACC["202 Accepted"]
    A -->|Sí| C{"S'ha creat<br/>un recurs?"}
    C -->|Sí| CRE["201 Created<br/>+ Location"]
    C -->|No| D{"Hi ha cos<br/>per retornar?"}
    D -->|Sí| OK["200 OK"]
    D -->|No| NC["204 No Content"]
    B -->|"Del servidor"| E{"És temporal?"}
    E -->|Sí| SRV["503 + Retry-After<br/>502 / 504 si és aigües amunt"]
    E -->|No| ERR["500 + tracaId"]
    B -->|"Del client"| F{"Sabem<br/>qui és?"}
    F -->|"No autenticat"| U401["401 + WWW-Authenticate"]
    F -->|"Sense permisos"| U403["403 Forbidden"]
    F -->|Sí| G{"Existeix el<br/>recurs?"}
    G -->|"No, i no tornarà"| G410["410 Gone"]
    G -->|No| G404["404 Not Found"]
    G -->|Sí| H{"El mètode<br/>està permès?"}
    H -->|No| H405["405 + Allow"]
    H -->|Sí| I{"Són vàlides<br/>les dades?"}
    I -->|No| I400["400 dades_invalides"]
    I -->|Sí| J{"L'estat del recurs<br/>permet l'operació?"}
    J -->|No| J409["409 Conflict"]
    J -->|Sí| OK

Recorre aquest arbre per a cada endpoint nou i tindràs mitja documentació escrita.

  1. El cos de l'error: problem+json i el format de la Botiga Aroma

El codi d'estat diu quina categoria de fallada s'ha produït; el cos diu què ha passat exactament. Sense cos, un 400 obliga el desenvolupador a endevinar.

8.1. L'estàndard: application/problem+json (RFC 9457)

Existeix un format estàndard de cos d'error, definit originalment a la RFC 7807 i actualitzat per la RFC 9457:

HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "https://api.botigaaroma.example/errors/estoc-insuficient",
  "title": "Estoc insuficient",
  "status": 409,
  "detail": "Només queden 80 unitats de 'Colòmbia Huila' i se n'han sol·licitat 100.",
  "instance": "/v1/comandes",
  "cafeId": "caf_002",
  "sollicitat": 100,
  "disponible": 80
}

Camps definits per la norma:

Camp Què és
type URI que identifica el tipus de problema; idealment apunta a documentació
title Resum llegible, estable per a un mateix type
status El codi HTTP, repetit al cos
detail Explicació d'aquesta ocurrència concreta
instance URI de l'ocurrència
extensions Camps propis al mateix nivell (cafeId, disponible…)

Avantatges: és estàndard, hi ha biblioteques que el generen i el consumeixen, i el type com a URI garanteix unicitat global. Inconvenients pràctics: els noms són críptics per a qui no coneix la RFC, type com a URL convida a inventar URLs que ningú no manté, les extensions al mateix nivell que els camps estàndard poden col·lidir, i el Content-Type diferent obliga els clients a gestionar dos tipus de resposta.

8.2. El format de la Botiga Aroma

{
  "error": {
    "codi": "estoc_insuficient",
    "missatge": "No hi ha prou unitats de 'Colòmbia Huila'.",
    "detalls": [
      { "cafeId": "caf_002", "sollicitat": 100, "disponible": 80 }
    ]
  }
}
Aspecte problem+json (RFC 9457) Format de la Botiga Aroma
Content-Type application/problem+json application/json
Identificador del tipus type (URI) codi (snake_case)
Text per a humans title + detail missatge
Errors múltiples No previst de sèrie detalls com a array
Estandardització Alta Pròpia
Llegibilitat per al consumidor Mitjana Alta
Embolcall Camps a l'arrel Tot sota error

Decisió de la Botiga Aroma: format propi, per tres raons: (1) l'embolcall error fa impossible confondre una resposta correcta amb una d'errònia, fins i tot ignorant el codi d'estat; (2) detalls com a array resol de manera natural la validació de formularis, que és el cas més freqüent; (3) mantenir application/json a tota l'API simplifica els clients. La decisió es documenta explícitament al costat de l'alternativa estàndard, i a 02-08 quedarà reflectida com a esquema reutilitzable a OpenAPI.

8.3. Regles del contracte d'errors

  1. codi és contracte. En snake_case, estable, únic, i mai no es tradueix. És el que el programari compara.
  2. missatge és per a humans. Pot canviar de redacció i es pot traduir (Accept-Language, 02-05). Mai el comparis al codi.
  3. detalls és un array, sempre present encara que estigui buit, perquè els clients no hagin de comprovar si existeix.
  4. Res de dades sensibles: ni consultes, ni traces de pila, ni si un correu existeix a la base de dades.
  5. Els 5xx porten tracaId; els 4xx no el necessiten.

La implementació de tot això com a middleware d'Express és la lliçó 03-07; aquí només hem fixat el contracte.

  1. Catàleg de codis d'error de negoci

El catàleg forma part del contracte tant com les URIs. Aquest és l'inicial de la Botiga Aroma:

codi HTTP Quan
dades_invalides 400 Validació de cos o de paràmetres
parametre_invalid 400 Query param mal format (limit=abc)
clau_idempotencia_requerida 400 Falta Idempotency-Key on és obligatòria
no_autenticat 401 Falta el token o és invàlid
token_caducat 401 Token expirat
permisos_insuficients 403 Identitat vàlida sense permisos
cafe_no_trobat 404 El cafè no existeix
client_no_trobat 404 El client no existeix
comanda_no_trobada 404 La comanda no existeix
ressenya_no_trobada 404 La ressenya no existeix
cistella_no_trobada 404 La cistella no existeix o ha caducat
metode_no_permes 405 Mètode no admès pel recurs
format_no_disponible 406 No es pot satisfer l'Accept
estoc_insuficient 409 No hi ha prou unitats
comanda_ja_pagada 409 Segon pagament de la mateixa comanda
comanda_ja_anullada 409 Segona anul·lació
comanda_no_enviada 409 Devolució d'una comanda no enviada
ressenya_ja_moderada 409 Segona moderació de la mateixa ressenya
cistella_buida 409 Confirmar una cistella sense línies
operacio_en_curs 409 Petició idèntica encara processant-se
cafe_descatalogat 410 Cafè retirat definitivament
conflicte_versio 412 If-Match amb ETag antic
cos_massa_gran 413 Se supera el límit de mida
format_no_suportat 415 Content-Type no admès
clau_idempotencia_reutilitzada 422 Mateixa clau, cos diferent
limit_peticions 429 Límit superat
error_intern 500 Excepció no controlada
servei_no_disponible 503 Manteniment o saturació

Convenció de noms: <entitat>_<problema> per al que és específic (cafe_no_trobat) i <problema> a seques per al que és transversal (dades_invalides). El catàleg només creix: retirar un codi és un canvi trencador (02-07).

  1. Antipatrons

10.1. Retornar sempre 200 amb exit: false

HTTP/1.1 200 OK

{ "exit": false, "missatge": "El cafè no existeix" }

És l'antipatró més estès i el més nociu. Conseqüències: els clients HTTP no detecten l'error, els reintents automàtics no es disparen, el monitoratge marca 100 % d'èxit, els proxys posen l'error a la memòria cau com si fos una resposta bona, i cada consumidor s'ha d'inventar la seva pròpia lògica de detecció. Renuncia del tot al nivell 2 de Richardson.

10.2. Fer servir 500 per a errors del client

Si un POST /v1/cafes amb preuEuros: "gratis" provoca un 500, el sistema del client reintentarà una petició condemnada al fracàs i el teu equip rebrà una alerta per una fallada aliena. Regla: si la petició no pot funcionar tal com està, és 4xx.

10.3. Inventar codis

299 Gairebé OK, 450 Error de negoci, 600 Fallada. Els intermediaris interpreten els codis desconeguts per la seva primera xifra en el millor dels casos, i els rebutgen en el pitjor. Fes servir només codis registrats; per al detall de negoci ja tens el camp codi del cos.

10.4. Altres que es veuen cada dia

  • 404 per a tot error del client, amagant 400, 403 i 409 sota el mateix número.
  • 401 quan falten permisos: envia l'usuari a autenticar-se una altra vegada, no serveix de res i sovint provoca bucles de refresc de token.
  • Missatges inútils: "Error", "Alguna cosa ha fallat", "Consulta el log".
  • Filtrar la traça de pila en producció.
  • Cos en un 204: hi ha clients que no llegeixen el flux i el deixaran a mitges.

Errors Comuns i Consells

  • Confondre 401 i 403. No sé qui ets davant de sé qui ets i no pots. Memoritza-ho així.
  • Oblidar Location en un 201. El client es queda sense la URI del recurs creat.
  • Oblidar Allow en un 405 o WWW-Authenticate en un 401: són obligatòries per norma i hi ha clients que en depenen.
  • Fer servir 409 per a errors de validació. 409 és de l'estat del recurs; les dades dolentes són 400.
  • Retornar 200 amb llista buida… en un element. La col·lecció buida és 200; l'element inexistent és 404.
  • Canviar el codi d'un endpoint ja publicat. Passar de 200 a 204 trenca clients que llegeixen el cos: és un canvi trencador (02-07).
  • Consell: escriu la taula de codis de cada endpoint abans d'implementar-lo. És una columna obligatòria de la documentació de referència (02-08).
  • Consell: comprova els teus errors amb curl -i. Veure la resposta crua descobreix Content-Type mal posats i cossos buits que un client elegant t'amaga.

Exercicis

Exercici 1: assignar el codi correcte

Indica el codi d'estat, el codi d'error i les capçaleres rellevants de cada situació:

  1. POST /v1/cafes sense capçalera Authorization.
  2. POST /v1/ressenyes/res_101/aprovacio amb el token d'un client normal.
  3. POST /v1/comandes amb 100 unitats de caf_002, que té estoc 80.
  4. GET /v1/cafes/caf_999.
  5. PUT /v1/comandes/com_5001 (la Botiga Aroma només admet PATCH allà).
  6. POST /v1/comandes/com_5001/pagament sobre una comanda ja pagada.
  7. PATCH /v1/cafes/caf_001 amb Content-Type: application/xml.
  8. POST /v1/cafes amb {"nom": "", "preuEuros": -3}.
  9. DELETE /v1/cistelles/cis_77/linies/caf_002, correcte.
  10. El servei intern d'estoc no respon en 5 segons.

Exercici 2: redissenyar respostes d'una API mal feta

Una API heretada respon així. Reescriu cada resposta amb el codi, les capçaleres i el cos correctes segons el contracte de la Botiga Aroma.

HTTP/1.1 200 OK
{ "exit": false, "error": "no trobat" }
HTTP/1.1 200 OK
{ "exit": true, "id": "com_5001" }        ← resposta a POST /v1/comandes
HTTP/1.1 500 Internal Server Error
{ "missatge": "ValidationError: preuEuros must be positive\n  at validar (/app/src/cafes.js:42:11)" }
HTTP/1.1 403 Forbidden
{ "missatge": "Has d'iniciar sessió" }

Exercici 3: traduir a problem+json

Tradueix aquest error de la Botiga Aroma al format application/problem+json de la RFC 9457, i explica què s'hi guanya i què s'hi perd en la traducció:

{
  "error": {
    "codi": "dades_invalides",
    "missatge": "El cos de la petició conté errors de validació.",
    "detalls": [
      { "camp": "preuEuros", "problema": "Ha de ser més gran que 0.", "valorRebut": -3 },
      { "camp": "torrefaccio", "problema": "Valor no permès.", "valorRebut": "torradíssim" }
    ]
  }
}

Solucions

Solució 1

# Codi codi Capçaleres
1 401 no_autenticat WWW-Authenticate: Bearer realm="..."
2 403 permisos_insuficients
3 409 estoc_insuficient — (amb detalls: sol·licitat 100, disponible 80)
4 404 cafe_no_trobat
5 405 metode_no_permes Allow: GET, PATCH, HEAD, OPTIONS
6 409 comanda_ja_pagada
7 415 format_no_suportat Accept-Patch: application/merge-patch+json
8 400 dades_invalides — (dues entrades a detalls, no una)
9 204 Sense cos
10 504 servei_no_disponible — (i tracaId al cos)

Solució 2

HTTP/1.1 404 Not Found
Content-Type: application/json

{ "error": { "codi": "cafe_no_trobat", "missatge": "No existeix cap cafè amb l'identificador 'caf_999'.", "detalls": [] } }
HTTP/1.1 201 Created
Content-Type: application/json
Location: https://api.botigaaroma.example/v1/comandes/com_5001

{
  "id": "com_5001",
  "clientId": "cli_842",
  "estat": "pendent_pagament",
  "totalEuros": 29.00,
  "dataCreacio": "2026-03-14T10:30:00Z",
  "_links": {
    "self": { "href": "/v1/comandes/com_5001" },
    "pagar": { "href": "/v1/comandes/com_5001/pagament", "method": "POST" }
  }
}
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": {
    "codi": "dades_invalides",
    "missatge": "El cos de la petició conté errors de validació.",
    "detalls": [ { "camp": "preuEuros", "problema": "Ha de ser un nombre més gran que 0.", "valorRebut": -3 } ]
  }
}

Aquí hi havia dues fallades: el codi (un error de validació és del client, 400, no 500) i la fuita de la traça de pila amb rutes internes del servidor.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api.botigaaroma.example"
Content-Type: application/json

{ "error": { "codi": "no_autenticat", "missatge": "Cal autenticació per accedir a aquest recurs.", "detalls": [] } }

El missatge "has d'iniciar sessió" delata que el problema és d'autenticació, així que el codi correcte és 401, no 403, i falta la capçalera WWW-Authenticate.

Solució 3

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
  "type": "https://api.botigaaroma.example/errors/dades-invalides",
  "title": "Dades invàlides",
  "status": 400,
  "detail": "El cos de la petició conté errors de validació.",
  "instance": "/v1/cafes",
  "errors": [
    { "camp": "preuEuros", "problema": "Ha de ser més gran que 0.", "valorRebut": -3 },
    { "camp": "torrefaccio", "problema": "Valor no permès.", "valorRebut": "torradíssim" }
  ]
}

S'hi guanya: un format estàndard que eines i biblioteques reconeixen sense configuració; un type com a URI globalment única que a més pot ser un enllaç a la documentació de l'error; i instance, que identifica l'ocurrència concreta i ajuda a correlacionar amb els logs.

S'hi perd: l'embolcall error, que permetia distingir d'un cop d'ull una resposta correcta d'una d'errònia sense mirar el codi; l'homogeneïtat del Content-Type a tota l'API; i la nitidesa dels noms —codi/missatge/detalls són més directes per al consumidor que type/title/detail—. A més, la llista d'errors de validació (errors) és una extensió pròpia en tots dos casos: la RFC no l'estandarditza, així que aquesta part s'ha de documentar igualment.

Conclusió

Els codis d'estat són la primera línia del contracte: diuen si l'operació ha anat bé, de qui és el problema i si té sentit reintentar. Ara saps quan retornar 201 amb Location i quan 204 sense cos, distingir 401 de 403 i 404 de 410, reservar 409 per als conflictes d'estat com l'estoc_insuficient, no confondre 406 amb 415, i acompanyar els 5xx de Retry-After i d'un tracaId sense filtrar res intern. Tens a més un arbre de decisió reutilitzable, el catàleg de codis d'error de negoci de la Botiga Aroma i una postura raonada davant d'application/problem+json. I saps què no cal fer: 200 amb exit: false, 500 per dades mal escrites pel client i codis inventats.

Fins aquí hem dissenyat el sobre: on va la petició, amb quin verb i amb quin resultat. Falta el contingut. A la lliçó següent, 02-05 Representacions, capçaleres i negociació de contingut, dissenyarem el cos de les respostes de la Botiga Aroma: noms i tipus de camp, dates, imports monetaris, nuls davant d'absents, l'embolcall dades/total, quan incrustar i quan enllaçar amb _links, l'expansió i la selecció de camps, i tota la negociació de contingut amb Accept, Content-Type, Accept-Language i Accept-Encoding, inclosa la factura en PDF i la pujada d'imatges de cafè.

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