La lliçó anterior va resoldre mitja promesa: Nómada Tasques ja sobreviu a F5. Però la Marta, l'Iván i la Lucía continuen tenint cadascun el seu propi localStorage, amb la seva pròpia versió del tauler, sense manera de veure's. L'altra meitat —que el tauler sigui el mateix per a tot l'equip— exigeix que les dades visquin en un servidor i que el navegador sàpiga parlar-hi sense recarregar la pàgina. Això és AJAX, i l'eina moderna per fer-ho és fetch. En aquesta lliçó aprendràs què va canviar AJAX al web, repassaràs l'HTTP que necessites de debò (verbs, codis i capçaleres), dominaràs fetch i l'objecte Response amb el seu parany més famós —un 404 no rebutja la promesa—, enviaràs dades en JSON i en FormData, construiràs URLs amb paràmetres, entendràs CORS prou per no perdre una tarda, i escriuràs js/dades/api-tasques.js, el germà de xarxa del repositori local. I per fi retiraràs aquelles llegirBacklogSimulat() i desarInformeSimulat() de 05-06 que fingien latència amb setTimeout.

Contingut

  1. Què és AJAX i què va canviar
  2. XMLHttpRequest, l'avantpassat
  3. HTTP en deu minuts: verbs, rutes, codis i capçaleres
  4. fetch: la petició mínima
  5. L'objecte Response i els mètodes de cos
  6. El parany fonamental: fetch no rebutja amb 404 ni 500
  7. Enviar dades: method, headers i body
  8. Enviar FormData i fitxers
  9. Paràmetres de consulta amb URL i URLSearchParams
  10. CORS: per què el navegador et bloqueja
  11. Autenticació: Authorization i credentials
  12. Nómada Tasques: js/dades/api-tasques.js
  13. Practicar de debò: json-server a la teva màquina
  14. Errors Habituals i Consells
  15. Exercicis
  16. Conclusió

  1. Què és AJAX i què va canviar

AJAX són les sigles d'Asynchronous JavaScript And XML, un nom encunyat el 2005 que avui és mitja mentida: gairebé ningú fa servir XML —es fa servir JSON— i la tècnica s'aplica a molt més que XML. El que la sigla anomena continua vigent: demanar dades al servidor des de JavaScript, sense recarregar la pàgina, i actualitzar només la part del DOM que canvia.

Compara els dos models:

Web clàssic (sense AJAX) Web amb AJAX
En prémer «marcar com a feta» El navegador envia un formulari i recarrega la pàgina sencera JavaScript envia una petició en segon pla
El que viatja de tornada Un document HTML complet Uns centenars de bytes de JSON
Estat de la interfície Es perd: scroll, focus, filtres Es conserva
Percepció de l'usuari Parpelleig blanc, espera La targeta canvia i prou
Feina del servidor Renderitzar tota la pàgina Retornar la dada

El que AJAX fa possible, en termes de Nómada Tasques: la Marta prem el botó d'avançar estat, la targeta es mou de columna, i ni el scroll ni el filtre per responsable ni el focus es perden. El cicle estat → render → esdeveniment → estat nou → render que vas muntar a 06-06 continua manant; l'únic que canvia és que ara una part de l'«estat nou» arriba del servidor.

I hi ha una cosa que no canvia i convé dir aviat: el servidor continua manant. Una petició AJAX no és més segura que un formulari; l'usuari pot fabricar la que vulgui des de la consola. Tota validació del client és una cortesia; la de debò és al servidor.

  1. XMLHttpRequest, l'avantpassat

Abans de fetch hi havia XMLHttpRequest (XHR), disponible des de principis dels 2000. El veuràs en codi antic i val la pena reconèixer-lo:

// Estil XHR: esdeveniments i estats numèrics, sense promeses
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.tallernomada.example/v1/tasques');
xhr.onload = function () {
  if (xhr.status >= 200 && xhr.status < 300) {
    const tasques = JSON.parse(xhr.responseText);   // l'anàlisi, a mà
    console.log(tasques.length);
  } else {
    console.error('Error HTTP', xhr.status);
  }
};
xhr.onerror = function () { console.error('Fallada de xarxa'); };
xhr.send();

És l'estil de callbacks de 05-05 portat a l'extrem: res de promeses, res d'async/await, i el famós readyState amb els seus cinc valors numèrics. La comparació:

XMLHttpRequest fetch
Model Esdeveniments i callbacks Promeses (05-06)
Sintaxi open + send + onload Una crida que retorna una promesa
Anàlisi de JSON Manual (JSON.parse(responseText)) await resposta.json()
Errors HTTP (404, 500) Mires xhr.status Tampoc rebutja: mires resposta.ok
Cancel·lació xhr.abort() AbortController (07-03)
Progrés de pujada Sí, esdeveniment progress No (només de baixada, amb streams)
Streaming de la resposta Limitat Sí, resposta.body és un ReadableStream
Timeout integrat Sí, xhr.timeout No, cal muntar-lo (07-03)

Avui es fa servir fetch llevat de dos casos: quan necessites una barra de progrés de pujada d'un fitxer gran, o quan mantens codi antic. Els dos buits de fetch —timeout i cancel·lació— es cobreixen amb AbortController, i aquesta és la lliçó següent.

  1. HTTP en deu minuts: verbs, rutes, codis i capçaleres

Una petició HTTP té quatre parts: un verb, una ruta, unes capçaleres i de vegades un cos. La resposta té un codi d'estat, les seves pròpies capçaleres i el seu cos.

Els verbs

Verb Què significa Porta cos? Idempotent? A Nómada Tasques
GET Llegir un recurs No Llistar tasques, llegir una tasca
POST Crear un recurs nou No Crear una tasca
PUT Reemplaçar un recurs sencer Desar una tasca amb tots els seus camps
PATCH Modificar part d'un recurs Depèn Canviar només l'estat
DELETE Esborrar un recurs No sol Eliminar una tasca

