Federico Lelli / SchoolNode.js & Express
GUIDA DI STUDIO · OFFLINE

Node.js ed Express,
dal primo script alle API

Un manuale ragionato delle 28 lezioni: definizioni al primo incontro, spiegazioni di chi fa cosa, esempi commentati e un laboratorio API completo.

28 lezioniEsempi guidati in ogni lezione1 progetto finaleNessuna connessione richiesta
Come usare questo manuale. Il percorso segue le 28 lezioni tecniche dei testi forniti. Ogni lezione contiene definizioni, esempi, spiegazioni dei ruoli, precisazioni sulle pratiche attuali e una prova guidata. È richiesta una conoscenza iniziale di JavaScript; i concetti di backend vengono introdotti nel testo. I blocchi completi indicano il file da creare; i frammenti vanno inseriti nell’app già preparata e non incollati tutti insieme. Avvia un solo server di esercizio per volta e fermalo con Ctrl+C. Il manuale si apre direttamente nel browser, anche offline.
ORIENTAMENTO

La mappa: chi fa cosa

Questa tabella anticipa i ruoli del percorso; le definizioni dettagliate e il codice si trovano nelle singole lezioni. Una API è un’interfaccia di programmazione; CRUD indica Create, Read, Update, Delete, cioè creare, leggere, aggiornare ed eliminare dati.

ComponenteCompitoDove lo incontri
JavaScriptLinguaggio con cui scrivi istruzioni e regoleIn tutto il manuale
V8Motore che esegue JavaScriptLezione 1
Node.jsAmbiente che esegue il codice e offre accesso a file, rete e processiLezione 1
npmInstalla pacchetti e avvia script del progettoLezione 9
HTTPRegole per richieste e risposte tra client e serverLezioni 8 e 15
ExpressFramework per organizzare middleware e route sopra NodeLezione 16
Client: browser, Postman, fetchInvia richieste; il browser può anche costruire e mostrare una paginaLezioni 18 e 23
Array e databaseL’array conserva dati nel processo; un database può conservarli e coordinarne l’accesso nel tempoLezione 24 e laboratorio

Come copiare gli esempi

I commenti indicano nomi dei file e comandi. Se un blocco contiene più nomi di file, separane il contenuto nei file corrispondenti. I frammenti con app presuppongono un’app Express già creata: inseriscili prima della 404 e dell’error handler. Una route duplicata può essere intercettata dalla prima registrata: sostituisci l’esempio precedente anziché accumularne copie. Gli esempi CommonJS usano .cjs; quelli ES Modules usano .mjs.

LEZIONE 01 · Fondamenti

Cos’è Node.js

Prima di iniziare: le parole della lezione

Runtime
Ambiente che esegue un programma e gli fornisce servizi: Node.js è il runtime, JavaScript è il linguaggio del codice.
V8
Motore JavaScript usato anche da Chrome: esegue JavaScript e può compilarlo in codice macchina. Il codice non viene prima tradotto in C++.
Frontend e backend
Il frontend è la parte con cui interagisce l’utente, spesso nel browser; il backend gestisce richieste, regole e dati sul server.
API, I/O e thread
API significa interfaccia di programmazione: funzioni e regole con cui usare un componente. I/O significa input/output, come leggere un file o scambiare dati in rete. Un thread è un flusso di esecuzione di istruzioni.
DOM e framework
Il DOM è la rappresentazione della pagina HTML manipolabile dal browser. Un framework è una struttura software che organizza lo sviluppo: Express sarà il framework web di questa guida.

Node.js è un ambiente di esecuzione JavaScript fuori dal browser. Usa il motore V8 e mette a disposizione API lato server: file system, rete, processi e altro. Non è un linguaggio diverso da JavaScript, né un framework web.

  • Browser: dispone di DOM, window, interfaccia grafica e Web API.
  • Node.js: dispone di process, file system, server HTTP e API di sistema. Non esiste un DOM di default.
  • Event loop: permette di coordinare molte operazioni I/O senza bloccare il thread JavaScript mentre si aspetta un risultato. Questo non significa che qualsiasi calcolo sia automaticamente parallelo.
console.log('Ciao da Node.js');
console.log('Versione:', process.version);
console.log('Sistema operativo:', process.platform);

Esempio pratico. Un server può avviare la lettura di un file e nel frattempo continuare a gestire altre richieste. Se invece esegue un ciclo CPU molto pesante sul thread principale, le altre richieste possono rallentare.

Chi fa cosa, e perché

Nella metafora della banca del testo, lo sportellista è il thread JavaScript: riceve le richieste e avvia le operazioni. Il lavoro che richiede attesa può essere affidato al sistema operativo o ad altri componenti di Node; quando è pronto, il codice riprende a elaborarne il risultato. Questo permette di servire più utenti senza tenere fermo lo sportello per ogni attesa.

Il server è il programma che offre un servizio; il computer che lo ospita viene spesso chiamato anch’esso server. Il client è il programma che lo contatta, ad esempio un browser. Node può eseguire anche strumenti da terminale, senza creare alcun server. V8 esegue JavaScript, Node fornisce le API, il sistema operativo gestisce risorse come file e rete.

Un altro esempio guidato

// ambiente.cjs — avvio: node ambiente.cjs
console.log('JavaScript:', [1, 2, 3].map(n => n * 2));
console.log('API Node:', process.version);
console.log('DOM disponibile?', typeof document);

Come leggerlo e risultato atteso. La prima riga stampa un array con 2, 4, 6; map trasforma ogni elemento. La seconda legge la versione del runtime. La terza stampa undefined: in un normale script Node non c’è una pagina da manipolare.

Prova tu. Indica chi esegue JavaScript, chi mostra la pagina e chi legge un file dal disco. Risposta: il motore nel runtime, il browser e le API di Node con il sistema operativo.

Torna all’indice ↑
LEZIONE 02 · Fondamenti

Installazione e ambiente

Prima di iniziare: le parole della lezione

LTS
Long-Term Support: linea di versioni mantenuta nel tempo con correzioni e aggiornamenti di sicurezza.
Terminale, shell e CLI
Il terminale mostra l’interfaccia testuale; la shell, per esempio PowerShell, interpreta i comandi; una CLI è un programma utilizzabile tramite comandi.
PATH e directory di lavoro
PATH è l’elenco di cartelle in cui la shell cerca gli eseguibili. La directory di lavoro è la cartella da cui un comando viene eseguito.
Editor e REPL
Un editor modifica i file di codice. Una REPL legge un’espressione, la esegue, stampa il risultato e attende il comando successivo.

Installa una versione LTS attualmente supportata dal sito ufficiale di Node.js. Il corso originale mostra versioni 14 e 16: sono riferimenti storici, non versioni da installare oggi. Usa un editor come Visual Studio Code; le estensioni sono facoltative.

node --version
npm --version
mkdir corso-node
cd corso-node

Crea index.js e lancialo:

node index.js

Esempio aggiuntivo: per aprire la console interattiva basta eseguire node, scrivere 2 + 2 e uscire con .exit. Controllo: se node non viene riconosciuto, riapri il terminale e verifica l'installazione e il PATH.

Chi fa cosa, e perché

L’installer rende disponibile node; npm è il gestore di pacchetti normalmente distribuito insieme a Node. Visual Studio Code serve a scrivere e salvare i file, mentre il terminale avvia il runtime. Scrivere codice nell’editor non lo esegue: occorre salvarlo e passare il file a node.

Le estensioni mostrate nella trascrizione propongono frammenti di codice, detti snippet. Sono facoltative e non installano né Node né Express. Tieni aperta nel terminale la cartella che contiene il tuo script: un file esistente in un’altra cartella può risultare “non trovato”.

Un altro esempio guidato

# PowerShell: un comando per riga
node --version
npm --version
Get-Location
node -e "console.log('Installazione riuscita')"

Come leggerlo e risultato atteso. --version chiede la versione; Get-Location mostra la cartella attuale; -e esegue il breve programma passato come testo e stampa Installazione riuscita. Per avviare un file con spazi nel nome usa node "primo esempio.cjs".

Prova tu. Apri una nuova shell dopo l’installazione e ripeti i controlli. Se node funziona ma npm è bloccato da PowerShell, prova npm.cmd --version prima di modificare impostazioni del sistema.

Torna all’indice ↑
LEZIONE 03 · Fondamenti

Prime righe di codice

Prima di iniziare: le parole della lezione

Script e processo
Uno script è un file di istruzioni; un processo è l’istanza del programma avviata dal sistema operativo.
Funzione e argomento
Una funzione raggruppa istruzioni richiamabili. Un argomento è un valore passato quando la si invoca; il parametro è il nome che lo riceve.
const, let e confronto stretto
const impedisce di riassegnare una variabile; let permette di riassegnarla. === confronta senza conversione automatica di tipo.

Un programma Node.js è un normale file .js eseguito dal terminale. La sintassi JavaScript resta valida, salvo le API specifiche del browser.

// index.js
const numero = 3;
if (numero < 3) console.log('Minore di 3');
else if (numero === 3) console.log('Uguale a 3');
else console.log('Maggiore di 3');
for (let i = 0; i < 3; i++) console.log('Giro', i);
node index.js

Esempio aggiuntivo: console.log(typeof document) stampa undefined in un normale processo Node.js. Prima di avviare il programma ricordati di salvare il file.

Chi fa cosa, e perché

Il comando node index.js carica il file salvato sul disco e ne esegue il codice dall’alto verso il basso. Il nome index è una convenzione, non una parola speciale. Un programma senza server, timer attivi o altre attività in attesa normalmente termina quando finisce il lavoro.

console.log scrive nel terminale del processo. Non aggiorna una pagina web. Un errore di sintassi impedisce di interpretare il codice; un errore durante l’esecuzione può avvenire dopo che alcune istruzioni sono già state eseguite. Leggi il file e il numero di riga indicati nel messaggio.

Un altro esempio guidato

// argomenti.cjs — avvio: node argomenti.cjs Anna
const nome = process.argv[2] ?? 'visitatore';
function saluta(persona) {
  return `Ciao, ${persona}!`;
}
console.log(saluta(nome));

Come leggerlo e risultato atteso. process.argv è l’array degli argomenti del processo: le prime due posizioni riguardano eseguibile e script. La posizione 2 contiene Anna. L’operatore ?? usa il valore a destra se quello a sinistra è null o undefined. La funzione restituisce il saluto, poi console.log lo stampa.

Prova tu. Avvia lo script senza nome: deve stampare Ciao, visitatore!. Poi modifica il valore predefinito, salva e rilancialo.

Torna all’indice ↑
LEZIONE 04 · Fondamenti

Oggetti globali e process

Prima di iniziare: le parole della lezione

Globale e ambito del modulo
Un globale è disponibile senza importazione. L’ambito del modulo contiene nomi validi solo in quel file: le variabili dichiarate con const non diventano automaticamente proprietà globali.
process e variabile d’ambiente
process descrive il processo Node. Una variabile d’ambiente è un valore di configurazione esterno al codice, letto con process.env.
Callback e timer
Una callback è una funzione passata a un’altra funzione perché la richiami. Un timer pianifica una callback dopo un intervallo minimo, senza garantirne l’istante esatto.

In Node.js trovi globali come console, setTimeout, process e globalThis. Il corso usa anche __dirname, __filename, require e module: sono disponibili nei moduli CommonJS, non allo stesso modo nei moduli ESM.

// index.cjs (CommonJS)
console.log(__dirname);
console.log(__filename);
console.log(process.cwd());
console.log(process.env.NODE_ENV ?? 'non definito');
setTimeout(() => console.log('Dopo 200 ms'), 200);
// index.mjs (ES modules): alternativa moderna
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const filename = fileURLToPath(import.meta.url);
const directory = dirname(filename);
console.log(directory);

NODE_ENV è una convenzione: non garantisce da sola che il codice sia sicuro o configurato correttamente. Evita di stampare in log l'intero process.env, perché può contenere segreti.

