L'app-minim.js de la lliçó anterior cabia en deu línies i arrencava amb aplicacio.listen(3000). Funciona, però no es pot provar, no es pot aturar de forma ordenada, no es pot configurar per entorn i no té lloc on créixer. En aquesta lliçó el convertim en l'esquelet real d'Escena Viva: una aplicació que es crea en un lloc i s'arrenca en un altre, amb la configuració centralitzada i validada, els ajustos del framework triats a consciència i una estructura de carpetes que aguanti els sis mòduls que queden de curs.

Contingut

  1. La separació clau: crear l'aplicació i arrencar-la
  2. src/app.js: la factoria crearAplicacio()
  3. src/servidor.js: l'arrencada
  4. Estructura de carpetes d'Escena Viva
  5. Ajustos de l'aplicació amb app.set i app.get
  6. Configuració per entorn: dotenv i src/config/index.js
  7. Muntar l'aplicació en un servidor http propi
  8. Aturada ordenada amb SIGTERM
  9. app.locals enfront de res.locals
  10. Una nota sobre motors de vistes

  1. La separació clau: crear l'aplicació i arrencar-la

Regla que no trencarem a la resta del curs:

Un mòdul crea l'aplicació i la retorna. Un altre mòdul diferent la posa a escoltar.

És a dir: src/app.js exporta crearAplicacio(), que construeix l'objecte d'Express amb totes les seves rutes i middleware i el retorna sense cridar listen. I src/servidor.js importa aquesta funció, crea el servidor HTTP i el posa a escoltar.

Sembla un detall d'estil. No ho és:

Raó Què passaria si barregessis totes dues coses
Proves (M9) supertest necessita l'aplicació sense arrencar: li passes l'app i ell aixeca un servidor efímer en un port lliure. Si app.js crida listen, cada fitxer de prova intentaria ocupar el port 3000 i les proves fallarien en paral·lel.
Aturada ordenada (M11) Per tancar bé necessites la referència a l'objecte Server, no a l'app. Només la tens si l'arrencada és explícita.
Reutilització Pots muntar diverses instàncies (API i mètriques en ports diferents) o imbricar-la amb app.use('/api/v1', crearAplicacio()).

A més, crearAplicacio() és una factoria, no un objecte exportat directament: permet passar-li opcions i garanteix que cada prova obtingui una aplicació neta, sense estat compartit de l'anterior.

  1. src/app.js: la factoria crearAplicacio()

Aquesta és la primera versió. Anirà creixent a 06-03 (routers), 06-04 (middleware propi), 06-05 (middleware de tercers) i 06-07 (errors).

// src/app.js
const express = require('express');
const { configuracio } = require('./config/index.js');

/**
 * Construeix l'aplicacio Express d'Escena Viva.
 * No arrenca cap servidor: nomes retorna l'aplicacio configurada.
 */
function crearAplicacio(opcions = {}) {
  const { servirEstatics = true } = opcions;
  const aplicacio = express();

  // --- Ajustos del framework (apartat 5) ---
  aplicacio.disable('x-powered-by');
  aplicacio.set('trust proxy', configuracio.confiarEnProxy);
  aplicacio.set('case sensitive routing', true);
  aplicacio.set('json spaces', configuracio.esProduccio ? 0 : 2);

  // --- Valors compartits per tota l'aplicacio (apartat 9) ---
  aplicacio.locals.nomPlataforma = 'Escena Viva';
  aplicacio.locals.versio = require('../package.json').version;

  // --- Middleware integrat: lectura del cos JSON i estatics ---
  aplicacio.use(express.json({ limit: configuracio.limitCos }));
  if (servirEstatics) {
    aplicacio.use(express.static(configuracio.directoriPublic, { index: 'index.html' }));
  }

  // --- Rutes (s'ompliran a 06-03) ---
  aplicacio.get('/salut', (peticio, resposta) => {
    const { nomPlataforma, versio } = aplicacio.locals;
    resposta.json({ estat: 'ok', plataforma: nomPlataforma, versio });
  });

  return aplicacio;
}

module.exports = { crearAplicacio };

