Durant tot el mòdul, Node ha estat el servidor: algú demanava i nosaltres responíem. Ara girem el paper. Un backend real gairebé mai no viu sol: cobra a través d'una passarel·la de pagament, envia correus amb un proveïdor, consulta tipus de canvi, valida adreces. En tots aquests casos el teu servidor és el client d'un altre servidor.

I ser client té els seus propis perills, diferents dels que ja coneixes. El principal és que depens d'una màquina que no controles: pot trigar trenta segons, pot tornar un 500, pot estar caiguda justament quan la teva pàgina té més visites. Si el teu codi no ho preveu, la lentitud d'un tercer es converteix en la lentitud de la teva plataforma.

Escena Viva vol mostrar els preus també en lliures per al públic britànic. En acabar tindràs src/serveis/canvi-divises.js: una crida a una API pública amb temps límit, reintents, memòria cau en memòria amb caducitat i degradació elegant —si el proveïdor falla, es mostren només euros en comptes de trencar la pàgina.

Contingut

  1. Node com a client: fetch, http.request i els clients de tercers
  2. La petició GET amb fetch i l'error clàssic
  3. Llegir la resposta: json(), text() i les capçaleres
  4. POST amb capçaleres i cos JSON
  5. Temps límit i cancel·lació amb AbortController
  6. Reintents: què és segur repetir i què no
  7. Què hi ha a sota: http.request
  8. src/serveis/canvi-divises.js: memòria cau i degradació elegant
  9. Claus d'API i reutilització de connexions

  1. Node com a client: fetch, http.request i els clients de tercers

Node té tres maneres de fer una petició HTTP sortint, i triar bé és senzill si entens d'on ve cadascuna.

fetch (global) http.request Tercers (axios, undici, got)
Instal·lació Cap, és global des de Node 18 Cap, mòdul del nucli npm install (Mòdul 5)
API Promeses, igual que al navegador Callbacks i streams Promeses, amb sucre
Cos JSON await resposta.json() Acumular trossos a mà Automàtic
Errors HTTP No rebutja: cal mirar ok No rebutja axios sí que rebutja en 4xx/5xx
Reintents, agents A mà A mà Inclosos o per opció
Quan fer-lo servir Per defecte Entendre el fons, control fi de streams Projectes amb moltes integracions

La recomanació del curs és directa: fetch per defecte. És estàndard, no afegeix dependències, i el codi que escriguis funciona igual al navegador i a Node. undici mereix una menció especial perquè és la implementació que hi ha sota fetch a Node; fer-lo servir directament dóna control sobre els agents i les connexions, i el veurem per sobre a l'apartat 9.

  1. La petició GET amb fetch i l'error clàssic

Una crida bàsica són dues línies —const resposta = await fetch(url) i const dades = await resposta.json()—, però aquest és l'error que comet tothom el primer cop:

// MALAMENT: sembla correcte i no ho es.
try {
  const dades = await (await fetch('https://api.exemple.test/tipus?base=EUR')).json();
  return dades.tipus.GBP;
} catch (error) {
  console.error('La crida ha fallat:', error);
}

fetch no rebutja la promesa quan el servidor respon amb 404 o 500. Des del seu punt de vista, una resposta 500 és un èxit: la petició ha viatjat, el servidor ha contestat, tens la teva resposta. Que el contingut sigui un error és cosa teva.

Només rebutja quan no hi ha hagut resposta: fallada de xarxa, DNS que no resol, connexió rebutjada, certificat TLS invàlid, o cancel·lació explícita.

Situació fetch rebutja? Què obtens
200 OK No response.ok === true
404 Not Found No response.ok === false, status 404
500 Internal Server Error No response.ok === false, status 500
El servidor no respon / no existeix Sí TypeError: fetch failed, amb cause
Temps límit exhaurit / cancel·lat Sí AbortError o TimeoutError

Al codi dolent de dalt, un 500 que torna HTML fa que resposta.json() llanci un SyntaxError, i acabes depurant un error de JSON quan el problema real era un altre. La comprovació és obligatòria:

const resposta = await fetch(url);

if (!resposta.ok) {
  const error = new Error(`L'API ha respost ${resposta.status} ${resposta.statusText}`);
  error.codi = 'SERVEI_EXTERN_CAIGUT';
  error.estatExtern = resposta.status;   // caldra per decidir si es reintenta
  throw error;
}

Traduir la fallada aliena al nostre vocabulari de domini és el que permet que la resta del sistema la tracti igual que qualsevol altre error: SERVEI_EXTERN_CAIGUT ja és a la taula de la lliçó 04-02 i val 503.

  1. Llegir la resposta: json(), text() i les capçaleres

L'objecte Response té l'estat, les capçaleres i el cos, i el cos es llegeix una sola vegada: és un stream, i consumir-lo l'exhaureix.

resposta.status;                        // 200
resposta.ok;                            // true si status esta entre 200 i 299
resposta.headers.get('content-type');   // 'application/json' (insensible a majuscules)
await resposta.json();                  // analitza el cos com a JSON
await resposta.text();                  // el cos com a text
await resposta.arrayBuffer();           // binari: un cartell, un PDF

Intentar llegir-lo dues vegades llança TypeError: Body is unusable. Si necessites el text i el JSON —molt útil per donar un missatge d'error decent—, llegeix el text una vegada i analitza'l tu:

const text = await resposta.text();

if (!resposta.ok) {
  // El cos d'un error sol portar una pista; es registra retallat.
  console.error(`[divises] ${resposta.status}: ${text.slice(0, 200)}`);
  throw errorDomini('SERVEI_EXTERN_CAIGUT', `L'API ha respost ${resposta.status}`);
}

const dades = JSON.parse(text);