Chi fa cosa, e perché

__dirname indica la cartella del modulo CommonJS che lo contiene; __filename ne indica il percorso completo. process.cwd() indica invece la cartella da cui hai avviato il processo. Se importi un file da una sottocartella, il suo __dirname cambia, ma cwd() resta quello del processo, salvo modifiche esplicite.

require carica un modulo e module ne rappresenta il contesto CommonJS: non sono normali globali universali. globalThis è il nome standard dell’oggetto globale, ma condividere dati tramite globali rende più difficile capire chi li modifica.

Un altro esempio guidato

// configurazione.cjs — node configurazione.cjs
const porta = Number(process.env.PORT ?? 3000);
if (!Number.isInteger(porta) || porta < 1 || porta > 65535) {
  console.error('PORT deve essere un intero tra 1 e 65535');
  process.exitCode = 1;
} else {
  console.log('Porta configurata:', porta);
}

Come leggerlo e risultato atteso. Senza configurazione stampa 3000. I valori di process.env sono normalmente stringhe: Number converte il testo, il controllo scarta valori non validi. exitCode = 1 comunica un esito di errore quando il processo termina.

Prova tu. In PowerShell esegui $env:PORT = "4000" e poi lo script: deve leggere 4000. Con Remove-Item Env:PORT rimuovi la variabile dalla shell corrente.

Torna all’indice ↑
LEZIONE 05 · Moduli e I/O

Creare moduli

Prima di iniziare: le parole della lezione

Modulo, importazione ed esportazione
Un modulo è un’unità di codice con un proprio ambito. Esportare significa rendere un valore utilizzabile da altri file; importare significa ottenere quel valore.
CommonJS ed ESM
CommonJS usa require e module.exports. ESM significa ECMAScript Modules: è il sistema standard JavaScript basato su import ed export.
Destructuring e cache
Il destructuring estrae proprietà da un oggetto. Una cache conserva un risultato per riutilizzarlo: i moduli caricati vengono normalmente riutilizzati nelle importazioni successive dello stesso processo.

Dividere un programma in moduli rende il codice più leggibile, testabile e riutilizzabile. Nel corso si usa soprattutto CommonJS (require, module.exports).

Esempio CommonJS:

// saluti.cjs
function saluta(nome) { return `Ciao, ${nome}!`; }
module.exports = { saluta };
// main.cjs
const { saluta } = require('./saluti.cjs');
console.log(saluta('Anna'));

Alternativa ES Modules:

// saluti.mjs
export const saluta = nome => `Ciao, ${nome}!`;
// main.mjs
import { saluta } from './saluti.mjs';
console.log(saluta('Marco'));

Nota: l'importazione può eseguire il codice al livello superiore del modulo; evita effetti collaterali inattesi all'import.

Chi fa cosa, e perché

Nel testo, separare nomi e funzioni dal file principale serve a dare a ogni file una responsabilità. Il modulo dei nomi contiene i dati; il modulo delle utilità contiene funzioni; il file principale li collega. Una variabile rimane privata finché non viene esportata.

In CommonJS module.exports = saluta esporta direttamente la funzione; module.exports = { saluta } esporta un oggetto che la contiene. Nel secondo caso const { saluta } = require(...) estrae quella proprietà. Scrivere solo exports = qualcosa non sostituisce module.exports. Inoltre importare un array non ne crea automaticamente una copia: lo stesso oggetto esportato può essere condiviso fra moduli.

Un altro esempio guidato

// prezzi.mjs
export function conIva(prezzo, aliquota = 0.22) {
  return prezzo * (1 + aliquota);
}

// main.mjs — salva questa parte in un secondo file
import { conIva } from './prezzi.mjs';
console.log(conIva(100).toFixed(2));
// Avvio: node main.mjs

Come leggerlo e risultato atteso. prezzi.mjs definisce il calcolo, main.mjs lo usa e stampa 122.00. ./ significa “rispetto al file che importa”; ../ sale alla cartella superiore. toFixed(2) produce una stringa con due decimali.

Prova tu. Esporta anche senzaIva, importala nel file principale e verifica che da 122 torni circa a 100. I numeri decimali JavaScript possono avere approssimazioni.

Torna all’indice ↑
LEZIONE 06 · Moduli e I/O

Moduli built-in: OS e PATH

Prima di iniziare: le parole della lezione

Built-in, OS e architettura
Built-in significa integrato nel runtime. OS è il sistema operativo. L’architettura indica la famiglia di istruzioni dell’ambiente, per esempio x64 o arm64.
Percorso assoluto e relativo
Un percorso assoluto parte dalla radice o dall’unità; uno relativo dipende da una cartella di riferimento.
Byte e separatore
Un byte è un’unità di informazione di otto bit. Il separatore divide i segmenti di un percorso: Windows e sistemi Unix possono usarne di diversi.

I moduli integrati sono già disponibili e non si installano con npm. node:os fornisce dati sul sistema; node:path gestisce percorsi in modo multipiattaforma.

// os-path.cjs
const os = require('node:os');
const path = require('node:path');
console.log(os.platform(), os.arch());
console.log('RAM totale (GiB):', (os.totalmem() / 1024**3).toFixed(1));
const file = path.join(__dirname, 'dati', 'utenti.json');
console.log(file);
console.log(path.basename(file)); // utenti.json
console.log(path.extname(file));  // .json

Esempio aggiuntivo: path.resolve('dati', 'utenti.json') crea un percorso assoluto a partire dalla directory di lavoro corrente; path.join combina segmenti e normalizza il percorso. Non concatenare a mano separatori per costruire percorsi.

Chi fa cosa, e perché

os.type() descrive il sistema, os.release() la sua release, os.freemem() la memoria libera e os.totalmem() quella totale, in byte. Dividere per 1024 ** 3 dà gibibyte, abbreviati GiB, anche quando un esempio usa genericamente l’etichetta GB.

path.join unisce segmenti e normalizza separatori e ..; path.resolve costruisce un percorso assoluto; basename estrae il nome finale e extname l’estensione. Nessuna di queste funzioni crea cartelle o verifica che il file esista: manipolano percorsi come testo.

Un altro esempio guidato

// percorsi.cjs — node percorsi.cjs
const path = require('node:path');
const os = require('node:os');
const destinazione = path.join(os.tmpdir(), 'corso-node', 'log.txt');
console.log(path.parse(destinazione));
console.log('Estensione:', path.extname(destinazione));

Come leggerlo e risultato atteso. os.tmpdir() restituisce la cartella temporanea del sistema. path.parse restituisce un oggetto con radice, cartella, nome completo, estensione e nome senza estensione. L’ultima riga stampa .txt; nessun file viene scritto.

Prova tu. Confronta path.resolve("dati") con path.join(__dirname, "dati") avviando lo script da un’altra cartella.

Torna all’indice ↑
LEZIONE 07 · Moduli e I/O

File system: sync e async

Prima di iniziare: le parole della lezione

File system, encoding e Buffer
Il file system organizza file e cartelle. L’encoding stabilisce come convertire byte in testo: UTF-8 è una scelta comune. Un Buffer contiene byte, non una stringa JavaScript.
Sincrono e asincrono
Una chiamata sincrona restituisce il risultato prima di proseguire e può bloccare il thread. Una chiamata asincrona avvia il lavoro e ne rende disponibile il risultato successivamente.
Promise, async e await
Una Promise rappresenta un esito futuro, riuscito o fallito. Una funzione async restituisce una Promise; await sospende quella funzione finché la Promise si risolve o fallisce.
try/catch
try delimita operazioni che possono fallire; catch riceve l’errore e permette di gestirlo.

fs consente di leggere e scrivere file. Le operazioni *Sync bloccano il thread JavaScript fino al termine; per server e attività I/O frequenti preferisci le API asincrone.

// file.cjs
const fs = require('node:fs');
fs.writeFileSync('nota.txt', 'Prima riga\n');
fs.appendFileSync('nota.txt', 'Seconda riga\n');
console.log(fs.readFileSync('nota.txt', 'utf8'));

Stessa operazione con Promise:

// file-async.mjs
import { readFile, writeFile, appendFile } from 'node:fs/promises';
try {
  await writeFile('nota.txt', 'Prima riga\n');
  await appendFile('nota.txt', 'Seconda riga\n');
  const testo = await readFile('nota.txt', 'utf8');
  console.log(testo);
} catch (errore) {
  console.error('Errore file:', errore.message);
}

Precisazione: writeFile sovrascrive per impostazione predefinita; appendFile aggiunge in fondo. Non mischiare letture e scritture concorrenti dello stesso file senza coordinarle.

Chi fa cosa, e perché

La versione a callback del corso segue la convenzione error-first: il primo parametro contiene l’eventuale errore, il secondo il risultato. Senza utf8, una lettura restituisce normalmente un Buffer. Se devi leggere due file e poi scrivere il contenuto combinato, devi aspettare entrambe le letture: posizionare la scrittura sotto le chiamate asincrone non basta.

Le Promise evitano di annidare molte callback. Due await consecutivi mantengono l’ordine delle operazioni; Promise.all può attendere più letture indipendenti. Non rende sicure scritture concorrenti sullo stesso file. Le API sincrone possono andare bene per piccoli script o preparazione iniziale; dentro una route rallentano anche gli altri utenti.

Un altro esempio guidato

// lettura.cjs — crea prima nota.txt nella cartella di avvio
const fs = require('node:fs');
console.log('1. Avvio lettura');
fs.readFile('nota.txt', 'utf8', (errore, testo) => {
  if (errore) {
    console.error('Lettura fallita:', errore.code);
    return;
  }
  console.log('3. Contenuto:', testo);
});
console.log('2. Il programma può proseguire');

Come leggerlo e risultato atteso. Le righe 1 e 2 vengono stampate prima della callback. Se il file manca, leggi tipicamente ENOENT, codice che segnala un percorso inesistente. return impedisce di usare testo dopo l’errore. Il percorso relativo viene risolto dalla directory di lavoro.

Prova tu. Rinomina il file di input e osserva il ramo di errore; poi ripristinalo. Verifica che la scrittura con writeFile sostituisca davvero il contenuto precedente prima di usarla su dati importanti.

Torna all’indice ↑
LEZIONE 08 · HTTP e runtime

Creare un server HTTP

Prima di iniziare: le parole della lezione

HTTP e server HTTP
HTTP è il protocollo con cui un client invia richieste e riceve risposte. Un server HTTP resta in ascolto e applica una funzione a ogni richiesta ricevuta.
Porta e localhost
Una porta identifica un punto di accesso di rete di un processo. localhost indica il computer locale; 3000 è una porta di sviluppo convenzionale.
req, res e route
req è l’oggetto della richiesta; res costruisce la risposta. Una route è una regola che collega metodo e percorso a una funzione di gestione.
Header e status
Gli header sono metadati della richiesta o risposta. Lo status è il codice numerico che ne descrive l’esito; 200 indica successo, 404 risorsa non trovata.

Node include node:http, utile per capire il funzionamento di un server senza librerie esterne.

// server.cjs
const http = require('node:http');
const server = http.createServer((req, res) => {
  if (req.url === '/' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    return res.end('Benvenuto!');
  }
  res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('Non trovato');
});
server.listen(3000, () => console.log('http://localhost:3000'));

Avvia con node server.cjs, visita http://localhost:3000 e prova /manca per il 404.

Concetto chiave: req contiene i dati della richiesta, res costruisce la risposta. Per il routing complesso, Express riduce il codice ripetitivo.

Chi fa cosa, e perché

http.createServer crea il server e registra la callback, ma non apre da solo la porta. listen lo mette in ascolto. Il callback di listen viene eseguito all’avvio; quello di createServer viene richiamato per ogni richiesta.

res.writeHead imposta status e header; res.write può inviare una parte del contenuto; res.end conclude la risposta, eventualmente con l’ultimo contenuto. Dopo aver risposto, un return evita di cadere nel ramo 404. Un server resta attivo perché ha risorse di rete in ascolto: fermalo con Ctrl+C.