Punts que val la pena assenyalar: no hi ha cap listen enlloc (aquest és el contracte); la funció rep opcions amb valors per defecte, de manera que crearAplicacio() sense arguments continua funcionant; la configuració arriba d'un mòdul, no de process.env escampat pel fitxer (apartat 6); i els diagnòstics no s'imprimeixen aquí, perquè app.js no parla per consola, només construeix.

  1. src/servidor.js: l'arrencada

// src/servidor.js
const http = require('node:http');
const { crearAplicacio } = require('./app.js');
const { configuracio } = require('./config/index.js');

/** Crea el servidor HTTP i el posa a escoltar. */
function arrencarServidor() {
  // Creem el servidor a ma en comptes de fer servir aplicacio.listen(...):
  // aixi conservem la referencia al Server per tancar-lo (apartats 7 i 8).
  const servidor = http.createServer(crearAplicacio());
  const { port, host, entorn } = configuracio;

  servidor.listen(port, host, () => {
    console.error(`[escena-viva] entorn=${entorn} escoltant a http://${host}:${port}`);
  });

  registrarAturadaOrdenada(servidor);
  return servidor;
}

module.exports = { arrencarServidor };

// El fitxer es modul i script alhora.
if (require.main === module) arrencarServidor();

La funció registrarAturadaOrdenada l'escriurem a l'apartat 8. Actualitza també els scripts del package.json, perquè el punt d'entrada ha canviat respecte del mòdul 4: "start": "node src/servidor.js" i "dev": "node --watch src/servidor.js".

  1. Estructura de carpetes d'Escena Viva

Express no imposa estructura. Aquesta és la nostra, pensada perquè cada fitxer tingui una sola raó per canviar.