Idempotent significa que repetir la mateixa petició deixa el sistema igual que fer-la una vegada. PUT de la tasca 6 amb les mateixes dades, mil vegades, deixa una tasca 6 amb aquelles dades. POST mil vegades crea mil tasques. Aquella distinció sembla teòrica fins que a 07-03 decideixis quines peticions es poden reintentar sense por.

Les rutes de recurs

Una API REST anomena coses, no accions, i deixa que el verb digui què fer-hi:

GET    /v1/tasques              → la col·lecció sencera
GET    /v1/tasques?estat=feta   → la col·lecció filtrada
GET    /v1/tasques/6            → un element concret
POST   /v1/tasques              → crear-ne un de nou (l'id l'assigna el servidor)
PUT    /v1/tasques/6            → reemplaçar el 6 sencer
PATCH  /v1/tasques/6            → canviar part del 6
DELETE /v1/tasques/6            → esborrar el 6

Un antipatró habitual és POST /v1/crearTasca o GET /v1/esborrarTasca?id=6. El segon és especialment dolent: un GET no ha de canviar res, i qualsevol cercador o precarregador del navegador el podria disparar.

El domini https://api.tallernomada.example/v1 que farem servir en tota la lliçó és fictici. El TLD .example està reservat per l'IANA precisament per a documentació i no resoldrà mai. A l'apartat 13 muntaràs un servidor real a la teva màquina per practicar.

Els codis d'estat

S'agrupen en famílies, i amb saber les famílies en vas sobrat:

Família Significat Els que veuràs
1xx Informatiu 101 Switching Protocols (el veuràs a WebSockets, 07-04)
2xx Èxit 200 OK, 201 Created (després d'un POST), 204 No Content (després d'un DELETE)
3xx Redirecció 301 permanent, 304 Not Modified (caché)
4xx Error del client: la petició està malament 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests
5xx Error del servidor: la petició estava bé, el servidor va fallar 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout

La frontera entre 4xx i 5xx és la que governa la teva gestió d'errors: un 4xx l'arregla el client (corregeix les dades, inicia sessió, deixa d'insistir); un 5xx no l'arregla el client (reintenta més tard i avisa). Dues parelles que es confonen sovint:

  • 401 Unauthorized significa «no sé qui ets» (falta o ha caducat la credencial). 403 Forbidden significa «sé qui ets i no pots». El primer demana iniciar sessió; el segon, no.
  • 204 No Content és un èxit sense cos. Cridar .json() sobre ell llança, perquè no hi ha res per analitzar. Ho tindràs en compte a esborrarTasca().

Les capçaleres

Capçalera Qui la posa Per a què
Content-Type Qui envia cos Quin format porta el cos: application/json, multipart/form-data
Accept El client Quins formats entén de tornada: application/json
Authorization El client La credencial: Bearer <token>
Cache-Control Tots dos Com es pot posar en memòria cau
Content-Length Qui envia cos Mida en bytes (la posa el navegador)
ETag / If-None-Match Servidor / client Versió del recurs, per estalviar transferència

L'error de novell més freqüent amb capçaleres: enviar un body amb JSON sense Content-Type: application/json. Molts servidors ho interpreten com a text pla i retornen un 400 desconcertant.

  1. fetch: la petició mínima

fetch(url, opcions) retorna una promesa que es resol amb un objecte Response així que arriben les capçaleres de la resposta. Amb el que saps de 05-06, es llegeix sense esforç:

// Amb async/await, que és com ho escriuràs sempre
async function llistar() {
  const resposta = await fetch('https://api.tallernomada.example/v1/tasques');
  const tasques = await resposta.json();
  console.log(tasques.length);
}

Dos await, i cadascun espera una cosa diferent. Això és el que més costa al principi:

sequenceDiagram
    participant JS as El teu codi
    participant N as Navegador
    participant S as API

    JS->>N: fetch(url)
    N->>S: GET /v1/tasques
    Note over N,S: Latència de xarxa
    S-->>N: 200 OK + capçaleres
    N-->>JS: ✅ promesa resolta amb Response
    Note over JS: El COS encara no ha arribat
    JS->>N: resposta.json()
    N-->>JS: (llegeix el cos, l'analitza)
    N-->>JS: ✅ segona promesa resolta amb les dades

La primera promesa es resol amb les capçaleres; el cos pot continuar arribant. Per això resposta.json() és una altra operació asíncrona i necessita el seu propi await. Oblidar-ho produeix l'error més comú de tots:

const resposta = await fetch(url);
const dades = resposta.json();           // ✗ falta l'await
console.log(dades.length);               // undefined  ← és una Promise, no un array

  1. L'objecte Response i els mètodes de cos

Response descriu la resposta completa:

Membre Tipus Què conté
ok boolean true si l'estat és entre 200 i 299
status number El codi: 200, 404, 500
statusText string El text: 'OK', 'Not Found'
headers Headers Les capçaleres, amb get, has, entries
url string La URL final (després de redireccions)
redirected boolean Si hi va haver redirecció
type string 'basic', 'cors', 'opaque'
bodyUsed boolean Si el cos ja s'ha consumit

I els mètodes de cos, tots asíncrons perquè tots retornen promeses:

Mètode Retorna Quan
json() El valor analitzat Respostes JSON. Llança si el cos no és JSON vàlid
text() string HTML, CSV, text, o per depurar què ha arribat de debò
blob() Blob Imatges, PDF, qualsevol binari
arrayBuffer() ArrayBuffer Binari a baix nivell (el faràs servir a 07-07 amb Wasm)
formData() FormData Respostes multipart o urlencoded
const resposta = await fetch('https://api.tallernomada.example/v1/tasques');

console.log(resposta.ok);                             // true
console.log(resposta.status, resposta.statusText);    // 200 'OK'
console.log(resposta.headers.get('content-type'));    // 'application/json; charset=utf-8'

for (const [nom, valor] of resposta.headers) {
  console.log(nom, '=', valor);
}

Els noms de capçalera no distingeixen majúscules: headers.get('Content-Type') i headers.get('content-type') són el mateix.

Una regla que sorprèn: el cos només es pot llegir una vegada. És un flux, i un cop consumit, s'ha acabat.

const resposta = await fetch(url);
const text = await resposta.text();
const dades = await resposta.json();   // ✗ TypeError: body stream already read

Si necessites llegir-lo dues vegades —per exemple, intentar json() i, si falla, mirar el text() per depurar—, clona abans:

const resposta = await fetch(url);
const copia = resposta.clone();         // ← clona ABANS de llegir

try {
  return await resposta.json();
} catch {
  console.error('La resposta no era JSON. Contingut real:', await copia.text());
  throw new Error('Resposta no interpretable');
}

Aquell patró et salvarà el dia que un proxy retorni una pàgina HTML d'error on esperaves JSON.

  1. El parany fonamental: fetch no rebutja amb 404 ni 500

Aquesta és la cosa que cal saber de fetch, i la que més codi trencat ha produït:

La promesa de fetch només es rebutja si la petició no va arribar a completar-se: xarxa caiguda, DNS que no resol, CORS bloquejat, petició cancel·lada. Si el servidor respon —encara que respongui 404, 403 o 500— la promesa es resol amb normalitat.

// ✗ Trencat: sembla correcte i no ho és
async function llistarTrencat() {
  try {
    const resposta = await fetch('https://api.tallernomada.example/v1/tasquez');   // ruta mal escrita
    const dades = await resposta.json();      // el servidor ha retornat 404 amb un JSON d'error
    return dades;                             // retorna { error: 'No trobat' } com si fossin tasques
  } catch (error) {
    console.error('Mai arribo aquí per un 404');
  }
}

El catch no s'executa. resposta.json() analitza el cos de l'error sense queixar-se, i la teva aplicació continua endavant amb escombraries. La correcció és sempre comprovar ok:

// ✓ Correcte
async function llistar() {
  const resposta = await fetch('https://api.tallernomada.example/v1/tasques');

  if (!resposta.ok) {                                   // ← la línia que no pot faltar
    throw new Error(`HTTP ${resposta.status} ${resposta.statusText}`);
  }
  return resposta.json();
}

La taula que explica el perquè del disseny:

Situació La promesa de fetch…? Com ho detectes
200 OK es resol resposta.ok === true
404 Not Found es resol resposta.ok === false, status 404
500 Internal Server Error es resol resposta.ok === false, status 500
Sense connexió / DNS falla es rebutja TypeError: Failed to fetch
Bloquejat per CORS es rebutja TypeError, amb detall només a la consola
Petició cancel·lada es rebutja AbortError (07-03)

La lògica del disseny és defensable: fetch promet fer la petició HTTP, i un 404 és una petició HTTP feta amb èxit la resposta de la qual és «no existeix». Que sigui defensable no treu que sigui la primera causa de bugs de xarxa. A l'apartat 12 encapsularem la comprovació una sola vegada per no tornar a oblidar-la.

Fixa't a més en el detall de l'última fila del diagnòstic: quan CORS bloqueja una petició, JavaScript rep un TypeError genèric sense detalls. El motiu real només apareix a la consola del navegador. És intencionat —donar detalls seria filtrar informació entre orígens— i significa que la consola és la teva única font de veritat davant d'una fallada de CORS.

  1. Enviar dades: method, headers i body

El segon paràmetre de fetch és un objecte d'opcions. Per crear una tasca:

const nova = {
  titol: 'Revisar la premsa de serigrafia',
  responsable: 'Iván',
  prioritat: 'alta',
  etiquetes: ['serigrafia', 'manteniment'],
  horesEstimades: 4,
  dataLimit: '2026-10-10'
};

const resposta = await fetch('https://api.tallernomada.example/v1/tasques', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',      // què envio
    'Accept': 'application/json'             // què espero rebre
  },
  body: JSON.stringify(nova)                 // ← el cos SEMPRE és text o binari
});