Un altro esempio guidato

// ora.cjs — node ora.cjs
const http = require('node:http');
const server = http.createServer((req, res) => {
  const url = new URL(req.url, 'http://localhost');
  if (req.method === 'GET' && url.pathname === '/ora') {
    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
    return res.end(JSON.stringify({ ora: new Date().toISOString() }));
  }
  res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('Percorso non trovato');
});
server.listen(3000, '127.0.0.1', () => console.log('Apri http://127.0.0.1:3000/ora'));

Come leggerlo e risultato atteso. new URL separa il percorso dalla query: anche /ora?formato=iso ha pathname /ora. La data è convertita in testo ISO; JSON.stringify produce il body JSON. Un’altra route riceve 404. Il server dell’esempio ascolta solo sull’interfaccia locale.

Prova tu. Apri /ora due volte e confronta i timestamp; prova poi un percorso inesistente e controlla lo status negli strumenti di rete del browser.

Torna all’indice ↑
LEZIONE 09 · Progetto e tooling

npm: pacchetti e comandi

Prima di iniziare: le parole della lezione

Pacchetto, dipendenza e registry
Un pacchetto è codice distribuito con metadati; una dipendenza è un pacchetto richiesto dal progetto. Il registry è il servizio da cui npm scarica i pacchetti.
Installazione locale e globale
Locale significa nella struttura del progetto; globale significa nella posizione configurata per gli strumenti della macchina, selezionata con -g.
node_modules e dipendenze transitive
node_modules contiene i pacchetti installati. Le dipendenze transitive sono quelle richieste dalle tue dipendenze.
CI
Continuous Integration: controlli automatici eseguiti sul progetto, per esempio installazione e test a ogni modifica.

npm è il gestore dei pacchetti usato normalmente con Node.js. Permette di installare librerie esterne e di eseguire script definiti nel progetto.

npm init -y
npm install express@5
npm install --save-dev nodemon
npm uninstall nodemon
npm ls --depth=0
npm run dev

npm run dev funziona solo se esiste uno script dev nel package.json. Esempio aggiuntivo: per riprodurre installazioni coerenti in progetti dotati di lockfile usa npm ci anziché npm install in CI. Non copiare casualmente dipendenze da tutorial datati.

Chi fa cosa, e perché

Il corso mostra librerie come Lodash e Bootstrap per rendere concreto il concetto di pacchetto: il primo offre funzioni di utilità, il secondo componenti e stili per il frontend. Installare un pacchetto non significa che sia tutto eseguibile nel browser o tutto utile a Node: dipende dal pacchetto.

npm init -y crea il manifesto con valori iniziali. npm install express installa la dipendenza locale e aggiorna manifesto e lockfile. npm uninstall rimuove il pacchetto dalle dipendenze dichiarate e aggiorna l’installazione: evita di cancellare a mano cartelle casuali in node_modules.

Un altro esempio guidato

# Dentro una cartella di esercizio
npm init -y
npm install express@5
npm ls --depth=0
npm explain express

Come leggerlo e risultato atteso. npm ls --depth=0 mostra le dipendenze dirette, senza espandere tutto l’albero. npm explain express spiega perché il pacchetto è installato. @5 richiede la major 5; il lockfile registra la versione precisa risolta.

Prova tu. Spiega la differenza tra il comando npm che installa Express e il codice require("express") che lo carica durante l’esecuzione.

Torna all’indice ↑
LEZIONE 10 · Progetto e tooling

package.json e versioni

Prima di iniziare: le parole della lezione

Manifesto e lockfile
Il manifesto dichiara il progetto e gli intervalli di versioni accettati. Il lockfile registra l’albero risolto con versioni precise e informazioni di integrità.
SemVer
Semantic Versioning: schema major.minor.patch. La major segnala modifiche incompatibili dichiarate, la minor funzionalità compatibili, la patch correzioni compatibili.
Script npm
Comando con un nome definito nell’oggetto scripts, avviabile con npm run nome. npm start è la forma breve per lo script start.

package.json è il manifesto del progetto: metadati, script e dipendenze. package-lock.json registra le versioni risolte ed è normalmente da includere nel versionamento.

{
  "name": "laboratorio-node",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js"
  },
  "dependencies": {
    "express": "^5.0.0"
  }
}

Attenzione: la versione di Express qui è illustrativa, non un vincolo da incollare ciecamente. È meglio usare npm install express, lasciando che npm scriva la versione effettivamente installata.

  • ^1.2.3: permette aggiornamenti compatibili secondo SemVer all'interno della major 1.
  • ~1.2.3: permette generalmente aggiornamenti patch nella minor 2.
  • dependencies: necessarie all'esecuzione; devDependencies: strumenti di sviluppo.

Chi fa cosa, e perché

name identifica il pacchetto; version è la versione del tuo progetto, non quella di Node; private: true impedisce la pubblicazione accidentale con npm. type sceglie l’interpretazione dei file .js. main, quando presente, descrive un punto di ingresso del pacchetto per chi lo carica: non sostituisce lo script start.

dependencies raccoglie ciò che serve in esecuzione; devDependencies gli strumenti usati nello sviluppo. Negli script, npm aggiunge al PATH i comandi dei pacchetti locali: per questo nodemon può funzionare senza installazione globale. Conserva nel controllo di versione manifesto e lockfile; normalmente escludi node_modules.

Un altro esempio guidato

{
  "name": "prima-api",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "start": "node server.cjs",
    "dev": "node --watch server.cjs",
    "check": "node --check server.cjs"
  }
}

Come leggerlo e risultato atteso. È un manifesto iniziale valido: salvalo come package.json nella cartella dell’esercizio, crea server.cjs e installa Express per aggiungere la dipendenza. npm run check controlla la sintassi senza avviare il server. JSON richiede doppi apici e non accetta commenti o virgole finali.

Prova tu. Crea saluto.cjs con una stampa e aggiungi "saluto": "node saluto.cjs" agli script; avvialo con npm run saluto.

Torna all’indice ↑
LEZIONE 11 · Progetto e tooling

Riavvio automatico: nodemon e watch

Prima di iniziare: le parole della lezione

Watcher e riavvio
Un watcher osserva modifiche ai file. Il riavvio termina il processo precedente e ne avvia uno nuovo, rieseguendo il programma dall’inizio.
Dipendenza di sviluppo
Strumento necessario per lavorare sul progetto, ma non per il normale funzionamento del server distribuito.
Hot reload
Aggiornamento del codice o dell’interfaccia senza un completo riavvio del contesto: non coincide con il semplice riavvio di Node.

Il problema mostrato nelle prime lezioni è dover riavviare manualmente Node dopo ogni modifica. nodemon lo risolve; le versioni moderne di Node offrono anche --watch.

# soluzione senza pacchetto aggiuntivo
node --watch server.js
# alternativa con dipendenza di sviluppo
npm install --save-dev nodemon
npx nodemon server.js

Esempio aggiuntivo: nel package.json imposta "dev": "node --watch server.js", poi esegui npm run dev. Per la produzione usa normalmente uno script di avvio stabile e una gestione appropriata del processo, non un watcher di sviluppo.

Chi fa cosa, e perché

Nel testo Nodemon risolve la sequenza “salva, Ctrl+C, rilancia”. Osserva i file e riavvia il processo. Non aggiorna automaticamente una scheda del browser: devi fare una nuova richiesta per vedere il comportamento del server aggiornato.

Il riavvio ricrea variabili, array e contatori. Se inserisci una persona nell’array RAM e poi salvi un file mentre il watcher è attivo, quella persona può sparire: il nuovo processo riparte dai dati iniziali. Non è un errore di POST, ma una conseguenza dell’assenza di persistenza.

Un altro esempio guidato

// osserva.cjs — avvio: node --watch osserva.cjs
console.log('Nuovo processo:', process.pid);
console.log('Avviato:', new Date().toISOString());
setInterval(() => console.log('Ancora attivo'), 5000);

Come leggerlo e risultato atteso. Il timer ripetuto mantiene attivo il programma. Cambia il messaggio e salva: il watcher riavvia lo script, cambia il timestamp e normalmente cambia anche il PID, cioè l’identificatore del processo. Con Ctrl+C termini la sessione di osservazione.

Prova tu. Aggiungi una variabile contatore e osserva che torna al valore iniziale a ogni riavvio. Distingui il riavvio del server dal refresh del browser.

Torna all’indice ↑
LEZIONE 12 · HTTP e runtime

Event loop e non-blocking

Prima di iniziare: le parole della lezione

Call stack ed event loop
Il call stack tiene traccia delle funzioni in esecuzione. L’event loop coordina quando eseguire callback di timer, rete e altre operazioni, quando lo stack permette di farlo.
Concorrenza e parallelismo
Concorrenza significa gestire più attività nello stesso periodo; parallelismo significa eseguirle davvero nello stesso istante su risorse distinte.
Thread pool e libuv
Un thread pool è un gruppo di thread riutilizzati per certi lavori. libuv è la libreria su cui Node basa parte della gestione di I/O, event loop e thread pool.
Microtask e CPU-bound
Le microtask includono reazioni alle Promise e vengono elaborate in specifici punti dell’esecuzione. CPU-bound indica lavoro dominato dal calcolo, anziché dall’attesa di dati.

L'event loop orchestra l'esecuzione del codice e dei callback quando le operazioni asincrone terminano. Non confondere asincronia, parallelismo e multithreading: il codice JavaScript di un processo Node normalmente gira su un thread principale, mentre parte del lavoro I/O è delegata al sistema operativo o al thread pool.

console.log('A');
setTimeout(() => console.log('C (timer)'), 0);
Promise.resolve().then(() => console.log('B (microtask)'));
console.log('D');
// A, D, B, C

Esempio aggiuntivo: await readFile(...) lascia spazio ad altre operazioni durante l'attesa; un ciclo while che occupa la CPU per molti secondi blocca comunque l'event loop. Per calcoli pesanti valuta worker thread o processi separati.

Chi fa cosa, e perché

Nella banca, lo sportellista non si moltiplica: coordina le pratiche. Allo stesso modo il JavaScript principale esegue un pezzo di codice alla volta; il sistema operativo può gestire attese di rete e il thread pool alcune operazioni, fra cui molte operazioni sui file. Dire “ogni richiesta ha un thread” oppure “Node usa un solo thread in assoluto” descrive male il funzionamento.

Nell’esempio A/D/B/C, le due stampe sincrone vengono prima della reazione della Promise; il timer non può interrompere il codice già in esecuzione. setTimeout(fn, 0) significa pianificare appena possibile dopo la soglia del timer, non chiamare subito fn. La parola async non sposta un ciclo pesante su un altro thread.

Un altro esempio guidato

// attese.mjs — node attese.mjs
import { setTimeout as pausa } from 'node:timers/promises';
async function lavoro(nome, ms) {
  await pausa(ms);
  return nome;
}
const risultati = await Promise.all([
  lavoro('lento', 80),
  lavoro('veloce', 20)
]);
console.log(risultati);

Come leggerlo e risultato atteso. Le due attese vengono avviate prima di attenderne l’esito insieme. Il risultato è ["lento", "veloce"]: Promise.all preserva l’ordine degli input, non quello di completamento. Se una Promise fallisce, l’attesa complessiva fallisce; le altre operazioni non sono cancellate automaticamente.

Prova tu. Sostituisci Promise.all con due await consecutivi e confronta il tempo complessivo: nel primo caso è vicino all’attesa maggiore, nel secondo alla somma, con variazioni dovute alla schedulazione.

Torna all’indice ↑
LEZIONE 13 · HTTP e runtime

EventEmitter e programmazione a eventi

Prima di iniziare: le parole della lezione

