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
- Les cinc famílies i per què importen
- La família 2xx: èxit
- La família 3xx: redirecció
- La família 4xx: error del client
- La família 5xx: error del servidor
- Taula mestra de la Botiga Aroma
- Arbre de decisió: com triar el codi correcte
- El cos de l'error:
problem+jsoni el format de la Botiga Aroma - Catàleg de codis d'error de negoci
- Antipatrons
- 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.
- 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.
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.
HTTP/1.1 206 Partial Content
Content-Type: application/pdf
Content-Range: bytes 24000-48212/48213
Content-Length: 24213No es fa servir per paginar JSON: per a això hi ha els mecanismes de 02-06.
- 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.
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.
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 | Sí | Manteniment, redirecció a una altra regió |
308 Permanent Redirect |
Permanent | Sí | 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.
- 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'incloureWWW-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 aAccept(sortida).415→ el servidor no entén el que el client envia aContent-Type(entrada).
HTTP/1.1 406 Not Acceptable
Content-Type: application/json
{ "error": { "codi": "format_no_disponible", "missatge": "Només s'admet application/json.", "detalls": [] } }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.
- 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.
- 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 |
- 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.
- El cos de l'error:
problem+json i el format de la Botiga Aroma
problem+json i el format de la Botiga AromaEl 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
codiés contracte. Ensnake_case, estable, únic, i mai no es tradueix. És el que el programari compara.missatgeés per a humans. Pot canviar de redacció i es pot traduir (Accept-Language, 02-05). Mai el comparis al codi.detallsés un array, sempre present encara que estigui buit, perquè els clients no hagin de comprovar si existeix.- Res de dades sensibles: ni consultes, ni traces de pila, ni si un correu existeix a la base de dades.
- Els
5xxportentracaId; els4xxno el necessiten.
La implementació de tot això com a middleware d'Express és la lliçó 03-07; aquí només hem fixat el contracte.
- 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).
- Antipatrons
10.1. Retornar sempre 200 amb exit: false
É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
404per a tot error del client, amagant400,403i409sota el mateix número.401quan 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
Locationen un201. El client es queda sense la URI del recurs creat. - Oblidar
Allowen un405oWWW-Authenticateen un401: són obligatòries per norma i hi ha clients que en depenen. - Fer servir
409per a errors de validació.409és de l'estat del recurs; les dades dolentes són400. - Retornar
200amb llista buida… en un element. La col·lecció buida és200; l'element inexistent és404. - Canviar el codi d'un endpoint ja publicat. Passar de
200a204trenca 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 descobreixContent-Typemal 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ó:
POST /v1/cafessense capçaleraAuthorization.POST /v1/ressenyes/res_101/aprovacioamb el token d'un client normal.POST /v1/comandesamb 100 unitats decaf_002, que té estoc 80.GET /v1/cafes/caf_999.PUT /v1/comandes/com_5001(la Botiga Aroma només admetPATCHallà).POST /v1/comandes/com_5001/pagamentsobre una comanda ja pagada.PATCH /v1/cafes/caf_001ambContent-Type: application/xml.POST /v1/cafesamb{"nom": "", "preuEuros": -3}.DELETE /v1/cistelles/cis_77/linies/caf_002, correcte.- 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 500 Internal Server Error
{ "missatge": "ValidationError: preuEuros must be positive\n at validar (/app/src/cafes.js:42:11)" }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
- Què és una API?
- Història i evolució de les APIs
- Fonaments d'HTTP per a APIs
- Principis bàsics de REST
- Model de maduresa de Richardson i HATEOAS
- REST vs. SOAP
- REST davant de GraphQL, gRPC i webhooks
Mòdul 2: Disseny d'APIs RESTful
- Principis de disseny d'APIs RESTful
- Recursos i URIs
- Mètodes HTTP
- Codis d'estat HTTP
- Representacions, capçaleres i negociació de contingut
- Filtratge, ordenació, paginació i cerca
- Versionat d'APIs
- Documentació d'APIs
Mòdul 3: Desenvolupament d'APIs RESTful
- Configuració de l'entorn de desenvolupament
- Creació d'un servidor bàsic
- Gestió de peticions i respostes
- Validació de dades d'entrada
- Persistència i capa d'accés a dades
- Autenticació i autorització
- Gestió d'errors
- Proves i validació
Mòdul 4: Bones Pràctiques i Seguretat
- Bones pràctiques en el disseny d'APIs
- Seguretat en APIs RESTful
- OAuth 2.0 i OpenID Connect a la pràctica
- Rate limiting i throttling
- CORS i polítiques de seguretat
- Memòria cau HTTP i rendiment
- Observabilitat: logs, mètriques i traces
Mòdul 5: Eines i Frameworks
- Postman per a proves d'APIs
- Swagger i OpenAPI per a documentació
- Frameworks populars per a APIs RESTful
- Contractes, mocks i proves automatitzades d'API
- Integració contínua i desplegament
- API gateways i portals de desenvolupador
