Come creare un server web in Node.js con il modulo HTTP
Introduzione
Quando visualizzi una pagina web nel tuo browser, stai effettuando una richiesta a un altro computer su Internet, che poi ti fornisce la pagina web come risposta. Quel computer con cui stai comunicando tramite Internet è un server web. Un server web riceve richieste HTTP da un client, come il tuo browser, e fornisce una risposta HTTP, come una pagina HTML o un JSON da un'API.
Il modulo http di Node.js fornisce tutto il necessario per costruire un server web senza dipendenze esterne. Gestisce le richieste in arrivo, analizza gli header HTTP, gestisce le connessioni e invia le risposte—tutto tramite semplici API JavaScript.
Il modulo http è la soluzione integrata di Node.js per creare server e client HTTP. A differenza di framework come Express.js, fa parte del core di Node.js, il che significa che non sono necessarie installazioni o dipendenze. Questo lo rende ideale per applicazioni leggere, per imparare le basi di HTTP o quando si desidera avere il pieno controllo sulla gestione delle richieste.
Per far sì che un server restituisca una pagina web è coinvolto molto software. Questo software generalmente rientra in due categorie: frontend e backend. Il codice frontend si occupa di come il contenuto viene presentato, come il colore di una barra di navigazione e lo stile del testo. Il codice backend si occupa di come i dati vengono scambiati, elaborati e memorizzati. Il codice che gestisce le richieste di rete dal tuo browser o comunica con il database è principalmente gestito dal codice backend.
Node.js permette agli sviluppatori di usare JavaScript per scrivere codice back-end, anche se tradizionalmente veniva usato nel browser per scrivere codice front-end. Avere frontend e backend insieme in questo modo riduce lo sforzo necessario per creare un server web, ed è una delle principali ragioni per cui Node.js è una scelta popolare per scrivere codice back-end.
Questo tutorial mostra come creare server web pronti per la produzione utilizzando il modulo http incluso in Node.js. Costruirai server che gestiscono il routing, analizzano i body delle richieste, servono file statici, implementano una gestione corretta degli errori e funzionano sia con la sintassi CommonJS che con i moduli ES.
Semplifica il deployment di applicazioni Node.js con la piattaforma App di Linux-Console.net. Distribuisci Node direttamente da GitHub in pochi minuti.
Punti Chiave
Prima di iniziare, ecco cosa riuscirai a realizzare:
Crea server HTTP utilizzando il modulo core
httpdi Node.js senza framework:
Imparerai come avviare un server web HTTP completamente funzionante usando solo il modulo corehttpdi Node.js. Questo significa evitare dipendenze esterne o framework, dandoti una comprensione più profonda di come funzionano realmente i server a basso livello, oltre a permettere implementazioni più leggere e flessibili per i tuoi casi d'uso.Gestisci diversi tipi di contenuto (JSON, HTML, CSV, file statici) con intestazioni adeguate:
Scoprirai come lavorare con più tipi di contenuto, rispondendo con HTML per le pagine web, JSON per le API, file CSV o servendo risorse statiche come immagini e script. Capirai come impostare correttamente le intestazioni HTTP comeContent-Type, assicurando che browser e client interpretino correttamente le tue risposte per esperienze utente fluide.Implementa il routing usando l'analisi degli URL delle richieste e le istruzioni switch:
Il routing è fondamentale per i server web, permettendo di fornire logiche diverse per URL e endpoint differenti. Vedrai tecniche per analizzare gli URL delle richieste in arrivo e strutturare la logica del server (ad esempio, con istruzioni switch) per fornire risposte personalizzate in base ai percorsi e ai metodi HTTP, simulando le basi di ciò che fanno dietro le quinte i framework più grandi.Elabora le richieste POST analizzando i corpi delle richieste e gestendo diversi tipi di contenuto:
Gestire i dati inviati dagli utenti è fondamentale per qualsiasi applicazione interattiva. Imparerai a gestire le richieste POST, inclusa la lettura e l'analisi del corpo della richiesta come dati grezzi, JSON o altri formati. Questa sezione copre il buffering dei dati in arrivo, la loro analisi sicura e la risposta in base al payload ricevuto, fornendo le basi per l'interazione con l'utente o lo sviluppo di API.Servire file statici in modo efficiente dal filesystem:
Molti server web necessitano di distribuire risorse statiche—immagini, fogli di stile, file JavaScript—direttamente agli utenti. Scoprirai come leggere i file dal disco e inviarli in modo efficiente tramite HTTP, gestendo correttamente i percorsi dei file per evitare problemi di sicurezza, ed esplorando ottimizzazioni come lo streaming dei file per migliorare le prestazioni con file di dimensioni maggiori.Implementare la gestione degli errori con codici di stato HTTP appropriati e logging:
Un server robusto deve gestire gli errori in modo efficace. Vedrai come rilevare, registrare e rispondere agli errori—come richieste errate o file mancanti—utilizzando codici di stato HTTP standardizzati come 400, 404 e 500. Imparerai anche le pratiche di base per il logging, in modo che i problemi siano tracciabili e gli utenti ricevano messaggi di errore informativi senza divulgare informazioni sensibili.-
Usa JavaScript moderno con entrambe le sintassi CommonJS (
require) e ES Modules (import):
Node.js supporta due sistemi di moduli: CommonJS e ES Modules. Praticherai l'uso di entrambe le istruzionirequire()eimport, comprendendo quando e come usare ciascuno stile. Questo non solo garantisce la compatibilità tra diversi progetti Node.js, ma ti aiuta anche a scrivere codice JavaScript moderno, pulito e manutenibile. Applicare le migliori pratiche di produzione, inclusi caching dei file, gestione degli errori e considerazioni sulla sicurezza:
Infine, toccherai le pratiche fondamentali per il deployment di server web Node.js in ambienti reali. Questo include sfruttare il caching dei file per le prestazioni, implementare strategie di recupero dagli errori per mantenere il server resiliente, oltre ad applicare misure di sicurezza di base (come la sanitizzazione degli input e intestazioni HTTP appropriate) per proteggere le tue applicazioni e gli utenti.
Comprendere il modulo HTTP
Il modulo http è l'interfaccia di basso livello di Node.js per la comunicazione HTTP. Fornisce:
- Creazione del server:
http.createServer()crea un'istanza di server HTTP - Gestione delle richieste: Le richieste in arrivo sono rappresentate da oggetti
IncomingMessage - Gestione delle risposte: Le risposte in uscita utilizzano oggetti
ServerResponse - Supporto allo streaming: Sia le richieste che le risposte sono in streaming, permettendo un trasferimento dati efficiente
Perché usare il modulo HTTP invece di Express? Mentre Express.js offre astrazioni comode, il modulo nativo http ti dà il pieno controllo, dimensioni del bundle più ridotte e una comprensione approfondita dei fondamenti di HTTP. È la base su cui si costruiscono i framework. Per API semplici, microservizi o scopi didattici, spesso è tutto ciò di cui hai bisogno.
Il modulo è basato sugli eventi: quando arriva una richiesta, il server emette un evento 'request' e chiama la tua funzione listener per le richieste. Questo design consente a Node.js di gestire in modo efficiente molte connessioni contemporanee su un singolo thread attraverso il suo event loop.
Concetti fondamentali da comprendere:
- I/O non bloccante: Il
httpmodulo utilizza il loop degli eventi di Node.js per gestire più connessioni senza creare thread separati per ogni richiesta. Questo lo rende molto efficiente per operazioni vincolate all'I/O. - Stream: Gli oggetti di richiesta e risposta ereditano dagli stream di Node.js. Questo significa che grandi payload possono essere elaborati a pezzi senza caricare tutto in memoria, cosa fondamentale per gestire caricamenti di file o servire file di grandi dimensioni.
- Intestazioni e codici di stato: Il modulo ti offre il controllo diretto sulle intestazioni HTTP (
res.setHeader()) e sui codici di stato (res.writeHead()), permettendo una gestione precisa della semantica HTTP. - Nessun routing automatico: A differenza dei framework, non c'è un routing integrato. Devi analizzare manualmente
req.urlereq.methodper determinare come rispondere, offrendo piena trasparenza su come funziona realmente il routing.
Quando si chiama http.createServer(), Node.js crea dietro le quinte un server TCP che ascolta le connessioni in arrivo, analizza i messaggi del protocollo HTTP ed emette eventi che il tuo codice può gestire. Comprendere questa operazione a basso livello ti aiuta a risolvere problemi e a prendere decisioni architettoniche informate.
Prerequisiti
Assicurati che Node.js sia installato sulla tua macchina di sviluppo. Questo tutorial funziona con Node.js 20.x o 22.x LTS (consigliato). Per installare Node.js su macOS o Ubuntu, segui i passaggi in Come Installare Node.js e Creare un Ambiente di Sviluppo Locale su macOS o Come Installare Node.js su Ubuntu.
La piattaforma Node.js supporta la creazione di server web fin da subito. Avere familiarità con le basi di Node.js è utile. Consulta la nostra guida su Come Scrivere ed Eseguire il Tuo Primo Programma in Node.js.
La programmazione asincrona viene utilizzata in alcune sezioni. Se sei nuovo al codice asincrono o al modulo
fs, consulta Come scrivere codice asincrono in Node.js.-
Per le sezioni focalizzate sulla produzione, avere familiarità con la gestione dei processi Linux (systemd/PM2) e con i reverse proxy (come Nginx) sarà utile quando si applicano le indicazioni per il deployment.
Passo 1 — Creare un server HTTP di base
Crea un server HTTP minimale che risponda a qualsiasi richiesta con testo semplice. Questa base si applica sia che tu usi Express sia che costruisca tutto da zero. Quando Node.js chiama http.createServer(), crea un server TCP che ascolta su una porta. Ogni connessione in arrivo attiva la tua funzione listener per le richieste.
Configura la cartella del tuo progetto:
mkdir first-servers
cd first-serversOra, crea il file che conterrà il codice:
touch hello.jsApri il file in un editor di testo. Useremo nano poiché è disponibile nel terminale:
nano hello.jsIniziamo caricando il modulo http, che è standard in tutte le installazioni di Node.js. Aggiungi la seguente riga a hello.js:
const http = require("http");Il modulo http contiene la funzione per creare il server, che vedremo più avanti. Se desideri saperne di più sui moduli in Node.js, dai un'occhiata al nostro articolo Come Creare un Modulo Node.js.
Il nostro prossimo passo sarà definire due costanti, l'host e la porta a cui il nostro server sarà collegato:
...
const host = 'localhost';
const port = 8000;Come accennato in precedenza, i server web accettano richieste dai browser e da altri client. Possiamo interagire con un server web inserendo un nome di dominio, che viene tradotto in un indirizzo IP da un server DNS. Un indirizzo IP è una sequenza unica di numeri che identifica una macchina su una rete, come Internet. Per ulteriori informazioni sui concetti di nomi di dominio, consulta il nostro articolo Introduzione alla terminologia, componenti e concetti del DNS.
Il valore localhost è un indirizzo privato speciale che i computer usano per riferirsi a se stessi. Di solito corrisponde all'indirizzo IP interno 127.0.0.1 ed è disponibile solo per il computer locale, non per eventuali reti locali a cui siamo connessi né per Internet.
La porta è un numero che i server utilizzano come punto finale o "porta" per il nostro indirizzo IP. Nel nostro esempio, useremo la porta 8000 per il nostro server web. Le porte 8080 e 8000 sono tipicamente usate come porte predefinite durante lo sviluppo e, nella maggior parte dei casi, gli sviluppatori le utilizzeranno piuttosto che altre porte per i server HTTP.
Quando colleghiamo il nostro server a questo host e porta, potremo accedere al nostro server visitando http://localhost:8000 in un browser locale.
Aggiungiamo una funzione speciale, che in Node.js chiamiamo request listener. Questa funzione serve a gestire una richiesta HTTP in arrivo e a restituire una risposta HTTP. Questa funzione deve avere due argomenti, un oggetto request e un oggetto response. L'oggetto request cattura tutti i dati della richiesta HTTP in arrivo. L'oggetto response viene usato per restituire risposte HTTP dal server.
Vogliamo che il nostro primo server restituisca questo messaggio ogni volta che qualcuno vi accede: "Il mio primo server!".
Aggiungiamo subito quella funzione:
...
const requestListener = function (req, res) {
res.writeHead(200);
res.end("My first server!");
};La funzione di solito viene nominata in base a ciò che fa. Ad esempio, se creassimo una funzione listener per le richieste per restituire una lista di libri, probabilmente la chiameremmo listBooks(). Poiché questo è un caso di esempio, useremo il nome generico requestListener.
Tutte le funzioni listener di richieste in Node.js accettano due argomenti: req e res (possiamo dare loro nomi diversi se vogliamo). La richiesta HTTP inviata dall'utente viene catturata in un oggetto Request, che corrisponde al primo argomento, req. La risposta HTTP che restituiamo all'utente si forma interagendo con l'oggetto Response nel secondo argomento, res.
La prima riga res.writeHead(200); imposta il codice di stato HTTP della risposta. I codici di stato HTTP indicano quanto bene una richiesta HTTP è stata gestita dal server. In questo caso, il codice di stato 200 corrisponde a "OK". Se sei interessato a conoscere i vari codici HTTP che i tuoi server web possono restituire e il significato che indicano, la nostra guida su Come Risolvere i Comuni Codici di Errore HTTP è un buon punto di partenza.
La riga successiva della funzione, res.end("Il mio primo server!");, invia la risposta HTTP al client che l'ha richiesta. Questa funzione restituisce qualsiasi dato che il server deve ritornare. In questo caso, sta restituendo dati di testo.
Finalmente, ora possiamo creare il nostro server e utilizzare il nostro listener di richieste:
...
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});Salva ed esci da nano premendo CTRL+X.
Nella prima riga, creiamo un nuovo oggetto server tramite la funzione createServer() del modulo http. Questo server accetta richieste HTTP e le passa alla nostra funzione requestListener().
Dopo aver creato il nostro server, dobbiamo associarlo a un indirizzo di rete. Lo facciamo con il metodo server.listen(). Accetta tre argomenti: port, host e una funzione di callback che viene eseguita quando il server inizia ad ascoltare.
Tutti questi argomenti sono opzionali, ma è una buona idea indicare esplicitamente quale porta e quale host vogliamo che un server web utilizzi. Quando si distribuiscono server web in ambienti diversi, conoscere la porta e l'host su cui stanno girando è necessario per configurare il bilanciamento del carico o un alias DNS.
La funzione di callback registra un messaggio nella nostra console così possiamo sapere quando il server ha iniziato ad ascoltare le connessioni.
Ecco il file completo hello.js con commenti in linea che spiegano ogni parte:
// Import the built-in http module - no installation needed
const http = require("http");
// Define where the server will listen
const host = 'localhost'; // Only accessible from this machine
const port = 8000; // Common development port
// Request listener: handles every incoming HTTP request
const requestListener = function (req, res) {
// Set HTTP status code to 200 (OK)
res.writeHead(200);
// Send the response body and close the connection
res.end("My first server!");
};
// Create the server with our request listener
const server = http.createServer(requestListener);
// Start listening for connections
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});Questo codice crea un server HTTP funzionante in meno di 20 righe. Ogni volta che un client si connette—indipendentemente dall'URL o dal metodo HTTP—il server risponde con "Il mio primo server!" e un codice di stato 200.
Nota: Anche se requestListener() non utilizza l'oggetto req, deve comunque essere il primo argomento della funzione.
Con meno di quindici righe di codice, ora abbiamo un server web. Vediamo come funziona e testiamolo dall'inizio alla fine eseguendo il programma:
node hello.jsNella console, vedremo questo output:
Server is running on http://localhost:8000Nota che il prompt scompare. Questo accade perché un server Node.js è un processo a lungo termine. Esce solo se incontra un errore che lo fa crashare e terminare, oppure se interrompiamo il processo Node.js che esegue il server.
In una finestra del terminale separata, comunicheremo con il server usando cURL, uno strumento da riga di comando per trasferire dati da e verso una rete. Inserisci il comando per effettuare una richiesta HTTP GET al nostro server in esecuzione:
curl http://localhost:8000Quando premiamo INVIO, il nostro terminale mostrerà il seguente output:
My first server!Abbiamo ora configurato un server e ricevuto la nostra prima risposta dal server.
Analizziamo cosa è successo quando abbiamo testato il nostro server. Usando cURL, abbiamo inviato una richiesta GET al server all'indirizzo http://localhost:8000. Il nostro server Node.js ha ascoltato le connessioni da quell'indirizzo. Il server ha passato quella richiesta alla funzione requestListener(). La funzione ha restituito dati di testo con il codice di stato 200. Il server ha poi inviato quella risposta a cURL, che ha mostrato il messaggio nel nostro terminale.
In produzione, di solito non ci si lega a localhost—si lega a 0.0.0.0 per accettare connessioni da qualsiasi interfaccia di rete. Tuttavia, per lo sviluppo locale, localhost è più sicuro e impedisce l'accesso esterno al tuo server di sviluppo. Inoltre, se la porta 8000 è già in uso, riceverai un errore EADDRINUSE—basta scegliere un'altra porta o fermare il processo che usa quella porta con lsof -i :8000 (macOS/Linux).
Prima di continuare, usciamo dal nostro server in esecuzione premendo CTRL+C. Questo interrompe l'esecuzione del server e ci riporta al prompt dei comandi.
Nella maggior parte dei siti web che visitiamo o delle API che utilizziamo, le risposte del server raramente sono in testo semplice. Riceviamo pagine HTML e dati JSON come formati di risposta comuni. Nel passo successivo, impareremo come restituire risposte HTTP nei formati di dati comuni che incontriamo sul web.
Se vuoi imparare a fare richieste HTTP da Node.js (il lato client), dai un'occhiata alla nostra guida su Come Creare un Client HTTP con il Core HTTP in Node.js.
Passo 2 — Restituire diversi tipi di contenuto
La risposta che restituiamo da un server web può assumere diversi formati. In precedenza sono stati menzionati JSON e HTML, e possiamo anche restituire altri formati di testo come XML e CSV. Infine, i server web possono restituire dati non testuali come PDF, file compressi, audio e video.
In questo articolo, oltre al testo semplice che abbiamo appena restituito, imparerai come restituire i seguenti tipi di dati:
- JSON
- CSV
- HTML
I tre tipi di dati sono tutti basati su testo e sono formati popolari per la distribuzione di contenuti sul web. Molti linguaggi e strumenti di sviluppo lato server supportano il ritorno di questi diversi tipi di dati. Nel contesto di Node.js, dobbiamo fare due cose:
- Imposta l'intestazione
Content-Typenelle nostre risposte HTTP con il valore appropriato. - Assicurati che
res.end()riceva i dati nel formato corretto.
Vediamo questo in pratica con alcuni esempi. Il codice che scriveremo in questa sezione e in quelle successive ha molte somiglianze con il codice che abbiamo scritto in precedenza. La maggior parte delle modifiche riguarda la funzione requestListener(). Creiamo dei file con questo “codice modello” per rendere più semplice seguire le sezioni future.
Crea un nuovo file chiamato html.js. Questo file sarà utilizzato più avanti per restituire testo HTML in una risposta HTTP. Metteremo qui il codice del template e lo copieremo sugli altri server che restituiscono vari tipi.
Nel terminale, inserisci quanto segue:
touch html.jsOra apri questo file in un editor di testo:
nano html.jsCopiamo il “codice modello.” Inseriscilo in nano:
const http = require("http");
const host = 'localhost';
const port = 8000;
const requestListener = function (req, res) {};
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});Salva ed esci da html.js con CTRL+X, poi torna al terminale.
Ora copiamo questo file in due nuovi file. Il primo file servirà a restituire i dati CSV nella risposta HTTP:
cp html.js csv.jsIl secondo file restituirà una risposta JSON sul server:
cp html.js json.jsI file rimanenti saranno per esercizi successivi:
cp html.js htmlFile.js
cp html.js routes.jsOra siamo pronti per continuare i nostri esercizi. Iniziamo restituendo JSON.
Servendo JSON
JavaScript Object Notation, comunemente noto come JSON, è un formato di scambio dati basato su testo. Come suggerisce il nome, deriva dagli oggetti JavaScript, ma è indipendente dal linguaggio, il che significa che può essere utilizzato da qualsiasi linguaggio di programmazione in grado di interpretarne la sintassi.
JSON è comunemente utilizzato dalle API per ricevere e restituire dati. La sua popolarità è dovuta alla dimensione inferiore dei trasferimenti di dati rispetto a standard di scambio dati precedenti come XML, oltre agli strumenti esistenti che permettono ai programmi di analizzarli senza sforzi eccessivi. Se vuoi saperne di più su JSON, puoi leggere la nostra guida su Come Lavorare con JSON in JavaScript.
Apri il file json.js con nano:
nano json.jsQuando si fornisce JSON, è necessario impostare l'intestazione Content-Type su application/json. Senza di essa, i browser e i client API trattano la risposta come testo semplice, causando il fallimento di fetch().json(). Imposta le intestazioni prima di chiamare writeHead()—una volta inviate, le intestazioni non possono essere modificate.
Errore comune: Chiamare res.writeHead(200) prima di res.setHeader() blocca gli header. Se provi a impostare gli header dopo writeHead(), vengono ignorati. Chiama sempre prima setHeader() e poi writeHead().
Modifica la funzione requestListener():
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(`{"message": "This is a JSON response"}`);
};
...Il codice di stato 200 indica successo. Il corpo della risposta contiene JSON valido.
Perché usare JSON.stringify() invece delle stringhe scritte a mano? Scrivere JSON manualmente è soggetto a errori. Se il tuo messaggio contiene virgolette, ritorni a capo o caratteri speciali, devi sfugarli manualmente. JSON.stringify() gestisce tutto questo automaticamente:
const data = {
message: "This is a JSON response",
timestamp: Date.now(),
note: 'He said "hello"' // Quotes automatically escaped
};
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(JSON.stringify(data));Attenzione ai riferimenti circolari: Se il tuo oggetto fa riferimento a se stesso, JSON.stringify() genera: TypeError: Converting circular structure to JSON. Questo accade spesso con i risultati delle query del database. Verifica sempre la struttura dei tuoi dati.
Cosa succede se provi a inviare un oggetto direttamente? Il modulo http accetta solo stringhe o Buffer. Se provi res.end({ message: "test" }), Node.js genera un'eccezione: TypeError [ERR_INVALID_ARG_TYPE]: Il primo argomento deve essere di tipo string o Buffer. Devi prima convertire gli oggetti in stringhe JSON.
Verifica che l'intestazione sia stata impostata correttamente: Usa il flag -i con curl per vedere le intestazioni della risposta:
curl -i http://localhost:8000Dovresti vedere Content-Type: application/json negli header. Se vedi text/plain o nessun Content-Type, significa che hai chiamato setHeader() dopo writeHead() oppure lo hai completamente dimenticato.
Scenario comune di debug: Se il tuo JavaScript frontend ottiene undefined quando chiami fetch('/api').then(r => r.json()), controlla gli header con curl -i. Il Content-Type deve essere esattamente application/json (sensible al maiuscolo/minuscolo per alcuni parser rigorosi).
Salva ed esci da json.js premendo CTRL+X. Ora, avviamo il server con il comando node:
node json.jsIn un altro terminale, raggiungiamo il server usando cURL:
curl http://localhost:8000Premendo INVIO, vedremo il seguente risultato:
{"message": "This is a JSON response"}Abbiamo ora restituito con successo una risposta JSON, proprio come molte delle API popolari con cui creiamo app. Assicurati di uscire dal server in esecuzione con CTRL+C in modo da poter tornare al prompt standard del terminale. Successivamente, diamo un’occhiata a un altro formato popolare per restituire dati: CSV.
Servendo CSV
Il formato di file Comma Separated Values (CSV) è uno standard di testo comunemente usato per fornire dati tabellari. Nella maggior parte dei casi, ogni riga è separata da un ritorno a capo e ogni elemento nella riga è separato da una virgola.
Nel nostro spazio di lavoro, apri il file csv.js con un editor di testo:
nano csv.jsAggiungiamo le seguenti righe alla nostra funzione requestListener():
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/csv");
res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
};
...Questa volta, il nostro Content-Type indica che viene restituito un file CSV, poiché il valore è text/csv. Il secondo header che aggiungiamo è Content-Disposition. Questo header indica al browser come visualizzare i dati, in particolare nel browser o come file separato.
Quando restituiamo risposte CSV, la maggior parte dei browser moderni scarica automaticamente il file anche se l'intestazione Content-Disposition non è impostata. Tuttavia, quando restituiamo un file CSV dovremmo comunque aggiungere questa intestazione in quanto ci permette di impostare il nome del file CSV. In questo caso, comunichiamo al browser che questo file CSV è un allegato e dovrebbe essere scaricato. Quindi indichiamo al browser che il nome del file è oceanpals.csv.
Scriviamo i dati CSV nella risposta HTTP:
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/csv");
res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
res.writeHead(200);
res.end(`id,name,email\n1,Sammy Shark,shark@ocean.com`);
};
...Restituire uno status 200/OK con i dati CSV. La virgola separa i valori in ogni colonna e il carattere di nuova riga (\n) separa le righe. Abbiamo due righe: la riga dell'intestazione e una riga di dati.
Trucco di formattazione CSV: L'esempio semplice sopra funziona, ma la generazione di CSV in produzione richiede l'escape dei caratteri. Se i tuoi dati contengono virgole (come "Smith, John") o virgolette, romperanno il formato CSV. Il corretto escape del CSV avvolge i valori tra virgolette e raddoppia le virgolette interne: "Smith, John" diventa "Smith, ""John""" nel CSV. Per la produzione, utilizza una libreria come csv-stringify o esegui correttamente l'escape dei valori per prevenire errori di parsing.
Cosa succede senza Content-Disposition? La maggior parte dei browser scarica il file se è impostato Content-Type: text/csv, ma il nome del file potrebbe essere il percorso URL o qualcosa di generico. L'intestazione attachment;filename=oceanpals.csv garantisce che il browser lo salvi con un nome specifico. Senza attachment, alcuni browser cercano di visualizzare il CSV direttamente, il che spesso risulta disordinato.
Testeremo questo server nel browser. Salva csv.js ed esci dall'editor con CTRL+X.
Avvia il server con il comando Node.js:
node csv.jsIn un altro Terminale, raggiungiamo il server usando cURL:
curl http://localhost:8000La console mostrerà questo:
id,name,email
1,Sammy Shark,shark@ocean.comSe andiamo su http://localhost:8000 nel nostro browser, verrà scaricato un file CSV. Il nome del file sarà oceanpals.csv.
Esci dal server in esecuzione con CTRL+C per tornare al prompt del terminale standard.
Dopo aver restituito JSON e CSV, abbiamo coperto due casi comuni per le API. Passiamo a come restituiamo i dati per i siti web che le persone visualizzano in un browser.
Servendo HTML
HTML (HyperText Markup Language) è il formato più comune da usare quando vogliamo che gli utenti interagiscano con il nostro server tramite un browser web. È stato creato per strutturare i contenuti web. I browser web sono progettati per visualizzare contenuti HTML, così come qualsiasi stile aggiungiamo con il CSS, un'altra tecnologia front-end che ci permette di modificare l'estetica dei nostri siti web.
Riapriamo html.js con il nostro editor di testo:
nano html.jsModifica la funzione requestListener() per restituire l'intestazione Content-Type appropriata per una risposta HTML:
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/html");
};
...Ora, ritorniamo il contenuto HTML all'utente. Aggiungi le righe evidenziate a html.js in modo che assomigli a questo:
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(`<html><body><h1>This is HTML</h1></body></html>`);
};
...Per prima cosa aggiungiamo il codice di stato HTTP. Poi chiamiamo response.end() con un argomento stringa che contiene HTML valido. Quando accediamo al nostro server nel browser, vedremo una pagina HTML con un tag header contenente This is HTML.
Salviamo ed usciamo premendo CTRL+X. Ora, avviamo il server con il comando node:
node html.jsVedremo Server is running on http://localhost:8000 quando il nostro programma sarà avviato.
Ora apri il browser e visita http://localhost:8000. La nostra pagina apparirà così:
Interrompiamo il server in esecuzione con CTRL+C e torniamo al prompt standard del terminale.
È comune che l'HTML venga scritto in un file, separato dal codice lato server come i nostri programmi Node.js. Successivamente, vediamo come possiamo restituire risposte HTML dai file.
Fase 3 — Servire una pagina HTML da un file
Possiamo servire HTML come stringhe in Node.js all'utente, ma è preferibile caricare file HTML e servirne il contenuto. In questo modo, man mano che il file HTML cresce, non dobbiamo mantenere lunghe stringhe nel nostro codice Node.js, mantenendolo più conciso e permettendoci di lavorare su ogni aspetto del nostro sito web in modo indipendente. Questa "separazione delle preoccupazioni" è comune in molte configurazioni di sviluppo web, quindi è utile sapere come caricare file HTML per supportarla in Node.js.
Per servire file HTML, carichiamo il file HTML con il modulo fs e utilizziamo i suoi dati quando scriviamo la nostra risposta HTTP.
Per prima cosa, creeremo un file HTML che il server web restituirà. Crea un nuovo file HTML:
touch index.htmlOra apri index.html in un editor di testo:
nano index.htmlLa nostra pagina web sarà minimale. Avrà uno sfondo arancione e mostrerà del testo di saluto al centro. Aggiungi questo codice al file:
<!DOCTYPE html>
<head>
<title>My Website</title>
<style>
*,
html {
margin: 0;
padding: 0;
border: 0;
}
html {
width: 100%;
height: 100%;
}
body {
width: 100%;
height: 100%;
position: relative;
background-color: rgb(236, 152, 42);
}
.center {
width: 100%;
height: 50%;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-family: "Trebuchet MS", Helvetica, sans-serif;
text-align: center;
}
h1 {
font-size: 144px;
}
p {
font-size: 64px;
}
</style>
</head>
<body>
<div class="center">
<h1>Hello Again!</h1>
<p>This is served from a file</p>
</div>
</body>
</html>Questa singola pagina web mostra due righe di testo: Hello Again! e This is served from a file. Le righe appaiono al centro della pagina, una sopra l'altra. La prima riga di testo è mostrata come intestazione, quindi sarà grande. La seconda riga di testo apparirà leggermente più piccola. Tutto il testo sarà di colore bianco e la pagina web avrà uno sfondo arancione.
Anche se non è l'argomento di questo articolo o di questa serie, se sei interessato a saperne di più su HTML, CSS e altre tecnologie web front-end, puoi dare un'occhiata alla guida di Mozilla Introduzione al Web.
Questo è tutto ciò di cui abbiamo bisogno per l'HTML, quindi salva ed esci dal file con CTRL+X. Ora possiamo passare al codice del server.
Per questo esercizio, lavoreremo su htmlFile.js. Aprilo con l'editor di testo:
nano htmlFile.jsPoiché dobbiamo leggere un file, iniziamo importando il modulo fs:
const http = require("http");
const fs = require('fs').promises;
...Questo modulo contiene una funzione readFile() che useremo per caricare il file HTML al suo posto. Importiamo la variante basata su promise in linea con le migliori pratiche del JavaScript moderno. Usiamo le promise poiché sintatticamente sono più concise delle callback, che avremmo dovuto usare se avessimo assegnato fs semplicemente con require('fs'). Per saperne di più sulle migliori pratiche di programmazione asincrona, puoi leggere la nostra guida Come Scrivere Codice Asincrono in Node.js.
Vogliamo che il nostro file HTML venga letto quando un utente richiede il nostro sistema. Iniziamo modificando requestListener() per leggere il file:
...
const requestListener = function (req, res) {
fs.readFile(__dirname + "/index.html")
};
...Usiamo il metodo fs.readFile() per caricare il file. Il suo argomento è __dirname + "/index.html". La variabile speciale __dirname contiene il percorso assoluto della posizione in cui viene eseguito il codice Node.js. Successivamente aggiungiamo /index.html in modo da poter caricare il file HTML che abbiamo creato in precedenza.
Ora ritorniamo alla pagina HTML una volta caricata:
...
const requestListener = function (req, res) {
fs.readFile(__dirname + "/index.html")
.then(contents => {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(contents);
})
};
...Se la promessa di fs.readFile() viene risolta con successo, restituirà i suoi dati. Usiamo il metodo then() per gestire questo caso. Il parametro contents contiene i dati del file HTML.
Per prima cosa impostiamo l'intestazione Content-Type su text/html per informare il client che stiamo restituendo dati HTML. Successivamente scriviamo il codice di stato per indicare che la richiesta è stata completata con successo. Infine inviamo al client la pagina HTML che abbiamo caricato, con i dati nella variabile contents.
Il metodo fs.readFile() può fallire a volte, quindi dovremmo gestire questo caso quando si verifica un errore. Aggiungi questo alla funzione requestListener():
...
const requestListener = function (req, res) {
fs.readFile(__dirname + "/index.html")
.then(contents => {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(contents);
})
.catch(err => {
res.writeHead(500);
res.end('Internal Server Error');
return;
});
};
...Salva il file ed esci da nano con CTRL+X.
Quando una promise incontra un errore, viene rifiutata. Gestiamo questo caso con il metodo catch(). Imposta il codice di stato su 500, indicando che si è verificato un errore interno, e invia un messaggio di errore comprensibile all'utente. In produzione, non dovresti mai mostrare dettagli grezzi dell'errore agli utenti, poiché questo potrebbe divulgare informazioni sensibili.
Esegui il nostro server con il comando node:
node htmlFile.jsNel browser web, visita http://localhost:8000. Vedrai questa pagina:
Hai ora restituito una pagina HTML dal server all'utente. Puoi interrompere il server in esecuzione con CTRL+C. Vedrai il prompt del terminale tornare quando lo farai.
Quando si scrive del codice del genere in produzione, potresti non voler caricare una pagina HTML ogni volta che ricevi una richiesta HTTP. Anche se questa pagina HTML è di circa 800 byte, siti web più complessi possono essere di dimensioni megabyte. I file di grandi dimensioni possono richiedere tempo per essere caricati. Se il tuo sito si aspetta molto traffico, potrebbe essere meglio caricare i file HTML all'avvio e salvare i loro contenuti. Dopo averli caricati, puoi configurare il server e farlo ascoltare le richieste su un indirizzo.
Per dimostrare questo metodo, vediamo come possiamo rielaborare il nostro server per renderlo più efficiente e scalabile.
Servire HTML in modo efficiente
Invece di caricare l'HTML per ogni richiesta, in questo passaggio lo caricheremo una sola volta all'inizio. La richiesta restituirà i dati che abbiamo caricato all'avvio.
Nel terminale, riapri lo script di Node.js con un editor di testo:
nano htmlFile.jsIniziamo aggiungendo una nuova variabile prima di creare la funzione requestListener():
...
let indexFile;
const requestListener = function (req, res) {
...Quando eseguiamo questo programma, questa variabile conterrà il contenuto del file HTML.
Ora, riorganizziamo la funzione requestListener(). Invece di caricare il file, restituirà ora il contenuto di indexFile:
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(indexFile);
};
...Successivamente, spostiamo la logica di lettura del file dalla funzione requestListener() all’avvio del nostro server. Apportiamo le seguenti modifiche mentre creiamo il server:
...
const server = http.createServer(requestListener);
fs.readFile(__dirname + "/index.html")
.then(contents => {
indexFile = contents;
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});
})
.catch(err => {
console.error(`Could not read index.html file: ${err}`);
process.exit(1);
});Salva il file ed esci da nano con CTRL+X.
Il codice che legge il file è simile a quello che abbiamo scritto nel nostro primo tentativo. Tuttavia, quando leggiamo con successo il file, salviamo ora il contenuto nella nostra variabile globale indexFile. Poi avviamo il server con il metodo listen(). La cosa fondamentale è che il file sia caricato prima che il server venga eseguito. In questo modo, la funzione requestListener() sarà sicura di restituire una pagina HTML, dato che indexFile non è più una variabile vuota.
Anche il nostro gestore degli errori è cambiato. Se il file non può essere caricato, catturiamo l'errore e lo stampiamo nella nostra console. Poi usciamo dal programma Node.js con la funzione exit() senza avviare il server. In questo modo possiamo capire perché la lettura del file è fallita, risolvere il problema e poi avviare di nuovo il server.
Abbiamo ora creato diversi server web che restituiscono vari tipi di dati a un utente. Finora, non abbiamo utilizzato alcun dato della richiesta per determinare cosa dovrebbe essere restituito. Dovremo utilizzare i dati della richiesta quando configuriamo diversi percorsi o rotte in un server Node.js, quindi ora vediamo come funzionano insieme.
Fase 4 — Gestione delle rotte utilizzando un oggetto di richiesta HTTP
La maggior parte dei siti web che visitiamo o delle API che utilizziamo di solito ha più di un endpoint in modo da poter accedere a diverse risorse. Un buon esempio sarebbe un sistema di gestione dei libri, uno che potrebbe essere utilizzato in una biblioteca. Non dovrebbe solo gestire i dati dei libri, ma anche i dati degli autori per facilitare la catalogazione e la ricerca.
Anche se i dati relativi ai libri e agli autori sono collegati, si tratta di due oggetti diversi. In questi casi, gli sviluppatori software di solito codificano ciascun oggetto su endpoint diversi come modo per indicare all'utente dell'API con quale tipo di dati stanno interagendo.
Creiamo un nuovo server per una piccola biblioteca, che restituirà due diversi tipi di dati. Se l'utente va all'indirizzo del nostro server su /books, riceverà una lista di libri in formato JSON. Se va su /authors, riceverà una lista di informazioni sugli autori in formato JSON.
Finora, il server restituisce la stessa risposta a ogni richiesta. Per supportare più risorse, memorizza i tuoi dati in variabili e utilizza un routing condizionale. Ad esempio:
const books = JSON.stringify([
{ title: "The Alchemist", author: "Paulo Coelho", year: 1988 },
{ title: "The Prophet", author: "Kahlil Gibran", year: 1923 }
]);
const authors = JSON.stringify([
{ name: "Paulo Coelho", countryOfBirth: "Brazil", yearOfBirth: 1947 },
{ name: "Kahlil Gibran", countryOfBirth: "Lebanon", yearOfBirth: 1883 }
]);
const requestListener = function (req, res) {
res.setHeader("Content-Type", "application/json");
switch (req.url) {
case "/books":
res.writeHead(200);
res.end(books);
break;
case "/authors":
res.writeHead(200);
res.end(authors);
break;
default:
res.writeHead(404);
res.end(JSON.stringify({error:"Resource not found"}));
}
}Prova con curl http://localhost:8000/books o /authors per vedere i rispettivi dati. Qualsiasi altro URL restituisce un errore 404.
Il problema con il semplice confronto di stringhe: req.url è una stringa grezza che include tutto ciò che viene dopo il dominio, cioè percorsi, stringhe di query e frammenti. Se qualcuno visita /books?id=123, il tuo case "/books" non corrisponderà perché req.url è /books?id=123, non /books. I parametri della query interrompono il routing: /books?sort=name non corrisponderà a /books. I caratteri codificati nell'URL possono interrompere il routing: /books%20reviews (spazio codificato come %20) non corrisponderà a /books reviews.
Usa invece l'API URL: L'API URL separa automaticamente il percorso dai parametri di query, decodifica i caratteri codificati nell'URL e gestisce correttamente i casi limite.
const host = req.headers.host || 'localhost:8000';
const url = new URL(req.url, `http://${host}`);
switch (url.pathname) {
case '/books':
// Validate HTTP method - only GET allowed for this endpoint
if (req.method !== 'GET') {
res.writeHead(405, { 'Allow': 'GET' });
return res.end(JSON.stringify({ error: 'Method not allowed' }));
}
res.writeHead(200);
return res.end(books);
case '/authors':
if (req.method !== 'GET') {
res.writeHead(405, { 'Allow': 'GET' });
return res.end(JSON.stringify({ error: 'Method not allowed' }));
}
res.writeHead(200);
return res.end(authors);
default:
res.writeHead(404);
return res.end(JSON.stringify({ error: 'Resource not found' }));
}Perché controllare il metodo HTTP? Lo stesso percorso potrebbe supportare metodi HTTP diversi (GET per recuperare, POST per creare). Senza questo controllo, una richiesta POST a /books restituirebbe l'elenco dei libri invece di creare un nuovo libro, violando le convenzioni delle API REST. Il codice di stato 405 Method Not Allowed con l'intestazione Allow indica ai client quali metodi sono validi per questo endpoint.
Gestione della query string: L'API URL separa automaticamente il percorso (/books) dai parametri di query (?id=123), quindi /books?id=123 instrada correttamente verso l'endpoint dei libri, mentre è comunque possibile accedere a url.searchParams.get('id') se necessario.
Testa vari scenari:
curl http://localhost:8000/books
curl http://localhost:8000/authors
curl http://localhost:8000/books?id=123 # Query string doesn't break routing
curl -X POST http://localhost:8000/books # Returns 405 Method Not Allowed
curl http://localhost:8000/nonexistent # Returns 404Passo 5 — Gestione delle richieste POST e analisi dei corpi delle richieste
Finora ci siamo occupati solo delle richieste GET. La maggior parte delle API deve accettare dati dai client, cosa che avviene tipicamente tramite richieste POST. Il modulo http richiede l'analisi manuale dei corpi delle richieste, che vengono ricevuti come flussi.
Osservazione critica: A differenza delle richieste GET, dove i dati arrivano nell'URL, i corpi delle richieste POST arrivano come un flusso. Questo significa che i dati non arrivano tutti in una volta, ma a pezzi. Se provi a analizzare il JSON prima che tutti i pezzi siano arrivati, otterrai dati incompleti o malformati. Devi prima raccogliere tutti i pezzi, poi eseguire l'analisi.
Ecco cosa succede se provi a fare il parsing troppo presto:
// WRONG - Body isn't complete yet
req.on('data', chunk => {
const data = JSON.parse(chunk); // Will fail - chunk is only part of the body
});
// CORRECT - Collect all chunks first
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
const data = JSON.parse(body); // Now body is complete
});Crea un server che accetti richieste POST con payload JSON. Crea un nuovo file:
touch post.js
cp routes.js post.jsApri post.js:
nano post.jsEcco un parser di corpo JSON pronto per la produzione che gestisce le questioni di sicurezza:
const MAX_BODY = 1 * 1024 * 1024; // 1MB limit prevents memory exhaustion
function parseJSONBody(req, res) {
return new Promise((resolve, reject) => {
let body = '';
let received = 0;
// Validate Content-Type before processing
const contentType = req.headers['content-type'] || '';
if (!contentType.startsWith('application/json')) {
res.writeHead(415, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Unsupported Content-Type' }));
return reject(new Error('Unsupported Content-Type'));
}
// Collect data chunks
req.on('data', chunk => {
received += chunk.length;
// Prevent memory exhaustion from huge payloads
if (received > MAX_BODY) {
res.writeHead(413, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Payload too large' }));
req.destroy(); // Stop receiving more data
return reject(new Error('Payload too large'));
}
body += chunk.toString(); // Convert Buffer to string
});
// All chunks received, now parse
req.on('end', () => {
try {
if (!body) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Empty request body' }));
return reject(new Error('Empty request body'));
}
const data = JSON.parse(body);
// Validate it's an object (not a string, number, etc.)
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON object' }));
return reject(new Error('Invalid JSON object'));
}
resolve(data);
} catch (parseError) {
// JSON.parse() throws for malformed JSON
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Malformed JSON' }));
console.error('JSON parse error:', parseError.message);
}
});
// Handle network errors
req.on('error', (error) => {
console.error('Request error:', error.message);
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Request error' }));
});
});
}Perché controllare il Content-Type? Se un client invia dati di un modulo con Content-Type: application/x-www-form-urlencoded, cercare di analizzarli come JSON fallirà. Il codice di stato 415 Unsupported Media Type comunica al client che il loro Content-Type non è accettabile.
Perché limitare la dimensione del corpo? Senza un limite, un client malevolo potrebbe inviare gigabyte di dati, esaurendo la memoria del tuo server. La risposta 413 Payload Too Large e req.destroy() interrompono immediatamente la connessione quando il limite viene superato.
Perché convalidare il risultato analizzato? JSON.parse('"hello"') riesce ma restituisce una stringa, non un oggetto. Il tuo codice potrebbe aspettarsi che data.property funzioni, ma le stringhe non hanno proprietà. Il controllo garantisce di ottenere un vero oggetto.
Usa questa funzione nel tuo gestore POST:
const requestListener = async function (req, res) {
if (req.method === 'POST' && req.url === '/books') {
const bookData = await parseJSONBody(req, res);
// Now use bookData safely
// ...
}
};Fase 6 — Servire file statici
Molte applicazioni web hanno bisogno di servire risorse statiche come immagini, file CSS, file JavaScript o altre risorse. Sebbene il modulo http non abbia un servizio di file statici integrato come express.static di Express, è possibile implementarlo utilizzando i moduli fs e path.
Problema di sicurezza critico: Se usi ingenuamente path.join(__dirname, 'public', req.url) senza convalida, un aggressore può accedere a qualsiasi file sul tuo server tramite attacchi di directory traversal. Una richiesta come GET /../../../etc/passwd potrebbe esporre file di sistema. Il controllo con path.resolve() garantisce che il percorso risolto non esca mai dalla directory public.
Ecco cosa può andare storto senza una corretta convalida del percorso:
// VULNERABLE - Allows directory traversal
const filePath = path.join(__dirname, 'public', req.url);
// Request: /../../../etc/passwd
// Resolves to: /etc/passwd (outside public directory!)
// SECURE - Validates path stays within public directory
const safePath = path.resolve(publicDir, '.' + reqPath);
if (!safePath.startsWith(publicDir)) {
res.writeHead(403);
res.end('Forbidden');
return;
}
// Request: /../../../etc/passwd
// safePath: /home/user/app/public/../../../etc/passwd
// Resolved: /etc/passwd
// Check fails because /etc/passwd doesn't start with /home/user/app/publicCrea un server di file statici sicuro:
touch static.jsApri static.js:
nano static.jsServi file statici in modo efficiente e sicuro facendo lo streaming con fs.createReadStream() e una risoluzione sicura dei percorsi:
const http = require("http");
const fs = require("fs");
const path = require("path");
const host = 'localhost';
const port = 8000;
const mimeTypes = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.pdf': 'application/pdf'
};
const publicDir = path.resolve(__dirname, 'public');
const requestListener = (req, res) => {
// Normalize the request path
let reqPath = req.url === '/' ? '/index.html' : req.url;
// Strip query string - ?id=123 shouldn't affect file path
reqPath = reqPath.split('?')[0];
// Resolve path and check it stays within publicDir
// The '.' prefix prevents absolute paths from being resolved incorrectly
const safePath = path.resolve(publicDir, '.' + reqPath);
// Critical security check: ensure resolved path is within publicDir
if (!safePath.startsWith(publicDir)) {
res.writeHead(403, { 'X-Content-Type-Options': 'nosniff' });
res.end('Forbidden');
return;
}
// Check if file exists and is actually a file (not a directory)
fs.stat(safePath, (err, stat) => {
if (err || !stat.isFile()) {
res.writeHead(404, { 'X-Content-Type-Options': 'nosniff' });
res.end('File not found');
return;
}
// Determine MIME type from file extension
const ext = path.extname(safePath).toLowerCase();
const contentType = mimeTypes[ext] || 'application/octet-stream';
res.setHeader("Content-Type", contentType);
res.setHeader("X-Content-Type-Options", "nosniff"); // Prevents MIME sniffing attacks
res.writeHead(200);
// Stream file instead of loading into memory
// Critical for large files - won't consume all memory
const stream = fs.createReadStream(safePath);
stream.pipe(res);
stream.on('error', () => {
// File might be deleted between stat() and createReadStream()
res.writeHead(500, { 'X-Content-Type-Options': 'nosniff' });
res.end('Server error');
});
});
};
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
console.log(`Serving static files from: ${publicDir}`);
});Crea una directory public e aggiungi un file di prova:
mkdir public
echo "<h1>Static File Served!</h1>" > public/index.htmlAvvia il server:
node static.jsVisita http://localhost:8000 nel tuo browser per vedere il file statico servito.
Perché usare lo streaming invece di fs.readFile()? Per un file video da 100 MB, fs.readFile() carica l'intero file in memoria prima di inviarlo. Con fs.createReadStream(), i dati fluiscono direttamente dal disco alla rete a piccoli blocchi, utilizzando una quantità minima di memoria indipendentemente dalla dimensione del file.
Perché verificare stat.isFile()? Se qualcuno richiede un percorso di directory come /images/, fs.stat() ha successo ma isFile() restituisce false. Senza questo controllo, si tenterebbe di inviare in streaming una directory, il che fallirebbe. Il controllo garantisce che vengano serviti solo file veri e propri.
Testare la protezione contro il percorso attraversabile:
curl http://localhost:8000/../../../etc/passwd
# Should return 403 Forbidden, not the system fileFase 7 — Gestione degli errori e registrazione
La gestione robusta degli errori è cruciale per i server di produzione. Il modulo http non fornisce una gestione degli errori automatica, quindi devi implementarla tu stesso.
La gestione corretta degli errori implica catturare gli errori asincroni, registrarli in modo appropriato, inviare risposte di errore comprensibili all'utente e prevenire il crash del server dovuto a errori non gestiti. Miglioriamo il nostro server con una gestione degli errori e un logging completi:
const http = require("http");
const fs = require('fs').promises;
const host = 'localhost';
const port = 8000;
// Simple logging utility
const log = {
info: (msg) => console.log(`[INFO] ${new Date().toISOString()} - ${msg}`),
error: (msg, err) => console.error(`[ERROR] ${new Date().toISOString()} - ${msg}`, err),
warn: (msg) => console.warn(`[WARN] ${new Date().toISOString()} - ${msg}`)
};
const requestListener = async function (req, res) {
try {
log.info(`${req.method} ${req.url} - ${req.headers['user-agent'] || 'Unknown'}`);
if (req.url === '/') {
try {
const data = await fs.readFile(__dirname + '/index.html');
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(data);
} catch (error) {
log.error('Failed to read index.html', error);
res.writeHead(500);
res.end('<h1>Internal Server Error</h1><p>Unable to load page.</p>');
}
} else if (req.url === '/error') {
throw new Error('Simulated error for testing');
} else {
res.writeHead(404);
res.end('<h1>Not Found</h1><p>The requested resource was not found.</p>');
}
} catch (error) {
log.error('Unhandled error in request handler', error);
const errorMessage = process.env.NODE_ENV !== 'production'
? error.message
: 'An unexpected error occurred';
res.writeHead(500);
res.end(`<h1>Internal Server Error</h1><p>${errorMessage}</p>`);
}
};
const server = http.createServer(requestListener);
// Handle server-level errors
server.on('error', (error) => {
if (error.code === 'EADDRINUSE') {
log.error(`Port ${port} is already in use`);
} else {
log.error('Server error', error);
}
process.exit(1);
});
// Handle client connection errors
server.on('clientError', (err, socket) => {
log.warn('Client error: ' + err.message);
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(port, host, () => {
log.info(`Server is running on http://${host}:${port}`);
});Chiusura Graduale (SIGTERM/SIGINT)
Quando si esegue sotto gestori di processi come PM2 o Docker, gestire i segnali di terminazione per chiudere il server in modo ordinato:
const connections = new Set();
server.on('connection', socket => {
connections.add(socket);
socket.on('close', () => connections.delete(socket));
});
const shutdown = (signal) => {
log.warn(`${signal} received: shutting down gracefully...`);
server.close(() => {
log.info('Server closed.');
process.exit(0);
});
setTimeout(() => {
log.warn('Forcing socket close...');
connections.forEach(s => s.destroy());
process.exit(1);
}, 10000).unref();
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));Pratiche principali di gestione degli errori mostrate:
- Blocchi try-catch attorno alle operazioni asincrone
- Gestori di errori a livello di server per problemi di connessione e porta
- Gestione degli errori client per richieste malformate
- Registrazione con timestamp e livelli di gravità
- Messaggi di errore consapevoli dell'ambiente (non esporre gli stack trace in produzione)
- Risposte di errore eleganti con codici di stato HTTP appropriati
Migliore Pratica: In produzione, considera l'uso di librerie di logging strutturato come Winston o Pino, e invia gli errori critici a servizi di monitoraggio come Sentry o Rollbar. Questo esempio utilizza un semplice logging su console a scopo didattico.
Passo 8 — Utilizzo della sintassi dei moduli ES (ESM)
Node.js moderno supporta i moduli ES (ESM) utilizzando la sintassi import/export invece di require/module.exports di CommonJS. Per usare ESM, rinomina il tuo file con estensione .mjs oppure imposta "type": "module" nel tuo package.json.
La sintassi ESM utilizza import invece di require e export invece di module.exports. Ecco lo stesso server di base usando la sintassi dei moduli ES:
Crea server.mjs:
touch server.mjs
nano server.mjsimport http from 'http';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
// ES Modules don't have __dirname, so we recreate it
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const host = 'localhost';
const port = 8000;
const requestListener = async (req, res) => {
try {
if (req.url === '/') {
const data = await fs.readFile(path.join(__dirname, 'index.html'));
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(data);
} else {
res.writeHead(404);
res.end('Not found');
}
} catch (error) {
console.error('Error:', error);
res.writeHead(500);
res.end('Server error');
}
};
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});Eseguilo:
node server.mjsDifferenze principali: In ESM, __dirname non è disponibile—devi ricavarlo da import.meta.url. Inoltre, i percorsi dei file nelle operazioni fs potrebbero richiedere una risoluzione esplicita. Per il resto, l'API del modulo HTTP rimane identica.
In alternativa, crea package.json con "type": "module":
{
"type": "module"
}Poi usa file .js con la sintassi ESM:
import http from 'http';
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('ESM Server');
});
server.listen(8000);Considerazioni sulla produzione
Quando si passa dallo sviluppo alla produzione, entrano in gioco diverse considerazioni importanti:
Supporto HTTPS
Il modulo http serve solo HTTP. Per HTTPS, usa il modulo https in modo analogo:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('path/to/private-key.pem'),
cert: fs.readFileSync('path/to/certificate.pem')
};
const server = https.createServer(options, requestListener);Sicurezza: Non inserire mai chiavi private o certificati nel controllo delle versioni. In produzione, utilizza variabili d'ambiente o servizi sicuri di gestione dei segreti.
Configurazione del Reverse Proxy
In produzione, i server Node.js solitamente funzionano dietro proxy inversi come Nginx o Apache. Il proxy gestisce la terminazione SSL, i file statici e il bilanciamento del carico. Il tuo server Node.js dovrebbe:
- Ascolta solo su
localhosto127.0.0.1 - Leggi le intestazioni
X-Forwarded-Forper gli IP dei client - Fidati degli header del proxy solo dal tuo server proxy
Consulta la nostra guida su Come Configurare Nginx come Proxy Inverso su Ubuntu per i dettagli sull'installazione.
Gestione dei processi
Per la produzione, utilizza gestori di processi come PM2 per gestire:
- Riavvii automatici in caso di arresti anomali
- Distribuzioni senza tempi di inattività
- Gestione dei registri
- Modalità cluster per utilizzare più core CPU
Scopri di più nella nostra guida Come configurare un'applicazione Node.js per la produzione su Ubuntu.
Ottimizzazione delle prestazioni
Cache dei file: Carica i file statici all'avvio (come mostrato nel Passo 3) invece di leggerli dal disco ad ogni richiesta. Questo riduce significativamente il carico di I/O.
Trasmetti file di grandi dimensioni: Per file superiori a 1MB, usa
fs.createReadStream()e inviali alla risposta invece di caricare l'intero file in memoria.Pooling di connessioni: Per le connessioni al database, utilizza il pooling di connessioni per evitare il sovraccarico di creare nuove connessioni per ogni richiesta.
Compressione: Usa il modulo
zlibper la compressione gzip delle risposte testuali per ridurre la larghezza di banda:const zlib = require('zlib'); const gzip = zlib.createGzip(); res.setHeader('Content-Encoding', 'gzip'); gzip.pipe(res); gzip.write(data); gzip.end();Modalità cluster: Utilizza il modulo
clusterdi Node.js per avviare processi worker pari al numero di core della CPU, massimizzando la produttività per operazioni intensive di CPU.Timeout delle richieste: Imposta valori di timeout ragionevoli per evitare che i client lenti consumino risorse:
server.setTimeout(30000); // 30 secondsMonitoraggio della memoria: Tieni traccia dell'uso della memoria per rilevare precocemente eventuali perdite. In produzione, monitora la dimensione dello heap e implementa riavvii graduali se la memoria cresce senza limiti.
Migliori pratiche per caching e compressione
Per migliorare ulteriormente le prestazioni e ridurre i tempi di risposta, applica la memorizzazione nella cache e la compressione dove appropriato.
Abilita la memorizzazione nella cache per i file statici:
Aggiungi gli headerCache-ControleETagalle risposte dei tuoi file statici. Ad esempio:const stat = fs.statSync(safePath); res.setHeader('Cache-Control', 'public, max-age=86400'); // cache for 1 day res.setHeader('ETag', `"${stat.size}-${stat.mtime.getTime()}"`);I browser e i CDN riutilizzeranno le copie memorizzate nella cache finché l'ETag o la durata della cache non scadranno.
Usa la compressione gzip o Brotli:
Risposte basate su testo come HTML, CSS, JS e JSON si comprimono molto bene. Puoi applicare la compressione con il modulozlibdi Node:const zlib = require('zlib'); const stream = fs.createReadStream(safePath); res.setHeader('Content-Encoding', 'gzip'); stream.pipe(zlib.createGzip()).pipe(res);Questo riduce l'uso della larghezza di banda e accelera la consegna per i clienti con connessioni più lente.
Evita di memorizzare eccessivamente nella cache gli endpoint dinamici:
Per le API o i dati frequentemente aggiornati, imposta durate della cache più brevi (ad es.,max-age=60) o usano-storequando necessario.
Queste ottimizzazioni completano lo streaming di file statici e lo spegnimento graduale, aiutando il tuo server Node.js a offrire alte prestazioni rimanendo sicuro e prevedibile.
Usa il modulo http di Node.js per API minime, routing personalizzato o per apprendere gli internals di HTTP.
Usa Express/Fastify quando hai bisogno di middleware, templating o prototipazione rapida.
Risoluzione dei problemi comuni
Porta già in uso (EADDRINUSE)
Errore: Errore: listen EADDRINUSE: indirizzo già in uso :::8000
Soluzione:
- Trova il processo che utilizza la porta:
lsof -i :8000(macOS/Linux) onetstat -ano | findstr :8000(Windows) - Termina il processo o usa una porta diversa
- Aggiungi la gestione degli errori (come mostrato al Passo 7) per gestire questo in modo appropriato
Corpo della richiesta non ricevuto
Problema: Il corpo della richiesta POST è vuoto o non definito
Soluzione: I corpi delle richieste sono stream—è necessario raccogliere i frammenti. Usa sempre il modello mostrato nel Passo 5:
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
// Now parse body
});Errori CORS nel browser
Problema: Il browser blocca le richieste a causa della politica CORS
Soluzione: Aggiungi intestazioni CORS nella tua risposta:
res.setHeader("Access-Control-Allow-Origin", "*"); // Or specific domain
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");Sicurezza: L'uso di "*" per Access-Control-Allow-Origin consente qualsiasi origine. In produzione, specifica esplicitamente il dominio del tuo frontend.
File statici restituiscono 404
Problema: I file statici non vengono trovati anche se esistono
Soluzione:
- Controlla la risoluzione del percorso del file
- Verifica che
__dirnamepunti alla directory corretta - Assicurati che i permessi del file consentano la lettura
- Verificare che la sanitizzazione del percorso non rimuova caratteri validi
Domande Frequenti
Cos'è il modulo HTTP in Node.js?
Il modulo http è il modulo integrato di Node.js per la creazione di server e client HTTP. Fornisce API di basso livello per gestire le richieste e le risposte HTTP senza richiedere dipendenze esterne. Fa parte del core di Node.js, quindi è disponibile in ogni installazione di Node.js.
Come faccio a creare un server web semplice in Node.js?
Crea un server usando http.createServer() con una funzione ascoltatore per le richieste:
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World');
});
server.listen(8000);Questo crea un server in ascolto sulla porta 8000 che risponde a tutte le richieste con "Hello World".
Qual è la differenza tra il modulo HTTP e Express.js?
Il modulo http è una funzionalità core di Node.js che fornisce la gestione HTTP a basso livello. Express.js è un framework costruito sopra http che aggiunge routing, middleware, templating e altre comodità. Il modulo http ti dà il controllo completo ma richiede una configurazione più manuale, mentre Express.js offre astrazioni a costo di ulteriori dipendenze.
Usa il modulo http quando:
- Creare API leggere o microservizi
- Imparare le basi di HTTP
- Vuoi zero dipendenze
- Controllo completo sulla gestione delle richieste/risposte
Usa Express.js quando:
- Costruire applicazioni complesse con molte rotte
- Hai bisogno di un ecosistema di middleware
- La prototipazione rapida è una priorità
- Vuoi modelli standard della comunità
Per un'introduzione completa a Express.js e su come semplifica lo sviluppo web, consulta la nostra guida su Node.js Express Basics.
Posso creare un'API REST usando Node.js senza Express?
Sì, assolutamente. Il modulo http è sufficiente per costruire API REST. Dovrai gestire manualmente:
- Analisi del percorso (utilizzando
req.urlereq.method) - Analisi del corpo della richiesta (raccolta dei frammenti del flusso)
- Formattazione della risposta
- Gestione degli errori
Per le API semplici, questo è spesso preferibile all'aggiunta di dipendenze dal framework. Consulta i Passaggi 4 e 5 di questo tutorial per esempi di routing e gestione delle richieste POST.
Come gestisco percorsi diversi utilizzando il modulo HTTP di Node.js?
Analizza req.url e utilizza la logica condizionale o un'istruzione switch:
const requestListener = (req, res) => {
switch (req.url) {
case '/':
res.writeHead(200);
res.end('Home page');
break;
case '/about':
res.writeHead(200);
res.end('About page');
break;
default:
res.writeHead(404);
res.end('Not found');
}
};Per un instradamento più complesso, puoi analizzare i percorsi URL, le stringhe di query utilizzando il modulo url, e gestire diversi metodi HTTP.
Come invio risposte HTML o JSON con il modulo HTTP?
Imposta correttamente l'intestazione Content-Type e invia i dati:
HTML:
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end('<h1>Hello</h1>');JSON:
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(JSON.stringify({ message: 'Hello' }));L'intestazione Content-Type indica al client come interpretare i dati della risposta.
Quale porta dovrei usare per un server Node.js locale?
Le porte di sviluppo comuni sono:
- 8000 o 8080: Sviluppo generale HTTP
- 3000: Popolare con framework come Express
- 5000: Anche comune
Qualsiasi porta sopra il 1024 funziona per gli utenti non root. Le porte sotto il 1024 richiedono privilegi di amministratore. Controlla la disponibilità con lsof -i :PORT (macOS/Linux) prima dell'uso.
Come posso gestire gli errori 404 in un server HTTP Node.js?
Aggiungi un caso predefinito nella logica di routing:
const requestListener = (req, res) => {
if (req.url === '/') {
res.writeHead(200);
res.end('Home');
} else {
res.writeHead(404);
res.end('Not Found');
}
};Imposta sempre il codice di stato su 404 per i percorsi inesistenti per rispettare gli standard HTTP.
Come faccio a servire file statici senza Express?
Usa il modulo fs per leggere i file e il modulo path per una risoluzione sicura dei percorsi:
const fs = require('fs').promises;
const path = require('path');
const requestListener = async (req, res) => {
const filePath = path.join(__dirname, 'public', req.url);
try {
const data = await fs.readFile(filePath);
res.writeHead(200);
res.end(data);
} catch (error) {
res.writeHead(404);
res.end('File not found');
}
};Valida sempre i percorsi per prevenire attacchi di traversal delle directory (vedi il Passo 6 per un'implementazione sicura).
Posso usare i Moduli ES (import) con il modulo HTTP in Node.js?
Sì, Node.js supporta i moduli ES. Usa uno dei seguenti:
- File con estensione
.mjs:
import http from 'http';
// ... rest of code- Imposta
"type": "module"inpackage.json, quindi usa file.js:
{
"type": "module"
}Nota che i moduli ES non hanno __dirname: ricavalo da import.meta.url come mostrato nel Passo 8.
Come faccio a analizzare i corpi delle richieste POST con il modulo HTTP?
I corpi dei POST arrivano come flussi. Raccogli i blocchi, poi analizzali:
const parseBody = (req) => {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
try {
resolve(JSON.parse(body)); // For JSON
} catch (error) {
reject(error);
}
});
req.on('error', reject);
});
};Questo modello funziona per JSON, dati dei moduli o qualsiasi tipo di contenuto basato su testo.
Il modulo HTTP è adatto per l'uso in produzione?
Sì, il modulo http è pronto per la produzione ed è utilizzato da molte applicazioni Node.js. Tuttavia, considera:
- Gestione dei processi: Usa PM2 o simili per riavvii automatici
- Reverse proxy: Eseguire dietro Nginx/Apache per SSL e bilanciamento del carico
- Gestione degli errori: Implementare una gestione completa degli errori e della registrazione
- Sicurezza: convalidare gli input, sanificare i percorsi, impostare limiti di dimensione sulle richieste
- Prestazioni: Memorizza nella cache i file statici, usa il clustering per operazioni intensive della CPU
Molte applicazioni di produzione utilizzano direttamente il modulo http, soprattutto microservizi e API leggere. Per una guida completa al deployment in produzione, consulta Come configurare un'applicazione Node.js per la produzione su Ubuntu 22.04.
Conclusione
In questo tutorial, hai costruito server HTTP Node.js pronti per la produzione partendo da zero. Hai iniziato con una semplice risposta testuale, per poi passare a servire diversi tipi di contenuti (JSON, CSV e HTML). Hai implementato il routing per gestire URL diversi, analizzato i corpi delle richieste POST, servito file statici in modo sicuro e aggiunto gestione completa degli errori e logging. Hai anche esplorato sia la sintassi CommonJS che quella dei moduli ES, oltre alle considerazioni per il deployment in produzione.
Ora sei in grado di creare server web che gestiscono requisiti diversi senza dipendere da framework. Con questa conoscenza, puoi:
- Crea API REST leggere per microservizi
- Serve siti web multipagina con routing appropriato
- Gestire caricamenti e download di file
- Implementare logica middleware personalizzata
- Prendere decisioni informate su quando i framework aggiungono valore rispetto a una complessità inutile
Prossimi passi:
- Approfondisci i fondamenti di Node.js: Torna alla serie How To Code in Node.js per una copertura completa
- Esplora la documentazione ufficiale: La documentazione del modulo HTTP di Node.js contiene riferimenti dettagliati all'API e casi d'uso avanzati
- Quando hai bisogno di funzionalità del framework: Prendi in considerazione le basi di Express.js per uno sviluppo rapido con supporto a middleware e template
Comprendere questi fondamenti di HTTP ti rende un sviluppatore Node.js più competente. Che tu scelga di usare framework o lavorare direttamente con il modulo http, ora comprendi i meccanismi di base che alimentano tutte le applicazioni web Node.js. Questa conoscenza ti aiuta a risolvere problemi, ottimizzare le prestazioni e progettare sistemi che scalano in modo efficace.
Grazie per aver imparato con la community di Linux-Console.net.