if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);

const creada = await resposta.json();
console.log(creada.id);                      // 7  ← l'id l'assigna el servidor (R1)

Quatre punts:

  • body mai és un objecte. Si li passes { titol: '…' } directament, el navegador el converteix amb String() i envia '[object Object]', exactament el mateix desastre que a localStorage. JSON.stringify és obligatori.
  • JSON.stringify fa servir el toJSON de les teves instàncies, així que li pots passar una Tasca directament i viatjarà completa, camps privats inclosos. Un altre cop 05-03 rendint.
  • L'id l'assigna el servidor, no el client. És la regla R1 del projecte, i en un sistema amb diversos usuaris és l'única manera de no col·lisionar.
  • fetch no llança si el servidor rebutja les dades. Un 422 Unprocessable Entity amb la llista de camps invàlids arriba com a resposta normal; cal comprovar ok i llegir el cos de l'error.

Les opcions que més faràs servir:

Opció Valors Per a què
method 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' El verb (per defecte 'GET')
headers Objecte o Headers Les capçaleres
body string, FormData, Blob, URLSearchParams El cos
credentials 'omit', 'same-origin', 'include' Si hi van cookies
mode 'cors', 'same-origin', 'no-cors' Política d'origen creuat
cache 'default', 'no-store', 'reload' Ús de la caché HTTP
signal AbortSignal Cancel·lació (07-03)
redirect 'follow', 'error', 'manual' Què fer amb els 3xx

  1. Enviar FormData i fitxers

Quan hi ha un fitxer pel mig —l'Iván vol adjuntar la foto de l'esbós—, JSON no serveix: és text. Es fa servir FormData, que ja coneixes de 06-07:

const formulari = document.querySelector('#nova-tasca');
const dades = new FormData(formulari);       // agafa tots els camps amb name
dades.append('adjunt', inputFitxer.files[0]);
dades.append('origen', 'web');

const resposta = await fetch('https://api.tallernomada.example/v1/tasques', {
  method: 'POST',
  body: dades                                // ← SENSE Content-Type!
});

No posis Content-Type quan envies FormData. El navegador el genera sol, i hi afegeix el boundary —un separador aleatori— que el servidor necessita per trossejar el cos. Si l'escrius tu, el boundary falta i el servidor no pot llegir res. És una fallada desconcertant, perquè el codi «sembla més complet» amb la capçalera posada.