resposta.headers és un objecte Headers, no un diccionari: es consulta amb .get() i és insensible a majúscules. Dues capçaleres que convé mirar a les API de tercers són retry-after (segons que et demana esperar després d'un 429) i les de límit de taxa, que se solen dir x-ratelimit-remaining.

  1. POST amb capçaleres i cos JSON

Enviar dades és el segon argument de fetch:

const resposta = await fetch('https://api.pagaments.test/cobraments', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.CLAU_PAGAMENTS}`,   // mai al codi
    Accept: 'application/json'
  },
  // El cos s'envia com a CADENA: cal serialitzar-lo a ma.
  body: JSON.stringify({ comandaId: 'ped-000042', importCentims: 5000, moneda: 'EUR' })
});

Els dos oblits habituals van junts: fetch no serialitza per tu (passar-li un objecte a body acaba enviant la cadena [object Object]) i no posa el Content-Type de JSON pel seu compte, així que el servidor rep una cosa que no sap interpretar i respon 400. Si el cos és un formulari, body: new URLSearchParams({...}) sí que posa el tipus correcte automàticament.

  1. Temps límit i cancel·lació amb AbortController

Aquest apartat és el més important de la lliçó. fetch no té temps límit per defecte. Si l'API de l'altre costat accepta la connexió i després es queda pensant, el teu await espera. I espera. Amb el temps per defecte del sistema, podrien ser minuts.

Ara suma-hi l'efecte: cada petició d'un usuari al teu servidor dispara una crida externa que triga dos minuts. Les peticions s'acumulen, els sòcols s'exhaureixen i la teva plataforma cau per culpa d'un tercer. Això no és un cas teòric: és la manera més comuna com un backend sa es degrada.

La solució és un AbortSignal. Des de Node 17.3 hi ha una drecera per al cas habitual: fetch(url, { signal: AbortSignal.timeout(3000) }) cancel·la la petició passats aquests mil·lisegons.

Quan salta, fetch rebutja amb un error el name del qual és 'TimeoutError'. Si necessites cancel·lar pel teu compte —per exemple, perquè l'usuari ha tancat la connexió (lliçó 04-05)—, fes servir el controlador complet:

const controlador = new AbortController();

// Si el client que ens ha demanat la pagina se'n va, cancellem la crida externa.
req.on('close', () => controlador.abort());

const resposta = await fetch(url, { signal: controlador.signal });

I si necessites les dues coses, AbortSignal.any([...]) combina senyals: es cancel·la amb la que passi primer.

try {
  const resposta = await fetch(url, { signal: AbortSignal.timeout(3000) });
  // ...
} catch (error) {
  if (error.name === 'AbortError') return;   // ho hem cancellat nosaltres: ningu no escolta
  if (error.name === 'TimeoutError') {
    throw errorDomini('SERVEI_EXTERN_CAIGUT', "L'API de divises no ha respost a temps");
  }
  throw error;
}

La regla, sense excepcions: tota crida sortint porta temps límit. Tres segons és un punt de partida raonable per a una API de dades; si el proveïdor necessita més, és una decisió conscient, no un descuit.

  1. Reintents: què és segur repetir i què no

Una fallada de xarxa pot ser passatgera. Reintentar té sentit... per a algunes coses. Reutilitzem reintentar de la lliçó 02-04, amb la seva espera exponencial, i li donem el criteri adequat.

El primer és el mètode. Els mètodes idempotents (lliçó 04-05) es poden repetir sense conseqüències; POST, no:

Mètode Reintentar? Risc si ho fas
GET, HEAD Sí, sempre Cap: només llegeixes
PUT, DELETE Sí Cap: l'estat final és el mateix
POST Només amb clau d'idempotència Cobrar dues vegades, crear dues comandes

El segon és la causa. No totes les fallades milloren esperant:

Resposta Reintentar? Per què
Fallada de xarxa, DNS, ECONNRESET Sí Sol ser transitòria
408, 429 Sí, respectant Retry-After El servidor demana expressament que esperis
500, 502, 503, 504 Sí Problema de l'altre costat, sovint passatger
400, 401, 403, 404, 422 No La teva petició està malament: repetir-la donarà el mateix
// Reintentar nomes el que pot millorar esperant.
function esReintentable(error) {
  if (error.name === 'TimeoutError') return true;
  if (error.codi !== 'SERVEI_EXTERN_CAIGUT') return true;   // fallada de xarxa
  const estat = error.estatExtern;
  return estat === 408 || estat === 429 || (estat >= 500 && estat <= 599);
}

const dades = await reintentar(() => demanarTipus(), {
  intents: 3,
  esperaInicialMs: 250,   // 250, 500, 1000 ms
  factor: 2,
  esReintentable
});

Reintentar un 401 és llençar temps i triplicar la càrrega d'un servei que ja t'ha dit que la teva clau està malament. I hi ha un perill més gran: si la teva API cau i tots els teus clients reintenten alhora, l'allau impedeix que es recuperi. Per això els reintents van amb espera exponencial i, en sistemes grans, amb una mica d'aleatorietat (jitter) perquè no coincideixin.

  1. Què hi ha a sota: http.request

fetch és còmode, però convé veure el mecanisme almenys una vegada. http.request (i https.request) és l'API original: torna un stream d'escriptura per al cos de la petició i t'entrega un stream de lectura amb la resposta.

const https = require('node:https');

function demanarJson(url) {
  return new Promise((resoldre, rebutjar) => {
    const peticio = https.request(url, { method: 'GET', timeout: 3000 }, (resposta) => {
      // resposta es un IncomingMessage: el MATEIX objecte que rep un servidor.
      const trossos = [];
      resposta.on('data', (tros) => trossos.push(tros));
      resposta.on('end', () => {
        // Acumular Buffer i descodificar al final: llico 04-05.
        const text = Buffer.concat(trossos).toString('utf8');
        try { resoldre({ estat: resposta.statusCode, dades: JSON.parse(text) }); }
        catch (error) { rebutjar(error); }
      });
    });

    peticio.on('timeout', () => peticio.destroy(new Error('Temps limit exhaurit')));
    peticio.on('error', rebutjar);
    peticio.end();   // OBLIGATORI: sense end() la peticio no s'envia
  });
}

El que és revelador és la simetria: l'IncomingMessage que reps com a client és de la mateixa classe que el req que rep el teu servidor, i la petició que envies s'escriu igual que una resposta. Client i servidor són el mateix mecanisme mirat des dels dos costats.

També es veu el que fetch t'estalvia: promeses, acumulació del cos, anàlisi, redireccions, descompressió. Fes servir http.request quan necessitis control de streams de veritat —descarregar un fitxer enorme directament a disc amb pipeline, sense passar per memòria— o quan treballis amb una API que exigeixi alguna cosa molt particular del sòcol.

  1. src/serveis/canvi-divises.js: memòria cau i degradació elegant

Tot junt, en el cas real d'Escena Viva. Dos requisits governen el disseny:

  • No cridar l'API externa a cada petició. Els tipus de canvi varien poc; consultar-los mil vegades per minut és absurd, lent i probablement et guanyi un 429. Es guarden a la memòria cau amb caducitat.
  • No trencar la pàgina si el proveïdor falla. Una cartellera sense preus en lliures continua sent útil; una cartellera amb un 503 no serveix de res. A això se'n diu degradació elegant.
// src/serveis/canvi-divises.js
// Consulta tipus de canvi en una API externa, amb memoria cau,
// temps limit, reintents i degradacio elegant.

const { reintentar } = require('../utils/reintentar.js');

const URL_BASE = process.env.URL_DIVISES ?? 'https://api.exemple.test/tipus';
const TEMPS_LIMIT_MS = 3000;
const VIDA_CACHE_MS = 60 * 60 * 1000;   // 1 hora: els tipus varien poc

// Cache de proces: { GBP: { tipus, caducaEl } }. Es perd en reiniciar,
// cosa acceptable aqui. Amb diversos processos caldria Redis (M10).
const cache = new Map();

async function demanarTipus(divisa) {
  const url = `${URL_BASE}?base=EUR&desti=${encodeURIComponent(divisa)}`;
  const resposta = await fetch(url, {
    signal: AbortSignal.timeout(TEMPS_LIMIT_MS),
    headers: { Accept: 'application/json' }
  });

  if (!resposta.ok) {
    const fallada = errorDomini('SERVEI_EXTERN_CAIGUT', `L'API de divises ha respost ${resposta.status}`);
    fallada.estatExtern = resposta.status;
    throw fallada;
  }

  const dades = await resposta.json();
  const tipus = Number(dades?.tipus?.[divisa]);

  // Mai no confiis en la forma d'una resposta aliena: validala.
  if (!Number.isFinite(tipus) || tipus <= 0) {
    throw errorDomini('SERVEI_EXTERN_CAIGUT', `Tipus de canvi no valid per a ${divisa}`);
  }
  return tipus;
}