Evento, emittente e listener
Un evento è una notifica con un nome ed eventuali dati. L’emittente la produce; un listener è la funzione registrata per reagire.
EventEmitter e istanza
EventEmitter è la classe di Node per notifiche a eventi. Un’istanza, creata con new, è un oggetto con i propri listener.
on, once, emit e off
on registra un listener; once lo registra per una sola esecuzione; emit emette; off rimuove un listener specifico.

EventEmitter fornisce un modello per emettere eventi e registrare ascoltatori. È utile per notifiche interne, strumenti e flussi applicativi.

// eventi.cjs
const { EventEmitter } = require('node:events');
const bus = new EventEmitter();
bus.on('utenteCreato', utente => console.log('Creato:', utente.nome));
bus.once('primoAccesso', () => console.log('Solo una volta'));
bus.emit('utenteCreato', { nome: 'Luca' });
bus.emit('primoAccesso');
bus.emit('primoAccesso'); // nessun secondo log

Attenzione: emit() invoca sincronamente gli ascoltatori registrati. EventEmitter non crea automaticamente code persistenti né rende le funzioni asincrone. Gestisci con cura gli eventi error.

Chi fa cosa, e perché

L’ordine conta: registra gli ascoltatori prima di emettere l’evento. Un normale EventEmitter non conserva un evento per chi si iscrive dopo. Gli argomenti passati dopo il nome in emit vengono consegnati ai listener nello stesso ordine.

La registrazione non chiama subito la funzione; emit chiama invece in modo sincrono gli ascoltatori già registrati. Se un listener avvia lavoro asincrono, quel lavoro ha la propria durata: l’emittente non aspetta automaticamente tutte le Promise. Il modello serve per disaccoppiare una notifica dalle reazioni, ma non garantisce consegna persistente fra processi.

Un altro esempio guidato

// ascoltatori.cjs — node ascoltatori.cjs
const { EventEmitter } = require('node:events');
const eventi = new EventEmitter();
function registra(id, totale) {
  console.log(`Ordine ${id}: ${totale} euro`);
}
eventi.on('ordine', registra);
eventi.emit('ordine', 7, 25);
eventi.off('ordine', registra);
eventi.emit('ordine', 8, 30);

Come leggerlo e risultato atteso. Viene stampato solo l’ordine 7. Per rimuovere un listener serve il riferimento alla stessa funzione: una nuova arrow function, anche con lo stesso corpo, è un altro oggetto funzione. Nell’esempio iniziale il secondo primoAccesso non stampa nulla perché il listener era registrato con once.

Prova tu. Aggiungi due listener allo stesso evento e verifica l’ordine delle stampe; poi emetti prima di registrarli per osservare che l’evento non viene riprodotto.

Torna all’indice ↑
LEZIONE 14 · HTTP e runtime

Stream: dati a pezzi

Prima di iniziare: le parole della lezione

Chunk, stream e buffering
Un chunk è una porzione di dati. Uno stream permette di leggerne o scriverne una sequenza; il buffering conserva temporaneamente porzioni in memoria.
Readable, Writable, Duplex e Transform
Readable produce dati; Writable li riceve; Duplex può fare entrambe le cose; Transform è uno stream duplex che trasforma ciò che riceve, come una compressione.
Backpressure e pipeline
La backpressure rallenta il produttore quando il consumatore non tiene il passo. Una pipeline collega più stream e ne coordina completamento ed errori.

Uno stream gestisce dati in modo incrementale, senza necessariamente caricare l'intero contenuto in memoria. È utile per file grandi, download e trasferimenti HTTP.

// copia.cjs
const fs = require('node:fs');
const { pipeline } = require('node:stream/promises');
(async () => {
  try {
    await pipeline(
      fs.createReadStream('origine.txt'),
      fs.createWriteStream('copia.txt')
    );
    console.log('Copia completata');
  } catch (errore) { console.error(errore.message); }
})();

Esempio aggiuntivo: con gli stream si può inviare al client un file di grandi dimensioni mentre lo si legge. pipeline() propaga in modo più affidabile errori e chiusura delle risorse rispetto a un semplice pipe() usato senza gestione degli errori.

Chi fa cosa, e perché

readFile rende disponibile il file completo; createReadStream lo fornisce a porzioni. Nel corso gli eventi data mostrano i chunk: la loro dimensione non coincide necessariamente con una riga o con un messaggio applicativo. Un file può quindi essere letto senza tenerlo tutto in memoria contemporaneamente.

highWaterMark controlla una soglia del buffer interno, non un limite rigido alla memoria totale. pipe collega una sorgente a una destinazione e gestisce il flusso; pipeline aiuta a coordinare anche errori e chiusura. Le richieste e risposte HTTP di Node sono a loro volta stream.

Un altro esempio guidato

// dimensione.mjs — crea prima origine.txt; node dimensione.mjs
import { createReadStream } from 'node:fs';
let byte = 0;
try {
  for await (const chunk of createReadStream('origine.txt')) {
    byte += chunk.length;
  }
  console.log('Byte letti:', byte);
} catch (errore) {
  console.error('Lettura fallita:', errore.message);
}

Come leggerlo e risultato atteso. for await...of consuma le porzioni disponibili senza dover scrivere a mano i listener data ed end. Senza encoding i chunk sono Buffer e length conta byte: lettere accentate UTF-8 possono occupare più di un byte.

Prova tu. Crea un file con caratteri accentati e confronta il numero di byte con il numero di caratteri del testo. Non sommare chunk.toString().length come se fossero sempre la stessa misura.

Torna all’indice ↑
LEZIONE 15 · HTTP e runtime

Richieste HTTP: struttura e metodi

Prima di iniziare: le parole della lezione

URL, host e percorso
L’URL identifica una risorsa: schema, host, eventuale porta, percorso e query. L’host identifica il computer o servizio di destinazione; il percorso identifica la risorsa presso quel servizio.
Body, Content-Type e Accept
Il body è il contenuto del messaggio. Content-Type descrive il formato inviato; Accept indica i formati di risposta accettati dal client.
HTTPS e TLS
HTTPS è HTTP protetto da TLS, che cifra il trasporto e autentica normalmente il server tramite certificati.
Idempotenza
Un’operazione è idempotente se ripeterla produce lo stesso effetto finale sullo stato desiderato. Non implica risposte identiche a ogni chiamata.

Una richiesta HTTP include metodo, percorso, header e opzionalmente un body. Una risposta include codice di stato, header e body. HTTP è un protocollo richiesta-risposta; HTTPS aggiunge la protezione TLS.

Metodo Uso tipico Esempio
GET Leggere GET /api/persone
POST Creare POST /api/persone
PUT Sostituire/aggiornare completamente PUT /api/persone/1
PATCH Modificare parzialmente PATCH /api/persone/1
DELETE Eliminare DELETE /api/persone/1

Codici frequenti: 200 OK, 201 creato, 204 riuscito senza contenuto, 400 richiesta errata, 404 non trovato, 500 errore del server.

curl -i http://localhost:3000/

Esempio aggiuntivo: un browser che apre un URL invia normalmente una GET; per inviare JSON e scegliere altri metodi puoi usare curl, Postman o il fetch() del frontend.

Chi fa cosa, e perché

Il browser invia la richiesta, il server la interpreta e costruisce la risposta. Aprire una pagina può produrre diverse richieste: documento HTML, foglio di stile, JavaScript, immagini e favicon. Un errore sulla favicon non significa necessariamente che la route principale abbia fallito.

I metodi esprimono l’intenzione: GET legge, POST richiede un’elaborazione o una creazione, PUT sostituisce, PATCH modifica parzialmente, DELETE elimina. Una risposta 404 significa che il server ha risposto ma non ha trovato quella risorsa; una connessione rifiutata significa invece che non sei riuscito a contattarlo su quell’indirizzo e porta.

Un altro esempio guidato

GET /api/persone?limite=2 HTTP/1.1
Host: localhost:3000
Accept: application/json

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"dati":[{"id":1,"nome":"Anna"}]}

Come leggerlo e risultato atteso. È uno schema didattico di richiesta e risposta HTTP/1.1, non un file JavaScript da avviare. La riga iniziale identifica metodo e destinazione; la riga vuota separa header e body. Il codice 200 appartiene alla risposta, non alla richiesta.

Prova tu. Apri gli strumenti sviluppatore del browser, scheda Rete/Network: identifica richiesta, status, Content-Type e contenuto della risposta. Non confondere il testo di un errore JSON con lo status HTTP.

Torna all’indice ↑
LEZIONE 16 · Express e API

Introduzione a Express

Prima di iniziare: le parole della lezione

Express e app
Express è il framework che organizza la gestione HTTP sopra Node. express() crea un’applicazione, qui chiamata app.
Routing, handler ed endpoint
Il routing seleziona la funzione in base a metodo e percorso. L’handler gestisce la richiesta. Un endpoint è un punto di accesso dell’API, generalmente identificato da metodo e URL.
Middleware
Funzione nella catena di gestione di una richiesta: può eseguire controlli, preparare dati, rispondere o passare il controllo alla funzione successiva.

Express è un framework web che semplifica server, middleware e routing. Non sostituisce Node: vi gira sopra.

mkdir mini-express
cd mini-express
npm init -y
npm install express@5
// server.cjs
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Home Express'));
app.get('/saluti/:nome', (req, res) => {
  res.json({ messaggio: `Ciao ${req.params.nome}` });
});
app.listen(3000, () => console.log('http://localhost:3000'));

Esegui node server.cjs. Esempio aggiuntivo: visita /saluti/Anna. Il callback della route riceve req (richiesta) e res (risposta).

Chi fa cosa, e perché

require("express") carica il pacchetto installato. express() costruisce l’app; app.get registra una route GET; app.listen crea e avvia il server HTTP. Express estende gli oggetti di Node con strumenti come res.send, res.json e req.params.

res.status(201) imposta lo status ma non invia da solo la risposta. res.send e res.json la inviano e normalmente la concludono. La concatenazione res.status(...).json(...) funziona perché status restituisce lo stesso oggetto risposta. Invia una sola risposta per richiesta.

Un altro esempio guidato

// mini.cjs — npm install express@5; poi node mini.cjs
const express = require('express');
const app = express();
app.get('/salute', (req, res) => {
  return res.status(200).json({ servizio: 'attivo' });
});
app.use((req, res) => {
  res.status(404).json({ errore: 'Percorso non trovato' });
});
app.listen(3000, '127.0.0.1', () => console.log('Server pronto sulla porta 3000'));

Come leggerlo e risultato atteso. GET /salute restituisce 200 e JSON. Una richiesta non gestita arriva al middleware finale e riceve 404. Il middleware senza percorso si applica a tutte le richieste che lo raggiungono. Deve stare dopo le route, altrimenti risponde prima di esse.

Prova tu. Aggiungi GET /versione prima della 404 e restituisci process.version. Prova anche POST /salute: avere una route GET non registra automaticamente una POST.

Torna all’indice ↑
LEZIONE 17 · Express e API

Servire file statici

Prima di iniziare: le parole della lezione

File statico e directory pubblica
Un file statico viene inviato senza generare il contenuto da un template a ogni richiesta. La directory pubblica contiene i file che scegli di rendere raggiungibili dai client.
Mount path e sendFile
Il mount path è il prefisso URL a cui colleghi un middleware. res.sendFile invia uno specifico file, normalmente indicato con percorso assoluto o con una directory root.
HTML, CSS e JavaScript client
HTML descrive la struttura della pagina; CSS il suo aspetto; il JavaScript client viene eseguito dal browser dopo il caricamento.

I file statici (HTML, CSS, immagini e JavaScript client) possono essere serviti direttamente da una directory con express.static.

// server.cjs
const express = require('express');
const path = require('node:path');
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
app.listen(3000);
<!-- public/index.html -->
<!doctype html><html lang="it"><meta charset="utf-8">
<title>Demo</title><h1>Benvenuto nel sito statico</h1></html>