Comparació ràpida dels tres formats de cos:

Format Content-Type El poses tu? Quan
JSON application/json El normal en una API
FormData multipart/form-data; boundary=… No Fitxers, o formularis tal qual
URLSearchParams application/x-www-form-urlencoded No APIs antigues, formularis simples
// URLSearchParams com a cos: el navegador posa el Content-Type correcte
const cos = new URLSearchParams({ estat: 'feta', revisor: 'Marta' });
await fetch('https://api.tallernomada.example/v1/tasques/6', { method: 'PATCH', body: cos });

  1. Paràmetres de consulta amb URL i URLSearchParams

Construir la cadena de consulta a mà és una font inesgotable de bugs de codificació:

// ✗ Fràgil: i si el text té un espai, un & o una ç?
const url = `https://api.tallernomada.example/v1/tasques?responsable=${responsable}&text=${text}`;
// Amb responsable = 'Lucía' i text = 'premsa & corró' → URL trencada

Les classes URL i URLSearchParams codifiquen per tu:

const url = new URL('https://api.tallernomada.example/v1/tasques');
url.searchParams.set('responsable', 'Lucía');
url.searchParams.set('estat', 'pendent');
url.searchParams.set('text', 'premsa & corró');
url.searchParams.set('limit', 20);

console.log(url.toString());
// https://api.tallernomada.example/v1/tasques?responsable=Luc%C3%ADa&estat=pendent&text=premsa+%26+corr%C3%B3&limit=20

const resposta = await fetch(url);           // fetch accepta un objecte URL, sense toString()

Els membres útils d'URLSearchParams:

Mètode Què fa
set(clau, valor) Posa el valor, reemplaçant els que hi hagués
append(clau, valor) Afegeix un altre valor amb la mateixa clau (?etiqueta=a&etiqueta=b)
get(clau) / getAll(clau) Llegeix el primer / tots
has(clau) Si existeix
delete(clau) El treu
toString() La cadena codificada, sense la ?

Un ajudant que faràs servir al projecte, saltant-se els filtres buits:

/** Construeix una URL de l'API amb només els paràmetres que tenen valor. */
function urlDeApi(ruta, parametres = {}) {
  const url = new URL(ruta, BASE);
  for (const [clau, valor] of Object.entries(parametres)) {
    if (valor === undefined || valor === null || valor === '') continue;   // filtre buit = sense filtre
    url.searchParams.set(clau, valor);
  }
  return url;
}

urlDeApi('/v1/tasques', { responsable: 'Iván', estat: '', text: null });
// https://api.tallernomada.example/v1/tasques?responsable=Iv%C3%A1n

Aquell new URL(ruta, BASE) amb dos arguments resol rutes relatives contra una base, igual que ho fa un <a href>. Molt còmode per no concatenar barres.

  1. CORS: per què el navegador et bloqueja

Tard o d'hora escriuràs una petició perfecta i la consola dirà una cosa com:

Access to fetch at 'https://api.tallernomada.example/v1/tasques' from origin
'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin'
header is present on the requested resource.

CORS (Cross-Origin Resource Sharing) és el mecanisme que decideix si una pàgina d'un origen pot llegir respostes d'un altre origen. Per defecte, el navegador aplica la same-origin policy: pots enviar peticions a altres orígens, però no llegir-ne les respostes tret que aquell servidor ho autoritzi.

Per què existeix? Perquè el navegador envia automàticament les cookies de l'usuari. Sense aquesta política, qualsevol pàgina maliciosa que visitessis podria demanar https://elteubanc.example/api/saldo amb les teves cookies i llegir la resposta.

Hi ha dos tipus de petició creuada, i la diferència importa molt:

Petició simple: el navegador l'envia directament i després decideix si et deixa llegir la resposta. Es considera simple si fa servir GET, HEAD o POST, i les seves capçaleres són les corrents, amb un Content-Type limitat a text/plain, multipart/form-data o application/x-www-form-urlencoded.

sequenceDiagram
    participant P as Pàgina<br/>localhost:3000
    participant N as Navegador
    participant S as api.tallernomada.example

    P->>N: fetch('https://api…/v1/tasques')
    N->>S: GET /v1/tasques<br/>Origin: http://localhost:3000
    S-->>N: 200 OK<br/>Access-Control-Allow-Origin: http://localhost:3000
    N->>N: La capçalera autoritza el meu origen?
    N-->>P: ✅ Resposta lliurada

Petició amb preflight: qualsevol altra cosa —PUT, PATCH, DELETE, o un POST amb Content-Type: application/json, o una capçalera Authorization— obliga el navegador a preguntar abans amb una petició OPTIONS.

sequenceDiagram
    participant P as Pàgina<br/>localhost:3000
    participant N as Navegador
    participant S as api.tallernomada.example

    P->>N: fetch(url, { method: 'PATCH', headers: {…} })
    Note over N: PATCH + Content-Type JSON<br/>⇒ cal preflight
    N->>S: OPTIONS /v1/tasques/6<br/>Origin: http://localhost:3000<br/>Access-Control-Request-Method: PATCH<br/>Access-Control-Request-Headers: content-type
    S-->>N: 204 No Content<br/>Access-Control-Allow-Origin: http://localhost:3000<br/>Access-Control-Allow-Methods: GET, POST, PATCH, DELETE<br/>Access-Control-Allow-Headers: content-type<br/>Access-Control-Max-Age: 86400
    N->>N: Autoritzat ✅
    N->>S: PATCH /v1/tasques/6 (la petició real)
    S-->>N: 200 OK + Access-Control-Allow-Origin
    N-->>P: ✅ Resposta lliurada

Aquell OPTIONS extra explica dues coses que desconcerten: per què a la pestanya Network apareixen dues entrades per una sola crida, i per què de vegades la fallada passa «abans» que el servidor vegi la teva petició real.

Les capçaleres que controla el servidor:

Capçalera de resposta Què autoritza
Access-Control-Allow-Origin Quin origen pot llegir (https://app.taller.example o *)
Access-Control-Allow-Methods Quins verbs es permeten
Access-Control-Allow-Headers Quines capçaleres pot enviar el client
Access-Control-Allow-Credentials Si es poden enviar cookies (true)
Access-Control-Expose-Headers Quines capçaleres de resposta pot llegir el teu JavaScript
Access-Control-Max-Age Quants segons es posa el preflight en memòria cau

I la conclusió que estalvia tardes senceres:

CORS no s'arregla des del client. Ni amb mode, ni amb capçaleres, ni amb trucs. L'aplica el navegador segons el que respon el servidor. Només hi ha tres sortides legítimes: que el servidor afegeixi les capçaleres, posar un proxy al teu mateix origen que reenviï les peticions, o servir API i web sota el mateix origen.

Dos matisos que es malinterpreten:

  • mode: 'no-cors' no desactiva CORS. Et retorna una resposta opaca: status 0, ok false i cos illegible. Serveix per a casos molt concrets (precarregar en memòria cau des d'un service worker), no per saltar-se res.
  • Les extensions tipus «Allow CORS» del navegador només desactiven la comprovació a la teva màquina. El teu codi continuarà trencat per a tota la resta. Fes-les servir, com a molt, per diagnosticar.
  • Access-Control-Allow-Origin: * és incompatible amb credencials. Si envies cookies, el servidor ha d'anomenar el teu origen exacte.

  1. Autenticació: Authorization i credentials

Hi ha dues maneres que l'API sàpiga qui ets.

Token a la capçalera Authorization, el patró habitual de les APIs:

const resposta = await fetch('https://api.tallernomada.example/v1/tasques', {
  headers: { 'Authorization': `Bearer ${token}` }
});

Cookies, que el navegador gestiona sol. Amb fetch cal demanar-les explícitament quan l'API és en un altre origen:

credentials Comportament
'same-origin' Per defecte: cookies només si la URL és del mateix origen
'include' Cookies sempre, també entre orígens (requereix Access-Control-Allow-Credentials: true)
'omit' No envia mai cookies
await fetch('https://api.tallernomada.example/v1/tasques', { credentials: 'include' });

Quina triar? Reprenent l'advertiment de 07-01:

Token a localStorage Cookie HttpOnly
El llegeix un XSS? , sencer No, JavaScript no la veu
S'envia sol? No, el poses tu a cada petició Sí, el navegador
Risc de CSRF Baix Existeix; es mitiga amb SameSite i tokens anti-CSRF
Caducitat La gestiones tu El servidor, amb Max-Age

La recomanació no ha canviat: per a una sessió d'usuari real, cookie HttpOnly; Secure; SameSite=Lax, emesa i validada pel servidor. Si la teva API imposa un token a localStorage —cosa habitual—, assumeix que un XSS és un robatori de compte i tracta la prevenció d'XSS (06-02: textContent en lloc d'innerHTML) com a part de la seguretat de l'autenticació.

Tres regles més, curtes i no negociables:

  • HTTPS sempre. Sobre http://, capçaleres i cossos viatgen llegibles per a qualsevol a la xarxa. Un token enviat per HTTP és un token públic.
  • No fiquis mai claus d'API al codi del client. Tot el que arriba al navegador és visible. Si una clau ha de romandre secreta, la petició la fa el teu servidor, no la pàgina.
  • Davant d'un 401, neteja la sessió local i envia a iniciar sessió. Reintentar amb un token caducat només genera soroll.

  1. Nómada Tasques: js/dades/api-tasques.js

Ja pots escriure el germà de xarxa de RepositoriLocal. La mateixa responsabilitat —traduir entre el món exterior i el model— amb un altre mitjà.

// js/dades/api-tasques.js
import { Tasca } from '../model/tasca.js';

/**
 * API fictícia d'exemple. El TLD .example està reservat i NO resol MAI:
 * per practicar de debò, arrenca el json-server de l'apartat 13 i canvia aquesta
 * constant per 'http://localhost:3000'.
 */
const BASE = 'https://api.tallernomada.example/v1';

const CAPCALERES_JSON = {
  'Content-Type': 'application/json',
  'Accept': 'application/json'
};

/** Uneix la base amb la ruta i afegeix només els paràmetres amb valor. */
function construirUrl(ruta, parametres = {}) {
  const url = new URL(BASE + ruta);
  for (const [clau, valor] of Object.entries(parametres)) {
    if (valor === undefined || valor === null || valor === '') continue;
    url.searchParams.set(clau, valor);
  }
  return url;
}

/**
 * Comprovació única d'`ok` per a tota l'aplicació: escrita una vegada, impossible d'oblidar.
 * A 07-03 aquesta funció creixerà fins a convertir-se en `demanarJson`, amb el seu ErrorDeApi.
 */
async function comprovar(resposta) {
  if (resposta.ok) return resposta;
  const detall = await resposta.text().catch(() => '');
  throw new Error(`HTTP ${resposta.status} ${resposta.statusText}${detall ? ` — ${detall}` : ''}`);
}

/** GET /v1/tasques → array d'instàncies Tasca (no d'objectes plans). */
export async function llistarTasques({ responsable, estat, text } = {}) {
  const resposta = await fetch(construirUrl('/tasques', { responsable, estat, text }), {
    headers: { 'Accept': 'application/json' }
  });
  await comprovar(resposta);
  const planes = await resposta.json();
  return planes.map((dades) => Tasca.desDeJSON(dades));      // ← mateixa frontera que a 07-01
}

/** GET /v1/tasques/:id */
export async function obtenirTasca(id) {
  const resposta = await fetch(construirUrl(`/tasques/${id}`), {
    headers: { 'Accept': 'application/json' }
  });
  await comprovar(resposta);
  return Tasca.desDeJSON(await resposta.json());
}

/** POST /v1/tasques → 201 Created amb la tasca ja proveïda d'id (R1: l'assigna el servidor). */
export async function crearTasca(dades) {
  const resposta = await fetch(construirUrl('/tasques'), {
    method: 'POST',
    headers: CAPCALERES_JSON,
    body: JSON.stringify(dades)              // si `dades` és una Tasca, el seu toJSON() la serialitza sencera
  });
  await comprovar(resposta);
  return Tasca.desDeJSON(await resposta.json());
}

/** PATCH /v1/tasques/:id → canvis parcials; PUT reemplaçaria la tasca sencera. */
export async function actualitzarTasca(id, canvis) {
  const resposta = await fetch(construirUrl(`/tasques/${id}`), {
    method: 'PATCH',
    headers: CAPCALERES_JSON,
    body: JSON.stringify(canvis)
  });
  await comprovar(resposta);
  return Tasca.desDeJSON(await resposta.json());
}

/** DELETE /v1/tasques/:id → normalment 204 No Content, SENSE cos per analitzar. */
export async function esborrarTasca(id) {
  const resposta = await fetch(construirUrl(`/tasques/${id}`), { method: 'DELETE' });
  await comprovar(resposta);
  return true;                               // ← res de resposta.json(): un 204 no té cos
}

Quatre decisions per subratllar:

  • La frontera del model es respecta. El mòdul retorna instàncies de Tasca, no objectes plans, exactament com feia RepositoriLocal. La resta de l'aplicació no sap d'on venen les dades. Aquest és el sentit de tenir una carpeta dades/.
  • comprovar està escrita una sola vegada. El parany de l'apartat 6 es neutralitza encapsulant-lo, no recordant-lo.
  • esborrarTasca no crida json(). Un 204 No Content no té cos i json() llançaria.
  • Aquí no hi ha try/catch. Aquest mòdul tradueix i propaga; qui decideix què mostrar a l'usuari és la vista. Com classificar i presentar aquells errors és justament el tema de 07-03.

I així se substitueixen per fi les funcions simulades de 05-06:

// js/app.js — abans
// const backlog = await llegirBacklogSimulat();    // setTimeout fingint latència

// js/app.js — ara
import { llistarTasques } from './dades/api-tasques.js';
import { RepositoriLocal } from './dades/repositori-local.js';

const repositori = new RepositoriLocal();

async function arrencar() {
  // 1 · Pintar immediatament el que hi hagi en local: la interfície no espera la xarxa
  const local = repositori.carregar();
  if (local !== null) { vista.actualitzar({ tauler: local }); }

  // 2 · Portar la veritat del servidor i refrescar
  const tasques = await llistarTasques();
  const tauler = new Tauler('Taller Nómada', tasques);
  vista.actualitzar({ tauler });
  repositori.desar(tauler);                  // ← la còpia local queda al dia
}

arrencar();

Aquí hi ha el patró que fa que una aplicació es percebi ràpida: local primer, xarxa després. L'emmagatzematge de 07-01 i la xarxa d'aquesta lliçó no competeixen; es complementen. (Aquell await despullat, sense try/catch ni estat de càrrega, encara està a mitges: el completaràs a la lliçó següent.)

  1. Practicar de debò: json-server a la teva màquina

api.tallernomada.example no existeix. Per practicar necessites un servidor real, i el més ràpid de muntar és json-server, que converteix un fitxer JSON en una API REST completa.

# 1 · Crea una carpeta per al servidor de proves
mkdir nomada-api && cd nomada-api

# 2 · Arrenca'l directament amb npx (no cal instal·lar res permanent)
npx json-server --watch db.json --port 3000

Amb aquest db.json, que és el backlog canònic del projecte:

{
  "tasques": [
    { "id": 1, "titol": "Redissenyar la sala polivalent", "responsable": "Iván",
      "prioritat": "alta", "estat": "en-curs", "etiquetes": ["disseny", "espai"],
      "horesEstimades": 12, "dataLimit": "2026-09-30", "revisor": "Marta" },
    { "id": 2, "titol": "Migrar el web del taller", "responsable": "Lucía",
      "prioritat": "mitjana", "estat": "pendent", "etiquetes": ["web"],
      "horesEstimades": 8, "dataLimit": "2026-10-15", "revisor": "Iván" },
    { "id": 3, "titol": "Catàleg d'enquadernació", "responsable": "Iván",
      "prioritat": "mitjana", "estat": "pendent", "etiquetes": ["disseny", "enquadernació"],
      "horesEstimades": 8, "dataLimit": "2026-10-05", "revisor": "Marta" },
    { "id": 4, "titol": "Inventari del magatzem de serigrafia", "responsable": "Lucía",
      "prioritat": "baixa", "estat": "feta", "etiquetes": ["taller", "inventari"],
      "horesEstimades": 3, "dataLimit": "2026-09-12", "revisor": "Marta" },
    { "id": 5, "titol": "Preparar la jornada de portes obertes", "responsable": "Marta",
      "prioritat": "alta", "estat": "en-curs", "etiquetes": ["esdeveniment"],
      "horesEstimades": 6, "dataLimit": "2026-11-20", "revisor": "Lucía" },
    { "id": 6, "titol": "Pressupost de la fusteria", "responsable": "Iván",
      "prioritat": "alta", "estat": "pendent", "etiquetes": ["fusteria", "compres"],
      "horesEstimades": 5, "dataLimit": "2026-09-05", "revisor": "Marta" }
  ]
}

Aquell fitxer dona immediatament totes les rutes que necessites:

curl http://localhost:3000/tasques
curl http://localhost:3000/tasques/6
curl "http://localhost:3000/tasques?responsable=Iván&estat=pendent"
curl -X POST http://localhost:3000/tasques \
     -H "Content-Type: application/json" \
     -d '{"titol":"Revisar la premsa","responsable":"Iván","horesEstimades":4}'
curl -X PATCH http://localhost:3000/tasques/6 \
     -H "Content-Type: application/json" -d '{"estat":"en-curs"}'
curl -X DELETE http://localhost:3000/tasques/6

Només has de canviar una línia al teu mòdul:

const BASE = 'http://localhost:3000';        // ← en lloc de l'API fictícia

I json-server ja envia Access-Control-Allow-Origin: *, així que no barallaràs amb CORS mentre aprens. Altres opcions per practicar: https://jsonplaceholder.typicode.com (API pública de només lectura simulada), https://httpbin.org (retorna la teva pròpia petició, ideal per inspeccionar capçaleres) o https://httpstat.us/500 (retorna el codi que li demanis, perfecte per provar la gestió d'errors de la lliçó següent).

Errors Habituals i Consells

  • Oblidar comprovar resposta.ok. L'error número u. Un 404 arriba com a èxit i la teva aplicació processa un missatge d'error com si fossin dades.
  • Oblidar l'await de resposta.json(). Obtens una Promise i tot el que hi facis dona undefined.
  • Passar un objecte com a body. Es converteix en '[object Object]'. JSON.stringify sempre.
  • Posar Content-Type en enviar FormData. Trenca el boundary i el servidor no pot llegir el cos.
  • Cridar .json() sobre un 204. No hi ha cos; llança. Comprova status === 204 o Content-Length.
  • Llegir el cos dues vegades. TypeError: body stream already read. Fes servir resposta.clone() abans de la primera lectura.
  • Concatenar paràmetres a mà. Un espai, un & o un accent trenquen la URL. Fes servir URL i URLSearchParams.
  • Intentar arreglar CORS des del client. No es pot. Llegeix la consola, parla amb qui manté l'API o munta un proxy.
  • Fer servir mode: 'no-cors' per «saltar-se» CORS. Retorna una resposta opaca i illegible.
  • Ficar una clau d'API al JavaScript del client. És pública des del moment en què es descarrega.
  • Consell: mira sempre la pestanya Network de les DevTools. Verb, codi, capçaleres, cos enviat i rebut, i l'OPTIONS del preflight. Gairebé tots els problemes de xarxa es diagnostiquen allà en trenta segons.
  • Consell: fes servir await resposta.text() quan json() falli. Veuràs si el servidor ha retornat una pàgina HTML d'error en lloc de JSON.
  • Consell: encapsula fetch en un únic mòdul. No el cridis mai solt des de la vista. Així la comprovació d'ok, la base de la URL i les capçaleres viuen en un sol lloc, i a 07-03 podràs afegir timeouts i reintents sense tocar la resta.
  • Consell: al navegador, copy(await (await fetch(url)).json()) a la consola copia la resposta al porta-retalls. Molt útil per inspeccionar formats.

Exercicis

Exercici 1 — L'embolcall que no es pot oblidar. Escriu demanarJson(url, opcions = {}) que: faci el fetch; si resposta.ok és fals, llegeixi el cos com a text i llanci un Error el missatge del qual inclogui el status, el statusText i aquell text; si l'estat és 204 retorni null; i en cas contrari retorni await resposta.json(). Reescriu després llistarTasques i esborrarTasca fent-lo servir, i comprova que el codi queda més curt.

Exercici 2 — Cercador de tasques amb paràmetres. Escriu cercarTasques({ text, responsable, estats, ordre, pagina }) que construeixi la URL amb URLSearchParams, ometent els valors buits. estats és un array (['pendent', 'en-curs']) i ha de generar ?estat=pendent&estat=en-curs. La paginació fa servir _page i _limit, els paràmetres de json-server. Retorna { tasques, total } llegint el total de la capçalera X-Total-Count.

Exercici 3 — Sincronitzar el tauler local amb el servidor. Escriu sincronitzar(repositori, api) que: carregui el tauler local; demani al servidor la llista de tasques; i retorni un informe { nomesLocal, nomesServidor, enTotsDos, coincideixen } comparant per id, on coincideixen és el nombre de tasques presents als dos costats amb el mateix estat. No modifiquis res encara: només informa. Fes servir Map i els mètodes d'array de 04-05.

Solucions

Solució 1

export async function demanarJson(url, opcions = {}) {
  const resposta = await fetch(url, opcions);

  if (!resposta.ok) {
    // El cos de l'error sol portar el detall útil; si no es pot llegir, continuem igual
    const detall = await resposta.text().catch(() => '');
    throw new Error(`HTTP ${resposta.status} ${resposta.statusText}${detall ? ` — ${detall}` : ''}`);
  }

  if (resposta.status === 204) return null;                 // sense cos

  const tipus = resposta.headers.get('content-type') ?? '';
  if (!tipus.includes('application/json')) {
    throw new Error(`S'esperava JSON i ha arribat "${tipus}"`);   // proxy, login HTML, error del CDN…
  }
  return resposta.json();
}
export async function llistarTasques(filtres = {}) {
  const planes = await demanarJson(construirUrl('/tasques', filtres), { headers: { Accept: 'application/json' } });
  return planes.map((d) => Tasca.desDeJSON(d));
}

export async function esborrarTasca(id) {
  await demanarJson(construirUrl(`/tasques/${id}`), { method: 'DELETE' });   // 204 → null, sense trencar-se
  return true;
}

La comprovació del content-type és la que evita la pitjor de les fallades: un proxy corporatiu o una pantalla d'inici de sessió retornen HTML amb estat 200, i sense aquella línia el json() llançaria un SyntaxError incomprensible. Aquesta funció és el germen del demanarJson complet que construiràs a 07-03.

Solució 2

export async function cercarTasques({ text = '', responsable = null, estats = [],
                                      ordre = 'dataLimit', pagina = 1, perPagina = 20 } = {}) {
  const url = new URL(`${BASE}/tasques`);

  if (text) url.searchParams.set('q', text);                       // cerca lliure de json-server
  if (responsable) url.searchParams.set('responsable', responsable);
  for (const estat of estats) url.searchParams.append('estat', estat);   // append, no set
  url.searchParams.set('_sort', ordre);
  url.searchParams.set('_page', pagina);
  url.searchParams.set('_limit', perPagina);

  const resposta = await fetch(url, { headers: { Accept: 'application/json' } });
  if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);

  const planes = await resposta.json();
  return {
    tasques: planes.map((d) => Tasca.desDeJSON(d)),
    total: Number(resposta.headers.get('X-Total-Count') ?? planes.length)
  };
}

La diferència entre set i append és la clau: set reemplaça, així que un bucle amb set deixaria només l'últim estat. I compte amb X-Total-Count: en una API d'un altre origen, aquella capçalera només serà llegible si el servidor l'exposa amb Access-Control-Expose-Headers. Amb json-server a localhost no hi ha problema.

Solució 3

export async function sincronitzar(repositori, api) {
  const local = repositori.carregar();
  const tasquesLocals = local === null ? [] : local.tasques;
  const tasquesRemotes = await api.llistarTasques();

  const perIdLocal = new Map(tasquesLocals.map((t) => [t.id, t]));
  const perIdRemot = new Map(tasquesRemotes.map((t) => [t.id, t]));

  const nomesLocal    = tasquesLocals.filter((t) => !perIdRemot.has(t.id)).map((t) => t.id);
  const nomesServidor = tasquesRemotes.filter((t) => !perIdLocal.has(t.id)).map((t) => t.id);
  const enTotsDos     = tasquesLocals.filter((t) => perIdRemot.has(t.id)).map((t) => t.id);

  const coincideixen = enTotsDos.filter((id) => perIdLocal.get(id).estat === perIdRemot.get(id).estat).length;

  return { nomesLocal, nomesServidor, enTotsDos, coincideixen, conflictes: enTotsDos.length - coincideixen };
}
console.log(await sincronitzar(repositori, api));
// { nomesLocal: [7], nomesServidor: [], enTotsDos: [1,2,3,4,5,6], coincideixen: 5, conflictes: 1 }

Els dos Map converteixen la comparació en operacions de cost constant en lloc de recórrer l'array remot per cada tasca local. I fixa't en el que revela el resultat: hi ha un conflicte. Què fer-hi —qui guanya quan local i servidor discrepen— és una decisió de producte, no tècnica, i hi tornaràs a 07-04 en parlar d'edició simultània.

Conclusió

Nómada Tasques ja sap parlar amb un servidor. Entens què significa AJAX i per què va canviar el web: demanar dades en segon pla i actualitzar només el que canvia, conservant scroll, focus i filtres, amb un JSON d'uns centenars de bytes en lloc d'un document HTML complet. Coneixes XMLHttpRequest prou per reconèixer-lo, i saps que fetch el substitueix en tot llevat del progrés de pujada, i que els seus dos buits —timeout i cancel·lació— s'omplen amb AbortController.

Tens l'HTTP que es fa servir cada dia: els verbs amb el seu significat i la seva idempotència (GET, PUT i DELETE sí, POST no), les rutes que anomenen recursos i no accions, les cinc famílies de codis amb la frontera decisiva entre el 4xx que arregla el client i el 5xx que no, i les capçaleres Content-Type, Accept i Authorization. Domines fetch i la seva doble espera —la primera promesa porta les capçaleres, la segona el cos—, l'objecte Response amb ok, status, headers i els cinc mètodes de cos, i la regla que el cos es llegeix una sola vegada tret que el clonis. I portes gravat el parany que defineix aquesta API: fetch no rebutja amb 404 ni amb 500; només rebutja si la petició no va arribar a completar-se. Per això la comprovació de resposta.ok no es recorda, s'encapsula.

Saps enviar dades amb method, headers i bodyJSON.stringify obligatori, i Content-Type prohibit quan el cos és FormData—, construir URLs amb URL i URLSearchParams distingint set d'append, i explicar CORS: la same-origin policy, la petició simple davant del preflight OPTIONS que duplica les entrades a la pestanya Network, les capçaleres Access-Control-* que decideix el servidor, i la conclusió que estalvia tardes senceres —no s'arregla des del client, i mode: 'no-cors' només et dona una resposta opaca—. I tens clara la part de seguretat: HTTPS sempre, tokens preferiblement en cookie HttpOnly i no a localStorage, cap clau secreta al codi del client, i cap confiança en la validació del navegador.

En codi, el mòdul js/dades/api-tasques.js amb llistarTasques, obtenirTasca, crearTasca, actualitzarTasca i esborrarTasca, que retorna instàncies de Tasca i no objectes plans, igual que feia RepositoriLocal: la capa dades/ és una frontera, i la resta de l'aplicació no sap si les dades venen del disc o de la xarxa. I un json-server a localhost:3000 amb el backlog canònic perquè puguis practicar contra un servidor de debò, perquè api.tallernomada.example és i continuarà sent fictici.

Ara bé: aquell codi funciona només quan tot va bé. I a la xarxa res va bé tota l'estona. El wifi del taller cau a mitja POST; l'API triga quinze segons i la Marta prem el botó tres vegades; el servidor retorna un 503 perquè estan desplegant; l'Iván escriu al cercador i es llancen vuit peticions de les quals arriba abans la penúltima, deixant a la pantalla resultats que no corresponen al que va escriure. Cap d'aquelles situacions la cobreix el que has escrit avui: no hi ha timeout, no hi ha cancel·lació, no hi ha reintents, no hi ha estat de càrrega i no hi ha manera de dir a l'usuari què ha passat. Això és exactament el que separa una demostració d'una aplicació, i és el tema de Peticions Robustes: Errors, Timeouts i AbortController, on AbortController —presentat de passada a 06-04— per fi ocuparà el lloc que li correspon.

Curs de JavaScript: De Principiant a Avançat

Mòdul 1: Introducció a JavaScript

Mòdul 2: Estructures de Control

Mòdul 3: Funcions

Mòdul 4: Objectes i Arrays

Mòdul 5: Objectes i Funcions Avançades

Mòdul 6: El Model d'Objectes del Document (DOM)

Mòdul 7: APIs del Navegador i Temes Avançats

Mòdul 8: Proves i Depuració

Mòdul 9: Rendiment i Optimització

Mòdul 10: Frameworks i Llibreries de JavaScript

Mòdul 11: Projecte Final

© Copyright 2026. Tots els drets reservats