// Torna el tipus EUR -> divisa, fent servir la cache si encara es vigent.
async function obtenirTipusCanvi(divisa) {
  const desat = cache.get(divisa);
  if (desat && desat.caducaEl > Date.now()) return desat.tipus;

  const tipus = await reintentar(() => demanarTipus(divisa), {
    intents: 3,
    esperaInicialMs: 250,
    esReintentable: (error) => error.name === 'TimeoutError' ||
      [408, 429].includes(error.estatExtern) || error.estatExtern >= 500
  });

  cache.set(divisa, { tipus, caducaEl: Date.now() + VIDA_CACHE_MS });
  return tipus;
}

// Converteix centims d'euro a centims d'una altra divisa. Tot enter.
function convertirCentims(centimsEuro, tipus) {
  return Math.round(centimsEuro * tipus);
}

// DEGRADACIO ELEGANT: si el servei falla, es tornen nomes euros
// i s'anota el motiu. La pagina continua funcionant.
async function preusDeSessio(sessio, divisa = 'GBP') {
  const preus = { EUR: sessio.preuCentims };
  try {
    preus[divisa] = convertirCentims(sessio.preuCentims, await obtenirTipusCanvi(divisa));
  } catch (error) {
    console.error(`[divises] sense conversio a ${divisa}: ${error.message}`);
    preus.avis = `Preu en ${divisa} no disponible temporalment`;
  }
  return preus;
}

