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
- Què és AJAX i què va canviar
XMLHttpRequest, l'avantpassat- HTTP en deu minuts: verbs, rutes, codis i capçaleres
fetch: la petició mínima- L'objecte
Responsei els mètodes de cos - El parany fonamental:
fetchno rebutja amb 404 ni 500 - Enviar dades:
method,headersibody - Enviar
FormDatai fitxers - Paràmetres de consulta amb
URLiURLSearchParams - CORS: per què el navegador et bloqueja
- Autenticació:
Authorizationicredentials - Nómada Tasques:
js/dades/api-tasques.js - Practicar de debò:
json-servera la teva màquina - Errors Habituals i Consells
- Exercicis
- Conclusió
- 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.
XMLHttpRequest, l'avantpassat
XMLHttpRequest, l'avantpassatAbans 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.
- 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 | Sí | Llistar tasques, llegir una tasca |
POST |
Crear un recurs nou | Sí | No | Crear una tasca |
PUT |
Reemplaçar un recurs sencer | Sí | Sí | Desar una tasca amb tots els seus camps |
PATCH |
Modificar part d'un recurs | Sí | Depèn | Canviar només l'estat |
DELETE |
Esborrar un recurs | No sol | Sí | 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/v1que farem servir en tota la lliçó és fictici. El TLD.exampleestà 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 Unauthorizedsignifica «no sé qui ets» (falta o ha caducat la credencial).403 Forbiddensignifica «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 aesborrarTasca().
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.
fetch: la petició mínima
fetch: la petició mínimafetch(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
- L'objecte
Response i els mètodes de cos
Response i els mètodes de cosResponse 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 readSi 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.
- El parany fonamental:
fetch no rebutja amb 404 ni 500
fetch no rebutja amb 404 ni 500Aquesta és la cosa que cal saber de fetch, i la que més codi trencat ha produït:
La promesa de
fetchnomé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.
- Enviar dades:
method, headers i body
method, headers i bodyEl 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:
bodymai és un objecte. Si li passes{ titol: '…' }directament, el navegador el converteix ambString()i envia'[object Object]', exactament el mateix desastre que alocalStorage.JSON.stringifyés obligatori.JSON.stringifyfa servir eltoJSONde les teves instàncies, així que li pots passar unaTascadirectament i viatjarà completa, camps privats inclosos. Un altre cop 05-03 rendint.- L'
idl'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. fetchno llança si el servidor rebutja les dades. Un422 Unprocessable Entityamb la llista de camps invàlids arriba com a resposta normal; cal comprovaroki 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 |
- Enviar
FormData i fitxers
FormData i fitxersQuan 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 |
Sí | 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 });
- Paràmetres de consulta amb
URL i URLSearchParams
URL i URLSearchParamsConstruir 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 trencadaLes 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%A1nAquell 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.
- 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:status0,okfalse 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.
- Autenticació:
Authorization i credentials
Authorization i credentialsHi 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 |
Quina triar? Reprenent l'advertiment de 07-01:
Token a localStorage |
Cookie HttpOnly |
|
|---|---|---|
| El llegeix un XSS? | Sí, 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.
- Nómada Tasques:
js/dades/api-tasques.js
js/dades/api-tasques.jsJa 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 feiaRepositoriLocal. La resta de l'aplicació no sap d'on venen les dades. Aquest és el sentit de tenir una carpetadades/. comprovarestà escrita una sola vegada. El parany de l'apartat 6 es neutralitza encapsulant-lo, no recordant-lo.esborrarTascano cridajson(). Un204 No Contentno té cos ijson()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.)
- Practicar de debò:
json-server a la teva màquina
json-server a la teva màquinaapi.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 3000Amb 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/6Només has de canviar una línia al teu mòdul:
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'
awaitderesposta.json(). Obtens unaPromisei tot el que hi facis donaundefined. - Passar un objecte com a
body. Es converteix en'[object Object]'.JSON.stringifysempre. - Posar
Content-Typeen enviarFormData. Trenca elboundaryi el servidor no pot llegir el cos. - Cridar
.json()sobre un204. No hi ha cos; llança. Comprovastatus === 204oContent-Length. - Llegir el cos dues vegades.
TypeError: body stream already read. Fes servirresposta.clone()abans de la primera lectura. - Concatenar paràmetres a mà. Un espai, un
&o un accent trenquen la URL. Fes servirURLiURLSearchParams. - 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'
OPTIONSdel preflight. Gairebé tots els problemes de xarxa es diagnostiquen allà en trenta segons. - Consell: fes servir
await resposta.text()quanjson()falli. Veuràs si el servidor ha retornat una pàgina HTML d'error en lloc de JSON. - Consell: encapsula
fetchen 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 body —JSON.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
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