Esempio aggiuntivo: creando public/stile.css il file diventa recuperabile con /stile.css. Nota: l'esempio usa CommonJS e quindi __dirname. Evita di mettere nella cartella pubblica credenziali o file sensibili.

Chi fa cosa, e perché

express.static restituisce un middleware: Express lo esegue sulle richieste che arrivano a quel punto. Con app.use(express.static(...)), un file public/stile.css si richiede come /stile.css, senza scrivere public nell’URL. Un index.html può essere servito automaticamente per la directory.

Il corso usa anche homepage.html, about.html, contatti.html e una pagina 404 con sendFile. La cartella fisica e il percorso web non devono coincidere: puoi esporre i file sotto /assets. “Statico” riguarda come il server produce la risposta: un file JavaScript statico può comunque rendere interattiva la pagina.

Un altro esempio guidato

// Dentro l'app Express CommonJS, prima della 404 finale
const path = require('node:path');
const cartella = path.join(__dirname, 'public');
app.use('/assets', express.static(cartella));
app.get('/about', (req, res, next) => {
  res.sendFile(path.join(cartella, 'about.html'), errore => {
    if (errore) next(errore);
  });
});

Come leggerlo e risultato atteso. Crea public/about.html con un titolo e un paragrafo. /about invia quel file; un eventuale public/stile.css è raggiungibile a /assets/stile.css. next(errore) passa un errore di invio alla gestione degli errori.

Prova tu. Collega un foglio CSS da about.html con href="/assets/stile.css". Crea anche una 404 e inviala con res.status(404).sendFile(...), gestendo gli errori di invio.

Torna all’indice ↑
LEZIONE 18 · Express e API

API JSON e server-side rendering

Prima di iniziare: le parole della lezione

API web e rappresentazione
Un’API web offre operazioni tramite HTTP. Una rappresentazione è il formato con cui una risorsa viene descritta al client, per esempio JSON o HTML.
Rendering, SSR e CSR
Rendering significa costruire la vista. SSR, Server-Side Rendering, la costruisce sul server; CSR, Client-Side Rendering, la costruisce nel browser usando JavaScript.
Template engine ed escaping
Un template engine combina un modello di pagina con dati. L’escaping trasforma caratteri speciali per visualizzarli come testo anziché interpretarli come markup.

Il corso distingue due modalità: API, che restituiscono normalmente dati come JSON, e SSR, in cui il server genera HTML per il browser. Sono approcci combinabili.

app.get('/api/profilo', (req, res) => {
  res.json({ id: 1, nome: 'Sara' });
});
app.get('/pagina', (req, res) => {
  res.type('html').send('<h1>Pagina generata dal server</h1>');
});

Esempio aggiuntivo: un frontend React può chiamare /api/profilo e costruire la vista nel browser; un'app SSR potrebbe invece renderizzare lato server un template con lo stesso profilo. In entrambi i casi serve gestire correttamente errori e dati in ingresso.

Chi fa cosa, e perché

Con un’API JSON il backend recupera e controlla i dati; il frontend decide come mostrarli. Con SSR il backend recupera i dati e genera anche l’HTML della vista, spesso usando un template engine. La scelta riguarda dove si prepara la pagina, non se esista o meno un database.

Le due modalità possono convivere: un sito serve pagine HTML e offre endpoint JSON per aggiornare singole parti. Un array restituito come JSON non si trasforma da solo in una tabella: serve codice client o un template. Analogamente, servire un file HTML senza modificarlo rimane semplice distribuzione di contenuto statico.

Un altro esempio guidato

<!-- demo-client.html: servilo dalla cartella public dell'app -->
<p id="nome">Caricamento...</p>
<script>
async function carica() {
  const risposta = await fetch('/api/profilo');
  if (!risposta.ok) throw new Error(`HTTP ${risposta.status}`);
  const profilo = await risposta.json();
  document.querySelector('#nome').textContent = profilo.nome;
}
carica().catch(() => {
  document.querySelector('#nome').textContent = 'Impossibile caricare';
});
</script>

Come leggerlo e risultato atteso. Usa l’endpoint /api/profilo della lezione e servi questa pagina dalla stessa app. Il browser esegue fetch, riceve JSON e aggiorna il testo con Sara. response.json() interpreta il body; textContent visualizza il nome come testo, senza inserirlo come HTML.

Prova tu. Sostituisci il percorso fetch con uno inesistente e verifica il messaggio di errore. Il controllo risposta.ok è necessario perché fetch non rigetta automaticamente per una risposta 404.

Torna all’indice ↑
LEZIONE 19 · Express e API

JSON come formato di scambio

Prima di iniziare: le parole della lezione

JSON e oggetto JavaScript
JSON, JavaScript Object Notation, è un formato di testo per dati. Un oggetto JavaScript è invece un valore già presente in memoria: le due cose non sono identiche.
Serializzazione e parsing
Serializzare significa convertire un valore in testo trasmissibile. Fare parsing significa analizzare il testo e ricostruire una struttura di dati.
Schema e validazione
Uno schema descrive campi, tipi e vincoli attesi. La validazione verifica che i dati rispettino quelle regole, dopo che il testo è stato interpretato.

JSON è un formato testuale con oggetti, array, stringhe, numeri, booleani e null. In Express res.json() serializza un oggetto JavaScript e imposta la risposta JSON.

app.use(express.json()); // interpreta richieste con body JSON
app.get('/api/prodotti', (req, res) => {
  res.json([{ id: 1, nome: 'Tastiera', prezzo: 45 }]);
});
app.post('/api/eco', (req, res) => {
  res.status(201).json({ ricevuto: req.body });
});

Esempio aggiuntivo: invia {"nome":"Mouse"} con header Content-Type: application/json. Sicurezza: il fatto che req.body sia stato interpretato come JSON non significa che i dati siano validi: controlla tipi, lunghezze e valori.

Chi fa cosa, e perché

res.json(oggetto) serializza il valore e imposta il formato della risposta. express.json() compie l’operazione opposta per le richieste con Content-Type JSON: legge il body e rende il valore disponibile come req.body. Non serve per poter inviare JSON con una GET; serve per leggere JSON in ingresso.

JSON usa doppi apici per chiavi e stringhe, non accetta commenti, funzioni o undefined. Una data JavaScript viene normalmente serializzata come stringa; un array può contenere oggetti, numeri e altri valori JSON. Il middleware non decide se un prezzo è positivo o se un nome è troppo lungo: sono regole applicative da verificare dopo.

Un altro esempio guidato

// conversioni.cjs — node conversioni.cjs
const persona = { nome: 'Anna', attiva: true, ruoli: ['editor'] };
const testo = JSON.stringify(persona);
const ricostruita = JSON.parse(testo);
console.log(typeof testo);       // string
console.log(ricostruita.nome);   // Anna
console.log(persona === ricostruita); // false

Come leggerlo e risultato atteso. JSON.stringify crea testo; JSON.parse crea una nuova struttura dati. Il confronto finale è falso perché i due oggetti hanno identità diverse. Non passare a res.json una stringa già serializzata se vuoi trasmettere un oggetto: finiresti per inviare una stringa JSON.

Prova tu. Prova JSON con una virgola finale e osserva il fallimento di JSON.parse. Poi invia un JSON valido ma con nome: 123: il parser riesce, la validazione del nome deve fallire.

Torna all’indice ↑
LEZIONE 20 · Express e API

Parametri della route

Prima di iniziare: le parole della lezione

Parametro di percorso e ID
Un parametro di percorso è un segmento variabile della route. Un ID è un identificatore della risorsa: può essere numerico oppure una stringa, secondo il modello dati.
find e valore assente
find restituisce il primo elemento che soddisfa una condizione; se nessuno la soddisfa restituisce undefined.
Conversione e intero sicuro
La conversione cambia il tipo di un valore. Un intero sicuro è rappresentabile senza perdita di precisione nel formato Number di JavaScript.

I parametri della route identificano una risorsa all'interno dell'URL. Nel percorso /api/persone/:id, il segmento :id è letto da req.params.id.

const persone = [{ id: 1, nome: 'Anna' }, { id: 2, nome: 'Luca' }];
app.get('/api/persone/:id', (req, res) => {
  const id = Number(req.params.id);
  if (!Number.isInteger(id)) return res.status(400).json({ errore: 'ID non valido' });
  const persona = persone.find(p => p.id === id);
  if (!persona) return res.status(404).json({ errore: 'Non trovata' });
  res.json(persona);
});

Esempio aggiuntivo: /api/persone/2 restituisce Luca; /api/persone/abc restituisce 400, mentre /api/persone/999 restituisce 404.

Chi fa cosa, e perché

Express riempie req.params usando i nomi preceduti da due punti. In /articoli/:categoria/:id puoi leggere req.params.categoria e req.params.id. La forma const { id } = req.params è destructuring, non una richiesta al database.

Se gli ID dell’array sono numeri, confrontare p.id === req.params.id fallisce perché "2" è una stringa. Convertire e validare risolve il problema. Non convertire però un ID testuale, per esempio un UUID, come se fosse numerico. Distingui ID scritto male (400) e ID valido che non esiste (404).

Un altro esempio guidato

// Frammento da inserire in un'app Express
app.get('/articoli/:categoria/:id', (req, res) => {
  const { categoria, id: testoId } = req.params;
  const id = Number(testoId);
  if (!/^[1-9]\d*$/.test(testoId) || !Number.isSafeInteger(id)) {
    return res.status(400).json({ errore: 'ID numerico positivo richiesto' });
  }
  res.json({ categoria, id });
});

Come leggerlo e risultato atteso. /articoli/node/12 restituisce categoria node e ID numerico 12. L’espressione regolare controlla che la stringa contenga solo cifre e inizi da 1 a 9; isSafeInteger controlla il numero. Questo endpoint dimostra il parsing e non verifica l’esistenza di un articolo.

Prova tu. Prova 12, abc, 0 e 1e2. Spiega perché il controllo numerico e la ricerca della risorsa rispondono a due domande diverse.

Torna all’indice ↑
LEZIONE 21 · Express e API

Query string e filtri

Prima di iniziare: le parole della lezione

Query string e filtro
La query string è la parte dell’URL dopo ?, composta da coppie chiave-valore separate da &. Un filtro seleziona elementi secondo una condizione.
Paginazione, limite e offset
La paginazione divide un insieme in parti. Il limite indica quanti elementi restituire; l’offset quanti saltarne dall’inizio.
filter e slice
filter crea un array con gli elementi che soddisfano una condizione. slice restituisce una porzione senza modificare l’array originale.

I parametri query seguono ? nell'URL: /api/persone?cerca=an&limite=5. In Express sono in req.query, che contiene input da considerare non attendibile.

app.get('/api/persone', (req, res) => {
  const cerca = String(req.query.cerca ?? '').toLowerCase();
  const limiteRaw = Number(req.query.limite ?? 10);
  const limite = Number.isInteger(limiteRaw)
    ? Math.min(Math.max(limiteRaw, 1), 100) : 10;
  const risultato = persone
    .filter(p => p.nome.toLowerCase().includes(cerca))
    .slice(0, limite);
  res.json(risultato);
});

Esempio aggiuntivo: la ricerca ?cerca=lu&limite=1 restituisce al massimo una persona il cui nome contiene lu. Per grandi dataset filtri e paginazione andrebbero applicati nel database, non all'array intero.

Chi fa cosa, e perché

Il parametro di percorso identifica tipicamente “quale persona”; la query descrive “quali persone, con quale filtro e limite”. /persone/2 e /persone?limite=2 hanno quindi significati diversi. req.query dipende dal parser configurato: non dare per scontato che ogni valore sia una stringa singola, soprattutto con parametri ripetuti.

La trascrizione usa una ricerca per prefisso: startsWith("an") trova Anna ma non Gianna. Nell’esempio del manuale includes("an") cerca invece in qualsiasi posizione. Documenta quale comportamento scegli. Con una collezione vuota o nessun risultato, una risposta 200 con elenco vuoto è normalmente corretta.