module.exports = { obtenirTipusCanvi, convertirCentims, preusDeSessio, cache };

Quatre decisions que val la pena subratllar:

  • El try/catch és a preusDeSessio, no a obtenirTipusCanvi. La funció de baix nivell llança —és el correcte: no pot decidir pel seu compte què és acceptable—. Qui coneix el context, i per tant sap que es pot viure sense lliures, és qui captura.
  • La resposta aliena es valida. Que l'API torni 200 no garanteix que el cos tingui la forma esperada. Un Number.isFinite de més t'estalvia un NaN propagant-se fins a un preu.
  • Els diners continuen sent enters. Math.round sobre cèntims: 2500 cèntims per un tipus de 0,84 són 2100 cèntims, mai 21.0000000003.
  • La memòria cau és de procés. Amb diversos processos (cluster, Mòdul 10) cadascun tindria la seva, cosa acceptable per a tipus de canvi i que no ho seria per a dades amb estat. Aquí és on entra Redis a la lliçó 10-03.

Comprovació amb un proveïdor inventat que no existeix:

URL_DIVISES=https://no-existeix.test/tipus node -e "
  const { preusDeSessio } = require('./src/serveis/canvi-divises.js');
  preusDeSessio({ preuCentims: 2500 }).then((p) => console.log(JSON.stringify(p, null, 2)));
"
# {"EUR": 2500, "avis": "Preu en GBP no disponible temporalment"}

El servei extern és mort i la resposta continua sent útil. Això és degradar bé.

  1. Claus d'API i reutilització de connexions

Una clau d'API mai no s'escriu al codi. Ni "temporalment", ni "només per provar". El codi acaba en un repositori, el repositori acaba compartit, i hi ha robots recorrent GitHub buscant exactament això. Les claus es llegeixen de l'entorn:

const CLAU = process.env.CLAU_DIVISES;

// Fallar a l'ARRENCADA, no a la primera peticio d'un usuari.
if (!CLAU) throw new Error("Falta la variable d'entorn CLAU_DIVISES");

Comprovar la configuració en arrencar i no a la primera crida és una diferència important: prefereixes que el desplegament falli immediatament abans que falli silenciosament el dimarts a la tarda. La gestió completa de la configuració —.env, secrets, entorns— és la lliçó 11-01.