escena-viva/
├── dades/                    # esdeveniments.json, vendes.csv (ja existia)
├── public/                   # index.html, estils.css, app.js (ja existia)
├── src/
│   ├── app.js                # crearAplicacio()
│   ├── servidor.js           # arrencada i aturada
│   ├── config/               # index.js (NOU) + rutes.js (ja existia)
│   ├── rutes/                # routers d'Express        (06-03)
│   ├── controladors/         # gestors (req, res)       (06-03)
│   ├── middleware/           # middleware propi         (06-04)
│   ├── esquemes/             # validacio amb zod        (06-06)
│   ├── errors.js             # jerarquia d'errors       (06-07)
│   ├── domini/               # Esdeveniment, Sessio, GestorDeVendes (ja existia)
│   ├── serveis/              # canvi-divises.js i logica de negoci
│   ├── cataleg-dades.js      # acces al JSON (ja existia)
│   ├── informes/ streams/ utils/ i servidor/ (l'artesanal del M4, referencia)
└── package.json

Responsabilitats, perquè ningú no tingui dubtes d'on va cada cosa:

Carpeta Responsabilitat Què no ha de contenir
rutes/ Declarar quina URL crida quin controlador i quin middleware la protegeix Lògica de negoci
controladors/ Traduir HTTP a domini: llegir req, cridar un servei, respondre Regles d'aforament, accés a fitxers
serveis/ Casos d'ús: orquestrar domini i dades Objectes req/res
domini/ Regles d'Escena Viva: aforament, estats, preus Res d'HTTP ni d'Express
middleware/ Preocupacions transversals: registre, identificador, memòria cau Regles específiques d'un endpoint
esquemes/ i config/ Forma esperada de l'entrada; lectura i validació de l'entorn Consultes, respostes ni lògica

La regla que ho resumeix: el domini no importa Express, i Express no importa fs. Si un fitxer de domini/ necessita require('express'), hi ha alguna cosa mal col·locada.

Advertiment contra la sobreenginyeria

Aquesta estructura té sentit per a una API que creixerà durant sis mòduls més. Per a un projecte de tres endpoints és exagerada. Senyals que t'estàs passant: controladors que només criden un servei que només crida un repositori que només fa una línia (tres fitxers per a una operació trivial), carpetes buides «per si de cas», i interfícies i abstraccions per a una única implementació que mai no en tindrà cap altra.

Comença per rutes/ + controladors/ i treu-ne serveis/ quan la lògica es repeteixi en dos controladors, no abans. A Escena Viva sí que arribarem a aquest punt, perquè la venda d'entrades es fa servir des de l'API i des dels informes.

  1. Ajustos de l'aplicació amb app.set i app.get

Express desa els ajustos en un diccionari intern. app.set(nom, valor) escriu, app.get(nom) llegeix (compte: app.get amb dos arguments registra una ruta GET; amb un, llegeix un ajust), i app.enable/app.disable són dreceres per a valors booleans.

Ajust Valor recomanat Què fa i per què
env process.env.NODE_ENV o development En producció s'activa la memòria cau de vistes i el gestor d'errors deixa d'enviar la traça (06-07)
x-powered-by desactivat Express afegeix X-Powered-By: Express a cada resposta: informació gratuïta per a qui busca objectius amb una versió coneguda. No és cap gran defensa, però no costa res
trust proxy 1 o la llista de proxies de confiança Darrere d'Nginx o d'un balancejador, req.ip és la IP del proxy i req.protocol és http encara que el client vingui per HTTPS. Amb aquest ajust, Express llegeix X-Forwarded-For i X-Forwarded-Proto. Imprescindible per al limitador de peticions de 06-05 i per al registre del mòdul 11
json spaces 2 en desenvolupament, 0 en producció Indenta el JSON de res.json(). Còmode en depurar amb curl, malbaratament d'amplada de banda en producció
case sensitive routing true Sense això, /Esdeveniments i /esdeveniments són la mateixa ruta. Que dues URL diferents retornin el mateix perjudica la memòria cau i el SEO
strict routing false Amb false, /esdeveniments i /esdeveniments/ són la mateixa ruta (comportament amable). Amb true, són diferents. El nostre normalitzarRuta del mòdul 4 feia justament això a mà
query parser 'simple' o 'extended' Com s'interpreten les query strings imbricades (?filtre[sala]=Bóveda). 'simple' fa servir querystring del nucli; 'extended' fa servir qs i admet imbricació

Des d'un gestor els pots llegir amb aplicacio.get('env') o aplicacio.get('trust proxy'); d'aquest últim depenen peticio.ip i peticio.protocol.

Compte amb trust proxy. Activa'l només si realment hi ha un proxy al davant. Exposat directament a internet, qualsevol pot falsificar la seva IP enviant una capçalera X-Forwarded-For, i el teu limitador de peticions deixa de servir de res. Per això a la nostra configuració és un valor d'entorn, no una constant.

  1. Configuració per entorn: dotenv i src/config/index.js

dotenv ja és dependència des del mòdul 5. La seva feina és carregar un fitxer .env dins de process.env. Res més: no valida ni converteix tipus. Això ho fem nosaltres.

# .env  (NO es puja al repositori; al repositori hi va .env.exemple)
NODE_ENV=desenvolupament
PORT=3000
LIMIT_COS=100kb
CONFIAR_EN_PROXY=0
ORIGENS_PERMESOS=http://localhost:5173,http://127.0.0.1:5173
// src/config/index.js
const path = require('node:path');
require('dotenv').config();

const { ARREL } = require('./rutes.js');

// Lectors tipats: cadascun llanca si el valor no compleix l'esperat.
const opcional = (nom, perDefecte) => (process.env[nom] ?? '').trim() || perDefecte;

function obligatoria(nom) {
  const valor = opcional(nom, '');
  if (valor === '') throw new Error(`Falta la variable obligatoria: ${nom}`);
  return valor;
}

function enter(nom, perDefecte, { minim, maxim }) {
  const numero = Number.parseInt(opcional(nom, String(perDefecte)), 10);
  if (!Number.isInteger(numero) || numero < minim || numero > maxim) {
    throw new Error(`La variable ${nom} ha de ser un enter entre ${minim} i ${maxim}`);
  }
  return numero;
}

const llista = (n) => opcional(n, '').split(',').map((e) => e.trim()).filter(Boolean);

const ENTORNS_VALIDS = ['desenvolupament', 'proves', 'produccio'];

function construirConfiguracio() {
  const entorn = opcional('NODE_ENV', 'desenvolupament');
  if (!ENTORNS_VALIDS.includes(entorn)) {
    throw new Error(`NODE_ENV ha de ser un de: ${ENTORNS_VALIDS.join(', ')}`);
  }

  return Object.freeze({
    entorn,
    esProduccio: entorn === 'produccio',
    port: enter('PORT', 3000, { minim: 1, maxim: 65535 }),
    host: opcional('HOST', '127.0.0.1'),
    limitCos: opcional('LIMIT_COS', '100kb'),
    confiarEnProxy: enter('CONFIAR_EN_PROXY', 0, { minim: 0, maxim: 10 }),
    origensPermesos: llista('ORIGENS_PERMESOS'),
    directoriPublic: path.join(ARREL, 'public'),
    // En produccio exigim clau de signatura; en desenvolupament hi ha valor de farciment.
    clauPassarella: entorn === 'produccio'
      ? obligatoria('CLAU_PASSARELLA')
      : opcional('CLAU_PASSARELLA', 'clau-de-desenvolupament'),
  });
}

let configuracio;
try {
  configuracio = construirConfiguracio();
} catch (error) {
  console.error(`[configuracio] ${error.message}`); // fallar rapid
  process.exit(1);
}

module.exports = { configuracio, ENTORNS_VALIDS };

Les idees que hi ha darrere d'aquest fitxer:

  1. Un únic punt de lectura de process.env. Buscar-lo al projecte ha de donar exactament un resultat: aquest fitxer. Així saps d'un cop d'ull què necessita l'aplicació per arrencar.
  2. Fallar ràpid. Si falta la clau de la passarel·la en producció, el procés mor a l'arrencada amb un missatge clar. L'alternativa —arrencar i fallar tres hores després, a la primera venda— és molt pitjor.
  3. Tipus correctes. process.env.PORT és la cadena '3000'; la configuració exposa el número 3000 i evita comparacions sorpresa.
  4. Objecte congelat amb Object.freeze: ningú no canvia la configuració en temps d'execució.
  5. El .env no va mai al repositori. Es versiona .env.exemple; afegeix .env al .gitignore.

Al mòdul 11 ampliarem això amb secrets gestionats; de moment, amb això n'hi ha prou.

  1. Muntar l'aplicació en un servidor http propi

A 06-01 vam veure que app és una funció gestora. Aquí ho aprofitem: escrivim http.createServer(aplicacio) en comptes d'aplicacio.listen(...). Què hi guanyes exactament?

Avantatge Per a què el necessitaràs
Tens la referència al Server Tancar-lo amb servidor.close() a l'aturada ordenada (apartat 8)
Pots acoblar altres protocols al mateix port Socket.IO al mòdul 12 fa new Server(servidor)
Pots ajustar temps d'espera servidor.keepAliveTimeout, servidor.headersTimeout al mòdul 11
Pots escoltar esdeveniments del servidor servidor.on('error', ...) per capturar EADDRINUSE amb un missatge decent
Pots tenir dos servidors amb la mateixa app HTTP i HTTPS, o un port de mètriques a part
// L'error classic: el port ja esta ocupat.
servidor.on('error', (error) => {
  if (error.code !== 'EADDRINUSE') throw error;
  console.error(`[escena-viva] el port ${configuracio.port} ja esta en us`);
  process.exit(1);
});

  1. Aturada ordenada amb SIGTERM

Quan un orquestrador (PM2, Docker, Kubernetes) vol aturar el teu procés, li envia el senyal SIGTERM. Si no fas res, Node mor immediatament i les peticions en curs es tallen a mitges: un comprador podria veure el navegador penjat just després d'enviar una comanda. L'aturada ordenada consisteix a deixar d'acceptar connexions noves, acabar les que estan en curs, tancar recursos i sortir.

// src/servidor.js (continuacio)
const MILLISEGONS_DE_GRACIA = 10_000;

function registrarAturadaOrdenada(servidor) {
  let aturant = false;

  function aturar(senyal) {
    if (aturant) return; // un segon senyal no duplica l'aturada
    aturant = true;
    console.error(`[escena-viva] rebut ${senyal}, tancant ordenadament...`);

    // 1. Deixem d'acceptar connexions noves; les que hi ha en curs acaben.
    servidor.close((error) => {
      if (error) console.error('[escena-viva] error en tancar:', error.message);
      process.exit(error ? 1 : 0);
    });

    // 2. Tanquem les connexions keep-alive ocioses per no esperar que expirin.
    servidor.closeIdleConnections();

    // 3. Xarxa de seguretat; unref evita que el temporitzador retingui el proces.
    setTimeout(() => {
      console.error('[escena-viva] tancament forcat despres del temps de gracia');
      process.exit(1);
    }, MILLISEGONS_DE_GRACIA).unref();
  }

  process.on('SIGTERM', () => aturar('SIGTERM'));
  process.on('SIGINT', () => aturar('SIGINT')); // Ctrl+C en desenvolupament
}

Detalls que la gent se sol saltar: l'indicador aturant evita que prémer Ctrl+C dues vegades llanci dues aturades simultànies; sense closeIdleConnections() una connexió keep-alive ociosa manté el servidor viu fins que expiri i el tancament s'eternitza; i el temporitzador amb unref() garanteix que el procés acabi encara que alguna cosa s'encalli, sense impedir que acabi abans si tot va bé. Aquí és on al mòdul 11 hi afegirem el tancament de la base de dades i el bolcat dels registres pendents.

  1. app.locals enfront de res.locals

Dos magatzems amb noms semblants i vides molt diferents.

app.locals res.locals
Àmbit Tota l'aplicació Una única petició/resposta
Vida Des de l'arrencada fins a l'aturada Des que arriba la petició fins que es respon
Compartit entre usuaris Sí No
Ús típic Nom de la plataforma, versió, constants Identificador de petició, usuari autenticat, instant d'inici
// Globals: es fixen una vegada, a crearAplicacio().
aplicacio.locals.sales = ['Teatro Almendra', 'Sala Boveda', 'Auditorio Ribera'];

// Per peticio: els posa un middleware (06-04).
aplicacio.use((peticio, resposta, seguent) => {
  resposta.locals.idPeticio = crypto.randomUUID();
  seguent();
});

Error greu i freqüent: desar dades de l'usuari actual a app.locals. Com que és global, el segon usuari veuria les dades del primer. Tot el que depengui de qui fa la petició va a res.locals (o a req, com farem amb req.esdeveniment i req.dadesValidades).

  1. Una nota sobre motors de vistes

Express sap renderitzar plantilles al servidor: app.set('view engine', 'ejs'), app.set('views', ...) i res.render('esdeveniment', { esdeveniment }). És la forma clàssica de generar HTML des de Node i continua sent vàlida per a llocs amb contingut servit.

Escena Viva no la fa servir. El nostre front-end és la carpeta public/ (HTML, CSS i un app.js que crida l'API amb fetch), i el servidor és una API JSON: la mateixa API serveix un front-end web, una app mòbil o un panell intern, i les seves respostes es proven amb supertest sense analitzar HTML. El que sí que conservem és express.static, que serveix aquests fitxers i substitueix el teu estatics.js del mòdul 4; el desmuntarem a 06-04.

Errors Comuns i Consells

  • Cridar listen() dins d'app.js. Trenca les proves del mòdul 9 i l'aturada del mòdul 11. Si veus un listen fora de servidor.js, és un error.
  • Exportar l'app ja construïda (module.exports = app) en comptes d'una factoria. Funciona fins que una prova necessita una altra configuració; llavors cal refactoritzar-ho tot.
  • process.env escampat per vint fitxers. Impossible saber què necessita l'aplicació. Centralitza-ho a config/index.js.
  • Confondre app.get('nom') amb app.get('/ruta', gestor). Un argument llegeix un ajust; dos registren una ruta.
  • Activar trust proxy sense proxy. Regales la capacitat de falsificar la IP a qualsevol.
  • Pujar el .env al repositori. Al .gitignore des del primer dia; versiona .env.exemple.
  • Consell: executa node -e "require('./src/config/index.js')" a l'script comprovar. Si la configuració és invàlida, ho sabràs a la integració contínua i no en producció.

Exercicis

Exercici 1: la separació a la pràctica

Crea src/app.js i src/servidor.js amb l'estructura d'aquesta lliçó i demostra que la separació funciona: escriu un tercer fitxer provar-app.js que importi crearAplicacio(), la munti en un http.createServer sobre el port 0 (Node n'assigna un de lliure), faci una petició a /salut amb fetch i tanqui el servidor. No ha de tocar src/servidor.js en cap moment.

Exercici 2: configuració que falla ràpid

Amplia src/config/index.js amb una variable MONEDA_PER_DEFECTE que només admeti EUR, USD o GBP, amb EUR per defecte. Comprova que un valor invàlid impedeix l'arrencada amb un missatge clar i codi de sortida 1.

Exercici 3: aturada observable

Afegeix a l'aturada ordenada un comptador de peticions en curs: un middleware l'incrementa en entrar i el decrementa a res.on('finish'). En rebre SIGTERM, el servidor ha d'imprimir per stderr quantes peticions quedaven vives. Prova-ho amb un endpoint lent (/lent, que triga 3 segons) i enviant el senyal mentre està en curs.

Solucions

Solució 1

// provar-app.js
const http = require('node:http');
const { crearAplicacio } = require('./src/app.js');

async function principal() {
  const servidor = http.createServer(crearAplicacio({ servirEstatics: false }));
  // Port 0: el sistema operatiu n'assigna un de lliure. Es el que fa supertest.
  await new Promise((resoldre) => servidor.listen(0, '127.0.0.1', resoldre));

  const resposta = await fetch(`http://127.0.0.1:${servidor.address().port}/salut`);
  console.log(JSON.stringify({ estat: resposta.status, cos: await resposta.json() }));

  await new Promise((resoldre) => servidor.close(resoldre));
}

principal().catch((error) => {
  console.error('[provar-app]', error.message);
  process.exitCode = 1;
});

Que això sigui possible sense arrencar src/servidor.js és exactament la prova que la separació està ben feta.

Solució 2

// Dins de src/config/index.js
const MONEDES_VALIDES = ['EUR', 'USD', 'GBP'];

function moneda(nom, perDefecte) {
  const valor = opcional(nom, perDefecte).toUpperCase();
  if (!MONEDES_VALIDES.includes(valor)) {
    throw new Error(`${nom} ha de ser una de: ${MONEDES_VALIDES.join(', ')}`);
  }
  return valor;
}
// ...dins de construirConfiguracio(): monedaPerDefecte: moneda('MONEDA_PER_DEFECTE', 'EUR'),
MONEDA_PER_DEFECTE=IEN node src/servidor.js
# [configuracio] MONEDA_PER_DEFECTE ha de ser una de: EUR, USD, GBP   (sortida: 1)

Solució 3

// src/servidor.js — fragments amb el comptador
let enCurs = 0;

// Registrat el PRIMER per comptar absolutament tot.
aplicacio.use((peticio, resposta, seguent) => {
  enCurs += 1;
  resposta.on('finish', () => { enCurs -= 1; });
  seguent();
});

aplicacio.get('/lent', (p, r) => setTimeout(() => r.json({ estat: 'ok' }), 3000));

process.on('SIGTERM', () => {
  console.error(`[escena-viva] SIGTERM amb ${enCurs} peticions en curs`);
  servidor.close(() => process.exit(0));
  servidor.closeIdleConnections();
});

Detall important: perquè el comptador compti, cal registrar-lo abans que les rutes. Aquest és el tema central de 06-04.

Conclusió

Ja no tens un script de joguina, sinó l'esquelet d'una aplicació de debò. La peça central és la separació entre crearAplicacio() —que construeix i retorna l'app sense arrencar-la— i src/servidor.js —que crea el servidor http, el posa a escoltar i sap aturar-lo ordenadament quan arriba SIGTERM—. Aquesta separació no és estètica: és el que farà possible provar amb supertest al mòdul 9, acoblar Socket.IO al mòdul 12 i desplegar amb PM2 i Docker al mòdul 11. A més has definit l'estructura de carpetes d'Escena Viva amb responsabilitats clares (i saps quan no aplicar-la), has triat a consciència els ajustos del framework (x-powered-by fora, trust proxy segons entorn, case sensitive routing actiu) i has centralitzat la configuració en un mòdul que llegeix process.env una sola vegada, valida tipus i rangs i falla ràpid si alguna cosa no quadra.

L'app, però, té una única ruta: /salut. A la lliçó següent, Enrutament a Express, l'omplim: paràmetres de ruta comparats amb el compilarPatro que vas escriure a mà, express.Router() per muntar /api/esdeveniments, /api/sessions i /api/comandes com a mòduls independents, router.param() per carregar l'esdeveniment una sola vegada, i la regla que evita més maldecaps: l'ordre de declaració de les rutes.

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