Un altro esempio guidato

// Costruire una query senza concatenare a mano i valori
const query = new URLSearchParams({ cerca: 'Anna Maria', limite: '2' });
console.log(`/api/persone?${query}`);
// /api/persone?cerca=Anna+Maria&limite=2

// Esempio di paginazione su un array già filtrato
const risultati = ['Anna', 'Luca', 'Sara', 'Marta'];
const offset = 2;
const limite = 2;
console.log(risultati.slice(offset, offset + limite));

Come leggerlo e risultato atteso. URLSearchParams codifica lo spazio nella query. L’ultima stampa contiene Sara e Marta: il secondo argomento di slice è l’indice finale escluso. Per una paginazione stabile serve un ordine definito dei risultati.

Prova tu. Prova limite assente, 0, testo e una chiave ripetuta. Decidi se correggere i valori fuori intervallo o rispondere 400, e mantieni la stessa regola nella tua API.

Torna all’indice ↑
LEZIONE 22 · Express e API

Middleware: ordine e next

Prima di iniziare: le parole della lezione

Catena di middleware e next
La catena è la sequenza registrata nell’app. next() passa il controllo alla funzione successiva compatibile; next(err) attiva il percorso di gestione errori.
Autenticazione e autorizzazione
L’autenticazione verifica chi è il chiamante; l’autorizzazione verifica se può compiere quella specifica operazione.
Logging e gestione errori
Il logging registra eventi utili a capire il comportamento del programma. Un gestore errori traduce un problema in una risposta controllata.

Un middleware è una funzione che opera tra richiesta e risposta. Può leggere/modificare req, rispondere o passare il controllo con next(); l'ordine di registrazione conta.

app.use((req, res, next) => {
  console.log(req.method, req.path);
  next();
});
app.use(express.json());
app.get('/salute', (req, res) => res.json({ ok: true }));
app.use((req, res) => res.status(404).json({ errore: 'Route inesistente' }));

Esempio aggiuntivo: un middleware di autenticazione può controllare una sessione prima delle route private. Attenzione: se non invii una risposta e non chiami next(), la richiesta resta in sospeso. Un middleware di errore ha firma (err, req, res, next).

Chi fa cosa, e perché

Nel testo il middleware viene prima scritto nel file principale, poi esportato in un modulo e infine applicato solo a un prefisso. app.use("/api", fn) limita la funzione a quel gruppo; app.get("/profilo", controllo, handler) la lega a una route. Puoi passare più middleware o un array di funzioni.

Un middleware ha tre possibilità: risponde e si ferma, chiama next, oppure segnala un errore. next() non equivale a return: il codice dopo può ancora eseguire. Usa return next() quando vuoi terminare subito il ramo. Un middleware che stampa “accesso effettuato” non autentica realmente nessuno.

Un altro esempio guidato

// Frammento: middleware specifico per una route
function richiediFormatoJson(req, res, next) {
  if (!req.is('application/json')) {
    return res.status(415).json({ errore: 'Invia application/json' });
  }
  return next();
}
app.post('/api/eco', richiediFormatoJson, express.json(), (req, res) => {
  res.json({ ricevuto: req.body });
});
// Evita di registrare anche un'altra POST /api/eco prima di questa.

Come leggerlo e risultato atteso. Il controllo legge il Content-Type e può rispondere 415, formato non supportato. Se passa, il parser interpreta JSON e l’handler risponde. L’ordine è controllo del formato → parser → handler; l’esempio assume che un parser globale precedente non abbia già letto il body.

Prova tu. Aggiungi log prima e dopo next e osserva l’ordine. Scrivi poi una 404 prima della route: vedrai perché quella route non viene più raggiunta.

Torna all’indice ↑
LEZIONE 23 · Express e API

Testare le API con Postman

Prima di iniziare: le parole della lezione

Client HTTP e collection
Un client HTTP invia richieste a un server. Una collection raccoglie richieste salvate e riutilizzabili per uno stesso progetto.
Body raw ed environment
Raw indica un body scritto direttamente come testo. Un environment contiene variabili, come la base URL, da riutilizzare nelle richieste.
Asserzione e regressione
Un’asserzione confronta un risultato con un’aspettativa. Un test di regressione controlla che una modifica non rompa comportamenti che funzionavano.

Postman è un client HTTP grafico, comodo per provare GET/POST/PUT/DELETE, header e JSON. Non è obbligatorio: puoi ottenere test riproducibili anche con curl.

# recuperare tutte le persone
curl -i http://localhost:3000/api/persone
# inviare dati JSON
curl -i -X POST http://localhost:3000/api/persone \
  -H "Content-Type: application/json" \
  -d '{"nome":"Giulia"}'

Esempio aggiuntivo: verifica non solo il contenuto ma anche lo status HTTP. Se la creazione riesce, il server dovrebbe restituire 201; se manca nome, 400. Nota: su Windows PowerShell curl può avere comportamenti diversi in base alla versione e agli alias; usa curl.exe se necessario.

Chi fa cosa, e perché

In Postman scegli metodo e URL completo, poi Body → raw → JSON per scrivere il contenuto. Controlla che sia inviato Content-Type: application/json. Premi Send con il server già attivo: Postman non avvia il backend. Nella risposta osserva status, header, body e tempo impiegato.

Per il laboratorio crea richieste separate per elenco, dettaglio, creazione, sostituzione e cancellazione. Salva il nuovo ID restituito dalla POST: i passaggi successivi devono riferirsi a quella risorsa, non presumere che abbia sempre ID 3. Un JSON con success: false e status 200 non equivale a un errore HTTP correttamente descritto.

Un altro esempio guidato

# PowerShell: dopo aver avviato l'API del laboratorio
$base = 'http://localhost:3000/api/persone'
$body = @{ nome = 'Giulia'; ruolo = 'analista' } | ConvertTo-Json
$creata = Invoke-RestMethod -Method Post -Uri $base -ContentType 'application/json' -Body $body
$creata.id
Invoke-RestMethod -Method Get -Uri "$base/$($creata.id)"

Come leggerlo e risultato atteso. La prima richiesta crea una persona; il risultato viene deserializzato da PowerShell in $creata. La seconda usa il suo ID per recuperarla. Questi comandi funzionano come alternativa ai comandi curl con continuazioni di riga tipiche di Bash.

Prova tu. Salva una richiesta con nome vuoto e controlla che lo status sia 400. Poi prova a server spento e confronta l’errore di connessione con una risposta 404.

Torna all’indice ↑
LEZIONE 24 · CRUD e architettura

API GET: elenco e dettaglio

Prima di iniziare: le parole della lezione

Risorsa e collezione
Una risorsa è un’entità esposta dall’API, per esempio una persona. Una collezione è un insieme di risorse.
Proiezione e contratto
Una proiezione sceglie quali campi restituire. Il contratto dell’API descrive URL, input, formato di risposta ed errori attesi.
RAM e persistenza
La RAM conserva i dati del processo in memoria. La persistenza li salva in modo da ritrovarli dopo un riavvio, per esempio in un database.

GET recupera risorse e non dovrebbe modificarle. Spesso un'API espone due route: elenco e dettaglio.

const persone = [{ id: 1, nome: 'Anna' }, { id: 2, nome: 'Luca' }];
app.get('/api/persone', (req, res) => res.json(persone));
app.get('/api/persone/:id', (req, res) => {
  const persona = persone.find(p => p.id === Number(req.params.id));
  if (!persona) return res.status(404).json({ errore: 'Non trovata' });
  res.json(persona);
});

Esempio aggiuntivo: GET /api/persone restituisce un array; GET /api/persone/1 un singolo oggetto. Gli esempi usano un array in memoria, quindi i dati scompaiono al riavvio.

Chi fa cosa, e perché

La GET dell’elenco restituisce più persone; quella del dettaglio ne cerca una. Una lista vuota è un risultato diverso da un singolo ID non trovato: nella prima situazione puoi rispondere 200 con [], nella seconda 404. La ricerca con find è lineare: su un array deve esaminare elementi fino a trovare quello giusto.

Importare persone.cjs non collega automaticamente una lettura al file sorgente a ogni richiesta. I dati caricati vivono nel processo: modificarli non riscrive il file JavaScript. Inoltre il contratto deve essere coerente: questa lezione restituisce un array nell’elenco, mentre il laboratorio aggiunge un oggetto con metadati totale e dati.

Un altro esempio guidato

// Frammento: app Express e array persone già definiti
app.get('/api/rubrica', (req, res) => {
  const dati = persone.map(({ id, nome }) => ({ id, nome }));
  res.json({ totale: dati.length, dati });
});

Come leggerlo e risultato atteso. map costruisce un nuovo array; il destructuring legge solo id e nome; l’oggetto finale espone quei campi. Se internamente aggiungi altri dati alla persona, questa risposta non li include automaticamente. Il totale qui è la lunghezza dell’elenco restituito.

Prova tu. Aggiungi un campo interno all’array e verifica che non compaia nella rubrica. Poi verifica elenco vuoto, ID valido e ID inesistente.

Torna all’indice ↑
LEZIONE 25 · CRUD e architettura

API POST: creare risorse

Prima di iniziare: le parole della lezione

Creazione e validazione
Creare significa aggiungere una risorsa con una propria identità. Validare significa controllare i dati prima che modifichino lo stato.
Normalizzazione e whitelist
Normalizzare significa portare dati in una forma coerente, per esempio togliere spazi esterni. Una whitelist è l’elenco dei campi esplicitamente accettati.
Location
Header di risposta che può indicare l’URL della risorsa appena creata.

POST inserisce una nuova risorsa. I dati arrivano spesso nel body JSON, da validare prima dell'inserimento.

app.use(express.json());
let prossimoId = 3;
app.post('/api/persone', (req, res) => {
  const nome = req.body?.nome;
  if (typeof nome !== 'string' || !nome.trim()) {
    return res.status(400).json({ errore: 'Nome obbligatorio' });
  }
  const persona = { id: prossimoId++, nome: nome.trim() };
  persone.push(persona);
  res.status(201).json(persona);
});

Esempio aggiuntivo: due POST con lo stesso nome generano due record con ID distinti. In un'app reale occorrono regole di unicità e un database se sono richiesti.

Chi fa cosa, e perché

Il client invia il nome; il server controlla tipo e contenuto, assegna l’ID e aggiunge il record. Il client non deve scegliere l’ID per questa POST. trim() rimuove gli spazi iniziali e finali; va chiamato dopo aver verificato che il valore sia una stringa.

prossimoId++ usa il valore corrente e poi incrementa il contatore. È sufficiente per l’esempio con un singolo processo e dati in RAM. Non usare persone.length + 1: dopo una cancellazione potresti riutilizzare un ID già presente. In un sistema persistente la generazione degli ID va coordinata con l’archivio.

Un altro esempio guidato

// Frammento per l'API: persone e prossimoId già definiti
app.post('/api/iscritti', (req, res) => {
  const nome = req.body?.nome;
  if (typeof nome !== 'string' || nome.trim().length < 2) {
    return res.status(400).json({ errore: 'Nome di almeno 2 caratteri' });
  }
  const persona = { id: prossimoId++, nome: nome.trim() };
  persone.push(persona);
  return res.status(201).location(`/api/persone/${persona.id}`).json(persona);
});

Come leggerlo e risultato atteso. Il parser JSON deve essere registrato prima della route. ?. legge nome senza errore se il body manca. push modifica l’array; la risposta 201 restituisce la nuova persona. Location rimanda alla route di dettaglio già esistente per la stessa collezione.

Prova tu. Invia un ID nel body e verifica che il server usi quello generato da lui. Invia poi due POST uguali: senza una regola di unicità o idempotenza creano due persone diverse.

Torna all’indice ↑
LEZIONE 26 · CRUD e architettura

API PUT: aggiornare

Prima di iniziare: le parole della lezione

PUT e PATCH
PUT esprime la sostituzione della rappresentazione della risorsa all’URL indicato. PATCH esprime una modifica parziale descritta dal body.
Campi modificabili e identità
I campi modificabili costituiscono i dati che l’API permette al client di cambiare. L’identità della risorsa resta quella selezionata dall’URL.
Indice dell’array
Posizione numerica di un elemento, a partire da zero: non è l’ID dell’elemento e può cambiare dopo una cancellazione.

PUT serve convenzionalmente a sostituire la rappresentazione della risorsa; il corso lo usa per aggiornare un elemento di un array. Per modifiche parziali si usa spesso PATCH.

app.put('/api/persone/:id', (req, res) => {
  const id = Number(req.params.id);
  const persona = persone.find(p => p.id === id);
  if (!persona) return res.status(404).json({ errore: 'Non trovata' });
  const nome = req.body?.nome;
  if (typeof nome !== 'string' || !nome.trim())
    return res.status(400).json({ errore: 'Nome non valido' });
  persona.nome = nome.trim();
  res.json(persona);
});

Esempio aggiuntivo: con PUT /api/persone/2 e body {"nome":"Marco"}, l'oggetto con ID 2 viene aggiornato senza cambiare ID.

Chi fa cosa, e perché

La trascrizione parte da un aggiornamento diretto nell’array. Per renderlo affidabile, prima cerca la persona per ID, poi valida tutto ciò che vuoi modificare. Non usare l’ID direttamente come indice: la persona 5 potrebbe trovarsi in posizione 2 oppure non esserci.

Nel modello ridotto con il solo campo nome, l’esempio PUT sopra sostituisce tutti i dati modificabili. Nel laboratorio i campi sono nome e ruolo: una PUT richiede entrambi. Se vuoi cambiare solo ruolo conservando nome, usa un contratto PATCH. Anche una PATCH può essere idempotente quando assegna valori, ma non lo è necessariamente quando esprime operazioni come “incrementa”.

Un altro esempio guidato

// Frammento: aggiunta all'API persone con campo ruolo
app.patch('/api/persone/:id', (req, res) => {
  const id = Number(req.params.id);
  if (!Number.isSafeInteger(id) || id <= 0)
    return res.status(400).json({ errore: 'ID non valido' });
  const persona = persone.find(p => p.id === id);
  if (!persona) return res.status(404).json({ errore: 'Non trovata' });
  const ruolo = req.body?.ruolo;
  if (typeof ruolo !== 'string' || !ruolo.trim() || ruolo.trim().length > 80)
    return res.status(400).json({ errore: 'Ruolo non valido' });
  persona.ruolo = ruolo.trim();
  res.json(persona);
});

Come leggerlo e risultato atteso. Inviando {"ruolo":"editor"} modifichi il ruolo senza toccare nome e ID. Questa PATCH accetta il solo campo ruolo come modifica; un body senza ruolo non è valido. È una scelta del contratto, non un formato universale per tutte le PATCH.

Prova tu. Invia la stessa PUT due volte: lo stato finale deve restare uguale. Confronta poi una PUT senza ruolo nel laboratorio con la PATCH qui sopra.

Torna all’indice ↑
LEZIONE 27 · CRUD e architettura

API DELETE: eliminare

Prima di iniziare: le parole della lezione

findIndex e splice
findIndex trova la posizione di un elemento e restituisce -1 se manca. splice modifica l’array eliminando o inserendo elementi a partire da una posizione.
204 No Content
Risposta di successo senza body. Il client non deve tentare di leggerla come JSON.
Cancellazione logica
Alternativa alla rimozione fisica: si marca un record come eliminato e lo si esclude dalle normali letture, secondo le regole applicative.

DELETE rimuove una risorsa individuata normalmente da un ID. Evita di confondere l'ID pubblico con l'indice dell'array: possono essere diversi.

app.delete('/api/persone/:id', (req, res) => {
  const id = Number(req.params.id);
  const posizione = persone.findIndex(p => p.id === id);
  if (posizione === -1)
    return res.status(404).json({ errore: 'Non trovata' });
  persone.splice(posizione, 1);
  res.status(204).end();
});

Esempio aggiuntivo: dopo aver eliminato l'ID 1, gli altri ID non dovrebbero essere ricalcolati. 204 non ha body; 200 con una conferma JSON è un'alternativa.

Chi fa cosa, e perché

findIndex collega l’ID pubblico alla posizione corrente nell’array. Verifica sempre -1: chiamare splice(-1, 1) elimina l’ultimo elemento, anche se quello richiesto non esiste. Dopo la rimozione non ricalcolare gli ID dei sopravvissuti: i loro URL devono continuare a identificare le stesse persone.

Una seconda DELETE può ricevere 404 dopo che la prima ha ricevuto 204. Questo non contraddice l’idempotenza: in entrambi i casi lo stato finale desiderato è che la risorsa non esista. Alcune API scelgono 204 anche se il record è già assente: documenta la politica.

Un altro esempio guidato

// elimina.mjs — client Node per l'API del laboratorio attiva
const risposta = await fetch('http://localhost:3000/api/persone/1', {
  method: 'DELETE'
});
if (risposta.status === 204) {
  console.log('Eliminazione riuscita, nessun body da leggere');
} else {
  console.log('Status:', risposta.status, 'Risposta:', await risposta.text());
}

Come leggerlo e risultato atteso. Alla prima esecuzione ottieni normalmente 204. La seconda restituisce 404 e il messaggio dell’API, se il server non è stato riavviato. response.json() su una risposta 204 fallirebbe perché non c’è testo JSON da interpretare.

Prova tu. Elimina un ID intermedio e recupera gli altri. Verifica che un ID inesistente non causi la cancellazione accidentale dell’ultimo elemento.

Torna all’indice ↑
LEZIONE 28 · CRUD e architettura

Express Router e organizzazione

Prima di iniziare: le parole della lezione

Router e prefisso
Un Router è un insieme componibile di route e middleware. Il prefisso di montaggio collega quel gruppo a una parte dell’URL dell’app.
Controller, service e repository
Il controller gestisce ingresso e risposta HTTP; il service applica regole del dominio; il repository legge e scrive i dati. Sono ruoli organizzativi, non oggetti creati automaticamente da Express.
MVC
Model–View–Controller: separa modello e dati, presentazione e gestione dell’interazione. In un’API JSON la presentazione non è necessariamente una pagina HTML.

Con l'aumentare delle route conviene separarle dal file principale. express.Router() consente di raggruppare endpoint e middleware.

// routes/persone.cjs
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => res.json([{ id: 1, nome: 'Anna' }]));
router.get('/:id', (req, res) => res.json({ id: Number(req.params.id) }));
module.exports = router;
// server.cjs
const express = require('express');
const personeRouter = require('./routes/persone.cjs');
const app = express();
app.use('/api/persone', personeRouter);
app.listen(3000);

Esempio aggiuntivo: un futuro progetto può dividere codice in routes/, controllers/, services/ e repositories/. Il video finale accenna a sviluppi successivi (MVC e database), ma queste parti non sono spiegate nel materiale introduttivo fornito.

Chi fa cosa, e perché

app.use("/api/persone", personeRouter) affida al router le richieste sotto quel prefisso. All’interno scrivi router.get("/") e router.get("/:id"). Ripetere /api/persone dentro il router produrrebbe un percorso duplicato. Per una richiesta al router, req.originalUrl conserva l’URL originale, mentre Express gestisce il percorso relativo al montaggio.

La catena completa può essere: server → parser e middleware → router → controller → service → repository → risposta. Il router sceglie chi gestisce la richiesta; non è lui, da solo, a rendere persistenti i dati. Parti da pochi file con responsabilità chiare e separali quando questo rende il flusso più comprensibile.

Un altro esempio guidato

// controllers/salute.cjs
function leggiSalute(req, res) {
  res.json({ ok: true });
}
module.exports = { leggiSalute };

// routes/salute.cjs — secondo file
const router = require('express').Router();
const { leggiSalute } = require('../controllers/salute.cjs');
router.get('/', leggiSalute);
module.exports = router;

// server.cjs — aggiunta prima di listen e della 404
// app.use('/api/salute', require('./routes/salute.cjs'));

Come leggerlo e risultato atteso. Crea le due cartelle e i due file separati. Nel server già configurato aggiungi la riga app.use togliendo //. GET /api/salute viene selezionata dal router e la risposta è costruita dal controller. Non chiamare leggiSalute() mentre registri la route: passa il riferimento alla funzione.

Prova tu. Sposta le route CRUD nel router persone e controlla che gli URL pubblici restino identici. Poi indica in quale file metteresti una regola “nome non vuoto” e in quale una query di lettura.

Torna all’indice ↑
LABORATORIO FINALE

Progetto completo — API persone con Express

Questo esercizio unisce Express, JSON, parametri della route, query string, middleware, GET, POST, PUT, PATCH, DELETE e gestione errori. È un'estensione didattica sviluppata per questa guida: il corso originale affronta questi pezzi in lezioni separate. L'archivio è volutamente in memoria.

1. Preparazione

mkdir api-persone
cd api-persone
npm init -y
npm install express@5

Crea un file server.cjs con il contenuto seguente.

2. Server completo (un solo file)

const express = require('express');
const app = express();
const PORT = 3000;

let persone = [
  { id: 1, nome: 'Anna', ruolo: 'designer' },
  { id: 2, nome: 'Luca', ruolo: 'developer' }
];
let prossimoId = 3;

// Middleware: registra metodo, percorso e tempo impiegato.
app.use((req, res, next) => {
  const inizio = Date.now();
  res.on('finish', () => {
    console.log(req.method, req.originalUrl, res.statusCode,
      `${Date.now() - inizio}ms`);
  });
  next();
});

// Il parser viene dopo il logger e prima delle route.
app.use(express.json({ limit: '32kb' }));

function nomeValido(nome) {
  return typeof nome === 'string' &&
    nome.trim().length >= 2 && nome.trim().length <= 80;
}
function idRichiesto(req, res) {
  const testo = req.params.id;
  const id = Number(testo);
  if (!/^[1-9]\d*$/.test(testo) || !Number.isSafeInteger(id)) {
    res.status(400).json({ errore: 'ID non valido' });
    return null;
  }
  return id;
}

// GET /api/persone?cerca=an&limite=10
app.get('/api/persone', (req, res) => {
  const { cerca = '', limite: testoLimite = '20' } = req.query;
  if (typeof cerca !== 'string' || typeof testoLimite !== 'string' ||
      !/^[1-9]\d*$/.test(testoLimite)) {
    return res.status(400).json({ errore: 'Query non valida' });
  }
  const limite = Number(testoLimite);
  if (!Number.isSafeInteger(limite) || limite > 100)
    return res.status(400).json({ errore: 'Limite da 1 a 100' });
  const risultati = persone.filter(p =>
    p.nome.toLowerCase().includes(cerca.toLowerCase())
  ).slice(0, limite);
  res.json({ totale: risultati.length, dati: risultati });
});

// GET /api/persone/:id
app.get('/api/persone/:id', (req, res) => {
  const id = idRichiesto(req, res);
  if (id === null) return;
  const persona = persone.find(p => p.id === id);
  if (!persona) return res.status(404).json({ errore: 'Non trovata' });
  res.json(persona);
});

// POST /api/persone
app.post('/api/persone', (req, res) => {
  const { nome, ruolo = 'utente' } = req.body ?? {};
  if (!nomeValido(nome) || typeof ruolo !== 'string' || !ruolo.trim() || ruolo.trim().length > 80)
    return res.status(400).json({ errore: 'Dati non validi' });
  const persona = { id: prossimoId++, nome: nome.trim(), ruolo: ruolo.trim() };
  persone.push(persona);
  res.status(201).location(`/api/persone/${persona.id}`).json(persona);
});

// PUT /api/persone/:id
app.put('/api/persone/:id', (req, res) => {
  const id = idRichiesto(req, res);
  if (id === null) return;
  const persona = persone.find(p => p.id === id);
  if (!persona) return res.status(404).json({ errore: 'Non trovata' });
  const { nome, ruolo } = req.body ?? {};
  if (!nomeValido(nome) || typeof ruolo !== 'string' || !ruolo.trim() || ruolo.trim().length > 80)
    return res.status(400).json({ errore: 'Dati non validi' });
  persona.nome = nome.trim();
  persona.ruolo = ruolo.trim();
  res.json(persona);
});

// PATCH /api/persone/:id — modifica soltanto il ruolo
app.patch('/api/persone/:id', (req, res) => {
  const id = idRichiesto(req, res);
  if (id === null) return;
  const persona = persone.find(p => p.id === id);
  if (!persona) return res.status(404).json({ errore: 'Non trovata' });
  const ruolo = req.body?.ruolo;
  if (typeof ruolo !== 'string' || !ruolo.trim() || ruolo.trim().length > 80)
    return res.status(400).json({ errore: 'Ruolo non valido' });
  persona.ruolo = ruolo.trim();
  res.json(persona);
});

// DELETE /api/persone/:id
app.delete('/api/persone/:id', (req, res) => {
  const id = idRichiesto(req, res);
  if (id === null) return;
  const prima = persone.length;
  persone = persone.filter(p => p.id !== id);
  if (persone.length === prima)
    return res.status(404).json({ errore: 'Non trovata' });
  res.status(204).end();
});

app.use((req, res) => res.status(404).json({ errore: 'Endpoint non trovato' }));
app.use((err, req, res, next) => {
  if (res.headersSent) return next(err);
  if (err.type === 'entity.parse.failed')
    return res.status(400).json({ errore: 'JSON malformato' });
  if (err.type === 'entity.too.large')
    return res.status(413).json({ errore: 'Body oltre 32kb' });
  if (Number.isInteger(err.status) && err.status >= 400 && err.status < 500)
    return res.status(err.status).json({ errore: 'Richiesta non valida' });
  console.error(err);
  res.status(500).json({ errore: 'Errore interno' });
});
app.listen(PORT, '127.0.0.1', () => console.log(`API: http://localhost:${PORT}`));

3. Provalo con il terminale

I comandi curl multilinea seguenti usano la sintassi Bash. Per PowerShell trovi subito dopo una sequenza equivalente. Apri una cartella dedicata all’esercizio e tieni attivo un solo server sulla porta 3000. Per chiuderlo usa Ctrl+C nel suo terminale.

node server.cjs

In un secondo terminale:

curl -i http://localhost:3000/api/persone
curl -i 'http://localhost:3000/api/persone?cerca=an&limite=1'
curl -i http://localhost:3000/api/persone/1
curl -i -X POST http://localhost:3000/api/persone \
  -H 'Content-Type: application/json' \
  -d '{"nome":"Giulia","ruolo":"analista"}'
curl -i -X PUT http://localhost:3000/api/persone/3 \
  -H 'Content-Type: application/json' \
  -d '{"nome":"Giulia","ruolo":"senior analyst"}'
curl -i -X DELETE http://localhost:3000/api/persone/3

La stessa sequenza in PowerShell

# Secondo terminale PowerShell; il server deve essere già attivo
$base = 'http://127.0.0.1:3000/api/persone'
Invoke-RestMethod -Uri $base
$json = @{ nome = 'Giulia'; ruolo = 'analista' } | ConvertTo-Json
$persona = Invoke-RestMethod -Method Post -Uri $base -ContentType 'application/json' -Body $json
$uri = "$base/$($persona.id)"
Invoke-RestMethod -Uri $uri
$json = @{ nome = 'Giulia Rossi'; ruolo = 'analista senior' } | ConvertTo-Json
Invoke-RestMethod -Method Put -Uri $uri -ContentType 'application/json' -Body $json
$json = @{ ruolo = 'responsabile' } | ConvertTo-Json
Invoke-RestMethod -Method Patch -Uri $uri -ContentType 'application/json' -Body $json
Invoke-RestMethod -Method Delete -Uri $uri
Invoke-RestMethod -Uri $base

La DELETE riuscita non produce contenuto. L’ID viene ricavato dalla POST: la sequenza funziona anche se hai già creato altre persone. In Windows PowerShell gli errori HTTP di Invoke-RestMethod possono essere mostrati come eccezioni, mentre Postman rende status e body direttamente ispezionabili.

4. Risultati e limiti

Richiesta Risultato atteso
GET elenco 200, oggetto con totale e dati
GET ID inesistente 404
POST valido 201, nuova persona con ID
POST con nome vuoto 400
PUT valido 200, persona aggiornata
DELETE valido 204, nessun corpo

Non è una API pronta per la produzione: mancano database persistente, autenticazione/autorizzazione, rate limiting, test automatici, logging strutturato e validazione completa degli input. L'obiettivo è esercitarsi sui concetti del corso.

5. Il viaggio di una richiesta, passo per passo

Client→ Logger→ Parser JSON→ Route→ Validazione→ Array in RAM→ Risposta

Per una POST, il client invia un testo JSON. Il logger registra una funzione sull’evento finish della risposta, poi chiama next. Il parser interpreta il testo. La route legge i campi ammessi, li valida e costruisce una nuova persona; push aggiorna l’array. Infine status, Location e json preparano e inviano la risposta. A quel punto il listener finish registra status e durata: questa misura la conclusione dell’invio dal server, non dimostra che l’utente abbia già letto il contenuto.

ParteResponsabilitàCosa controllare
nomeValidoVerifica nome testuale di 2–80 caratteri dopo trimNon accetta numeri o soli spazi
idRichiestoLegge e valida l’ID numerico nel percorsoRestituisce null dopo una risposta 400
GET elencoFiltra e limita i risultatiQuery ripetute o limite fuori da 1–100 danno 400
POSTCrea e assegna l’identitàNome richiesto; ruolo predefinito utente se assente
PUTSostituisce i campi modificabiliNome e ruolo obbligatori
PATCHModifica il solo ruoloNome resta invariato; gli altri campi sono ignorati
DELETERimuove la persona mantenendo gli altri ID204 senza body, oppure 404
Error handlerTraduce errori del parser e del server400 per JSON malformato; 413 per body troppo grande; 500 per errore interno

totale è qui il numero di risultati restituiti dopo il limite, non il numero di tutte le corrispondenze possibili. Non sono implementati offset o cursori di paginazione. res.headersSent indica che gli header sono già partiti: in quel caso il gestore passa l’errore alla gestione successiva, evitando una seconda risposta. Per approfondire: gestione errori Express e errori dei parser.

Controlli negativi con risultati attesi

  • GET /api/persone/abc → 400; GET /api/persone/999 → 404.
  • GET /api/persone?limite=0 o ?limite=2&limite=3 → 400.
  • POST con {"nome":" "} → 400 e nessun inserimento.
  • POST con JSON troncato, per esempio {"nome": → 400, non 500.
  • PUT senza ruolo → 400; PATCH con ruolo valido → 200 senza cambiare nome.
  • DELETE ripetuta sullo stesso ID → prima 204, poi 404.
  • Riavvio del server → dati iniziali ripristinati: l’array non è persistente.

Il contatore e l’array sono condivisi soltanto nel singolo processo. Per dati durevoli e più istanze serve un archivio coordinato. Gli esempi non implementano utenti autenticati: il campo ruolo è un semplice dato descrittivo della persona e non conferisce permessi all’applicazione.

CONSOLIDAMENTO

Ripasso e percorso di studio

Come affrontare il materiale

  1. Fondamenti (01–04): esegui script, distingui Node dal browser, conosci process.
  2. Moduli e I/O (05–07): importa moduli e confronta letture sincrone/asincrone.
  3. HTTP e runtime (08, 12–15): crea un server, osserva event loop, eventi e stream.
  4. Progetto e tooling (09–11): usa npm, package.json e riavvio automatico.
  5. Express e API (16–23): crea route, JSON, filtri, middleware e testa le richieste.
  6. CRUD (24–28): completa il progetto finale, poi separa le route.

Errori frequenti da evitare

  • Scambiare req.params.id (una stringa) con un numero senza conversione e validazione.
  • Credere che async renda non bloccante un calcolo CPU impegnativo.
  • Restituire 200 quando la risorsa non esiste invece di 404.
  • Dimenticare express.json() prima delle route che leggono il body JSON.
  • Pensare che require e import siano intercambiabili senza configurazione.
  • Usare percorsi di file relativi senza sapere da quale cartella si avvia il processo.
  • Trattare i dati in RAM come se fossero persistenti.

Cinque esercizi progressivi

  1. Base: modifica il primo script affinché stampi pari o dispari per i numeri da 1 a 20.
  2. Moduli: crea calcoli.cjs con somma() e media(), importale in main.cjs.
  3. File: salva un array di appunti in appunti.json, poi rileggilo tramite node:fs/promises.
  4. HTTP: aggiungi /ora al server HTTP e restituisci un timestamp ISO in JSON.
  5. CRUD: aggiungi all’API completa una ricerca per ruolo ed estendi la PATCH perché accetti anche il nome, validando almeno un campo modificabile.

Domande di ripasso con risposta

Chi esegue il codice della route?
Node esegue la funzione JavaScript che Express ha selezionato in base a metodo, percorso e ordine dei middleware.
Perché req.body può essere assente?
Il parser può mancare, essere registrato troppo tardi, non riconoscere il Content-Type o non avere un body da interpretare.
Perché una POST scompare dopo un salvataggio?
Se il watcher riavvia il processo, l’array in RAM viene ricreato. La persistenza richiede un salvataggio esplicito.
Un await rende il calcolo parallelo?
No: permette di attendere una Promise senza bloccare la funzione chiamante nello stesso modo di una chiamata sincrona, ma un calcolo pesante sul thread principale resta bloccante.
Qual è la differenza fra 400 e 404?
400 descrive input non valido; 404 una risorsa o route non trovata, secondo il contratto dell’API.

Cosa studiare dopo

Il corso introduttivo termina sostanzialmente con Express Router. Per sviluppare backend reali approfondisci database e SQL, autenticazione, autorizzazione, convalida degli input, test, sicurezza delle API, gestione errori, TypeScript e deploy. Il filmato conclusivo del testo fornito presenta un corso separato a pagamento: non è una lezione tecnica da riassumere come parte delle 28.

CONSULTAZIONE

Materiale di partenza e pratiche attuali

Il manuale rielabora il testo incollato e i 28 documenti delle lezioni presenti nella cartella del corso, correggendo i termini deformati dalla trascrizione automatica. Le spiegazioni guidate e gli esempi supplementari sono integrazioni didattiche. Il video 29 è una presentazione commerciale: non aggiunge una ventinovesima lezione tecnica.

Nel materialeCome leggerlo oggi
Node 14/16Usa una linea LTS ancora supportata; verifica lo stato nella tabella ufficiale.
require e module.exportsCommonJS è ancora supportato. ESM offre import/export standard; scegli il formato in modo esplicito.
NodemonResta valido; per il riavvio di base esiste anche il watch integrato in Node.
Callback di fsRestano valide; Promise e await semplificano molti flussi asincroni.
Vecchie route ExpressGli esempi nuovi si riferiscono a Express 5; controlla le differenze prima di copiare percorsi da Express 4.
Array importato = copiaL’importazione non copia automaticamente l’array e modificare l’oggetto non riscrive il file sorgente.
Statico = SSRServire HTML già scritto è statico; SSR genera la vista lato server usando dati.
Risposta 200 per ogni esitoUsa gli status secondo il risultato: creazione 201, assenza 404, input errato 400.

La documentazione online può evolvere. Il testo del manuale è consultabile offline; i collegamenti richiedono una connessione. Per i dettagli normativi fai riferimento alle pagine ufficiali collegate vicino alle relative spiegazioni.

Materiali didattici curati e rielaborati da Federico Lelli. Le fonti e i riferimenti specifici sono indicati nei materiali disponibili.