Sobre la reutilització de connexions: obrir una connexió HTTPS és car. Cal fer la salutació TCP (un viatge d'anada i tornada) i la negociació TLS (dos més). En una API a 80 ms de distància, això són uns 240 ms abans d'enviar un sol byte útil. Si fas mil crides i obres mil connexions, llences quatre minuts en salutacions.

La solució és keep-alive: mantenir la connexió oberta i reutilitzar-la. Node ho fa per tu en dos llocs:

  • fetch fa servir undici per sota, que manté un pool de connexions amb keep-alive activat per defecte. No has de fer res.
  • http.request fa servir http.globalAgent, que des de Node 19 també porta keepAlive: true. En versions anteriors calia crear l'agent a mà:
// Un agent amb keep-alive, compartit per totes les crides al mateix servei.
const agent = new https.Agent({ keepAlive: true, maxSockets: 50 });
https.request(url, { agent }, gestionarResposta);

El que sí que has d'evitar és el contrari: crear un agent nou per petició, cosa que anul·la tot el benefici. I si necessites control fi sobre el pool —límit de connexions per destinació, temps de vida—, undici exposa el seu Agent directament. Hi tornarem en mesurar el rendiment a la lliçó 10-04.

Errors Comuns i Consells

  • Suposar que fetch rebutja davant d'un 404 o un 500. No ho fa. Comprova response.ok sempre, i tradueix la fallada al teu vocabulari de domini.
  • Cridar sense temps límit. És la via més ràpida perquè un tercer lent et tombi el servidor. AbortSignal.timeout(3000) a tota crida sortint.
  • Passar un objecte a body sense JSON.stringify. Envies [object Object] i reps un 400 desconcertant. I recorda't del Content-Type.
  • Llegir el cos dues vegades. TypeError: Body is unusable. Llegeix text() una vegada i analitza'l tu si necessites les dues coses.
  • Reintentar un POST sense clau d'idempotència, o reintentar un 400/401. El primer duplica cobraments; el segon és càrrega inútil.
  • Guardar a la memòria cau sense caducitat, o guardar-hi també les respostes d'error. La primera cosa et deixa amb dades velles per sempre; la segona converteix una fallada puntual en una fallada d'una hora.
  • Posar claus d'API al codi. A l'entorn, i comprovades a l'arrencada.
  • Consell: valida la forma del que torna una API aliena abans de fer-la servir —un 200 no és garantia de res— i decideix sempre, explícitament, què passa si el servei extern falla. Degradar sol ser millor que propagar l'error, però és una decisió de negoci, no tècnica.

Exercicis

Exercici 1: comprovar que fetch no rebutja

Amb el servidor d'Escena Viva arrencat, escriu src/laboratori/provar-fetch.js que demani /esdeveniments (200), /esdeveniments/evt-999 (404), /no-existeix (404) i http://localhost:9999/ (sense servidor), tot dins d'un mateix try/catch. Imprimeix per a cadascuna si la promesa s'ha resolt o s'ha rebutjat, l'ok, l'status i l'error.name. Explica quina és l'única que entra al catch i per què.

Exercici 2: mesurar l'efecte de la memòria cau

Afegeix a canvi-divises.js un comptador de crides reals a l'API i exposa GET /divises/estadistiques amb { crides, encertsCache, midaCache }. Llança 50 peticions seguides a una ruta que faci servir preusDeSessio i comprova quantes arriben al proveïdor. Després baixa VIDA_CACHE_MS a 100 ms, repeteix-ho i explica la diferència.

Exercici 3: temps límit i degradació

Munta un servidor de proves al port 4000 l'única ruta del qual esperi 5 segons abans de respondre (fent servir dormir). Apunta-hi URL_DIVISES i comprova que preusDeSessio torna el preu en euros amb el seu avís al cap d'uns 3 segons, no de 5. Mesura el temps real amb console.time i explica per què no són exactament 3000 ms si hi ha reintents activats.

Solucions

Solució 1. Només l'última entra al catch: és l'única en què no hi ha hagut resposta.

const urls = ['http://localhost:3000/esdeveniments', 'http://localhost:3000/esdeveniments/evt-999',
  'http://localhost:3000/no-existeix', 'http://localhost:9999/'];

for (const url of urls) {
  try {
    const resposta = await fetch(url);
    console.log(`${url.padEnd(46)} | resolta | ok=${resposta.ok} | status=${resposta.status}`);
  } catch (error) {
    console.log(`${url.padEnd(46)} | REBUTJADA | ${error.name}: ${error.cause?.code ?? error.message}`);
  }
}

Els dos 404 es resolen amb ok=false: per a fetch, la petició ha estat un èxit. localhost:9999 rebutja amb TypeError: fetch failed i un cause.code d'ECONNREFUSED, perquè no hi ha ningú escoltant. Aquest és exactament el motiu que if (!resposta.ok) no sigui opcional.

Solució 2. Amb la memòria cau d'una hora, les 50 peticions produeixen una sola crida real: la primera la porta i les 49 restants la troben vigent.

const estadistiques = { crides: 0, encertsCache: 0 };

const desat = cache.get(divisa);
if (desat && desat.caducaEl > Date.now()) {
  estadistiques.encertsCache++;
  return desat.tipus;
}
estadistiques.crides++;   // ...i la resta d'obtenirTipusCanvi, igual

Amb VIDA_CACHE_MS = 100, les crides es disparen perquè gairebé cada petició troba l'entrada caducada. Aquí es veu que el valor de caducitat és un equilibri explícit entre frescor i cost: per a un tipus de canvi, una hora sobra; per a l'aforament d'una sessió, guardar-lo a la memòria cau ja seria un error.

Solució 3. El temps total no són 3000 ms sinó uns 3000 + 250 + 3000 + 500 + 3000 ≈ 9,75 s si hi ha tres intents, perquè cada intent té el seu propi temps límit i entremig s'espera. És l'efecte que cal tenir present en triar els números: el pitjor cas d'una crida amb reintents és la suma de tots ells.

// El servidor lent de proves
require('node:http').createServer(async (req, res) => {
  await dormir(5000);
  res.end(JSON.stringify({ tipus: { GBP: 0.84 } }));
}).listen(4000);

El preu en euros arriba igualment. Si el requisit és "mai més de 3 segons en total", el temps límit no pot viure només a cada intent: cal embolcallar el conjunt amb un AbortSignal.timeout global, o reduir els intents. Que un reintent no sigui gratis és justament la raó que esReintentable hagi de ser restrictiu.

Conclusió

Escena Viva ja parla amb el món en les dues direccions. Com a client, la teva eina per defecte és fetch, global i estàndard des de Node 18, amb http.request per sota per quan necessitis control de streams i clients de tercers quan el projecte acumuli integracions. I el primer que cal gravar-se és que fetch no rebutja davant d'un 404 ni d'un 500: només rebutja quan no hi ha hagut resposta. Comprovar response.ok i traduir la fallada a SERVEI_EXTERN_CAIGUT —que la taula de 04-02 converteix en 503— és obligatori, igual que llegir el cos una sola vegada i validar la forma del que arriba.

Saps enviar un POST amb el seu JSON.stringify explícit i el seu Content-Type, i sobretot saps que tota crida sortint porta temps límit: AbortSignal.timeout(3000), o un AbortController complet quan qui se'n va és el teu propi client. Sense això, la lentitud d'un tercer es converteix en la caiguda de la teva plataforma, que és la forma més comuna i més evitable de degradació. Els reintents amb espera exponencial reutilitzen reintentar del Mòdul 2 amb un criteri doble: només mètodes idempotents —POST únicament amb clau d'idempotència— i només causes que puguin millorar esperant: xarxa, 408, 429 i 5xx, mai un 400 ni un 401.

I tens el servei complet: src/serveis/canvi-divises.js, amb memòria cau en memòria amb caducitat per no cridar a cada petició —una decisió d'equilibri entre frescor i cost, que amb diversos processos demanarà Redis (10-03)—, conversió de diners en cèntims enters amb Math.round, i degradació elegant: si el proveïdor cau, la cartellera mostra euros i un avís en comptes d'un error. Les claus d'API viuen a process.env i es comproven a l'arrencada, i les connexions es reutilitzen amb keep-alive, que fetch et dóna fet gràcies a undici.

Amb això tanques el Mòdul 4. Escena Viva ha passat de ser un programa de terminal a ser una plataforma web: un servidor node:http que és un EventEmitter amb arrencada robusta i aturada ordenada, respostes coherents amb la seva taula d'estats i la seva traducció d'error.codi a HTTP, un enrutador propi amb patrons compilats i gestió d'errors centralitzada, un front-end servit amb streams, memòria cau condicional i la ruta blindada contra el recorregut de directoris, un POST /comandes que valida i ven de veritat, i un client HTTP resistent a les fallades alienes. Tot això sense una sola dependència externa: només el nucli de Node.

Aquest "sense dependències" ha estat deliberat, i també ha tingut un preu que has pagat a mà: enrutament, anàlisi de cossos, tipus MIME, memòria cau. Al Mòdul 5: NPM i Gestió de Paquets fem el pas següent i aprenem a recolzar-nos en la feina d'altres amb criteri: què és package.json, com s'instal·len i es versionen les dependències, què significa realment ^1.2.3, per a què serveix package-lock.json, com automatitzar el projecte amb scripts i com avaluar la seguretat del que et portes a casa. És el pas previ imprescindible perquè, al Mòdul 6, Express substitueixi el teu enrutador i reconeguis en cadascuna de les seves peces alguna cosa que ja has escrit tu.

Curs de Node.js: De Principiant a Avançat

Mòdul 1: Introducció a Node.js

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats