Backend con Node.js ed Express
Costruisci il server per DevNotes da zero: routing CRUD completo, middleware, CORS e gestione degli errori con Express.
Creeremo una cartella separata server/ nella root del progetto con un’applicazione Express completa. Esporrà le route /api/notes con tutti i metodi CRUD, userà dati in-memory per ora e sarà pronto a ricevere le richieste dal frontend React nella prossima lezione.
Node.js — JavaScript senza il browser
Node.js è un runtime JavaScript costruito sul motore V8 di Chrome. Permette di eseguire JavaScript al di fuori del browser: su un server, in un terminale, come script di build. La caratteristica chiave è il suo modello event loop + I/O non bloccante: invece di aspettare che una lettura dal disco o una query al database finiscano, Node.js esegue il resto del codice e torna sul risultato quando è pronto.
Usando JavaScript sia sul frontend (React) che sul backend (Node.js) condividi la stessa lingua, gli stessi strumenti (npm, ESLint, Prettier) e lo stesso modello mentale. Non devi cambiare contesto mentale quando passi dal componente React alla route Express.
Express — il framework minimalista
Express è un framework HTTP per Node.js che aggiunge tre cose fondamentali al modulo http nativo: routing dichiarativo, middleware componibili e API semplificate per leggere richieste e inviare risposte.
Struttura del progetto server
Il backend vivrà in una cartella server/ separata dalla cartella src/ di React. Questo mantiene le responsabilità ben divise:
devnotes/ ← root del monorepo ├── src/ ← frontend React (già esistente) ├── server/ ← backend Express (NUOVO) │ ├── index.js ← entry point del server │ ├── routes/ │ │ └── notes.js ← router dedicato alle note │ └── data/ │ └── notes.js ← dati in-memory (array) ├── package.json ← root package (frontend) └── vite.config.js
Setup del server Express
Prima di tutto inizializziamo il package del server e installiamo le dipendenze:
# dalla root del progetto cd server npm init -y npm install express cors
Poi creiamo il file principale server/index.js:
// server/index.js const express = require('express'); const cors = require('cors'); const notesRouter = require('./routes/notes'); const app = express(); const PORT = process.env.PORT || 3001; // ─── MIDDLEWARE GLOBALI ──────────────────────────────────────── // 1. CORS: consente al frontend (localhost:5173) di chiamare il backend app.use(cors({ origin: 'http://localhost:5173', // porta default di Vite methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], })); // 2. Body parser JSON: popola req.body con il payload JSON della richiesta app.use(express.json()); // 3. Logger minimale: stampa metodo e URL di ogni richiesta app.use((req, res, next) => { console.log(`${new Date().toISOString()} | ${req.method} ${req.url}`); next(); // passa al middleware/route successivo }); // ─── ROUTE ──────────────────────────────────────────────────── app.use('/api/notes', notesRouter); // Route di health check — utile per verificare che il server sia vivo app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // ─── ERRORE 404 (route non trovata) ─────────────────────────── app.use((req, res) => { res.status(404).json({ error: 'Route non trovata' }); }); // ─── GESTORE ERRORI GLOBALE ──────────────────────────────────── // Firma speciale con 4 parametri: Express riconosce automaticamente i middleware di errore app.use((err, req, res, next) => { console.error(err.stack); res.status(err.status || 500).json({ error: err.message || 'Errore interno del server' }); }); app.listen(PORT, () => { console.log(`Server DevNotes in ascolto su http://localhost:${PORT}`); });
I middleware — il cuore di Express
Un middleware è una funzione con la firma (req, res, next). Ogni richiesta attraversa in sequenza tutti i middleware registrati finché uno di essi invia una risposta o chiama next() per passare al successivo.
| Middleware | Cosa fa | Quando usarlo |
|---|---|---|
cors() | Aggiunge gli header Access-Control-Allow-* alla risposta | Sempre — il browser blocca le richieste cross-origin senza questi header |
express.json() | Parsa il body JSON e lo mette in req.body | Per tutte le route che ricevono POST/PUT/PATCH |
express.static() | Serve file statici da una cartella | Per servire la build React in produzione |
| Logger personalizzato | Stampa metodo + URL + timestamp | In sviluppo per il debug; in produzione usa `morgan` |
| Error handler (4 params) | Intercetta errori lanciati con next(err) | Sempre — va registrato per ultimo |
L’ordine con cui chiami app.use() è l’ordine di esecuzione. I middleware globali (CORS, body parser) devono venire prima delle route. Il gestore degli errori deve essere sempre l’ultimo.
CORS — perché è necessario
Il browser implementa la Same-Origin Policy: blocca le richieste JavaScript verso origini diverse da quella della pagina. Origine = schema + dominio + porta. localhost:5173 (Vite) e localhost:3001 (Express) hanno porte diverse → origini diverse → il browser blocca le richieste senza gli header CORS.
Il browser lancia: Access to fetch at 'http://localhost:3001/api/notes' from origin 'http://localhost:5173' has been blocked by CORS policy
Il server aggiunge Access-Control-Allow-Origin: http://localhost:5173 → il browser permette la richiesta.
I dati in-memory
Per ora le note vivono in un array JavaScript nel processo del server. Questo significa che si perdono ad ogni riavvio — ma è perfetto per sviluppare e testare la logica delle route prima di collegare un database.
// server/data/notes.js let notes = [ { id: 1, title: 'Setup del progetto', body: 'Installato Node.js, Vite e VS Code con le estensioni giuste.', tag: 'lavoro', createdAt: '2026-01-10T09:00:00.000Z', }, { id: 2, title: 'Componenti JSX', body: 'Header, NoteCard e NoteList. Le props fluiscono dall\'alto verso il basso.', tag: 'studio', createdAt: '2026-01-12T10:30:00.000Z', }, ]; // Contatore per gli ID (simula l'auto-increment del database) let nextId = 3; module.exports = { notes, getNextId: () => nextId++ };
Router CRUD per le note
Express permette di raggruppare route correlate in un Router separato. Crea server/routes/notes.js:
// server/routes/notes.js const express = require('express'); const { notes, getNextId } = require('../data/notes'); const router = express.Router(); // ─── GET /api/notes ──────────────────────────────────────────── // Restituisce tutte le note. Supporta ?tag= per filtrare. router.get('/', (req, res) => { const { tag } = req.query; const result = tag ? notes.filter(n => n.tag === tag) : notes; res.json(result); }); // ─── GET /api/notes/:id ──────────────────────────────────────── router.get('/:id', (req, res) => { const note = notes.find(n => n.id === Number(req.params.id)); if (!note) return res.status(404).json({ error: 'Nota non trovata' }); res.json(note); }); // ─── POST /api/notes ─────────────────────────────────────────── // Body atteso: { title, body, tag } router.post('/', (req, res) => { const { title, body, tag = 'personale' } = req.body; // Validazione minima if (!title || !body) { return res.status(400).json({ error: 'title e body sono obbligatori' }); } const newNote = { id: getNextId(), title, body, tag, createdAt: new Date().toISOString(), }; notes.push(newNote); res.status(201).json(newNote); // 201 Created }); // ─── PUT /api/notes/:id ──────────────────────────────────────── // Sostituzione completa della nota router.put('/:id', (req, res) => { const idx = notes.findIndex(n => n.id === Number(req.params.id)); if (idx === -1) return res.status(404).json({ error: 'Nota non trovata' }); const { title, body, tag } = req.body; if (!title || !body) { return res.status(400).json({ error: 'title e body sono obbligatori' }); } notes[idx] = { ...notes[idx], title, body, tag, updatedAt: new Date().toISOString() }; res.json(notes[idx]); }); // ─── DELETE /api/notes/:id ───────────────────────────────────── router.delete('/:id', (req, res) => { const idx = notes.findIndex(n => n.id === Number(req.params.id)); if (idx === -1) return res.status(404).json({ error: 'Nota non trovata' }); notes.splice(idx, 1); res.status(204).send(); // 204 No Content — nessun body }); module.exports = router;
Testare il server con curl e il browser
Avvia il server con node server/index.js e testalo prima ancora di collegarlo a React:
# Health check curl http://localhost:3001/health # Lista note curl http://localhost:3001/api/notes # Nota per ID curl http://localhost:3001/api/notes/1 # Crea una nota (POST con body JSON) curl -X POST http://localhost:3001/api/notes \ -H "Content-Type: application/json" \ -d '{"title":"Nuova nota","body":"Contenuto di test","tag":"lavoro"}' # Elimina la nota con id 1 curl -X DELETE http://localhost:3001/api/notes/1
Invece di curl puoi installare l’estensione REST Client (Huachao Mao) su VS Code: crea un file .http e invia richieste direttamente dall’editor con un click.
Gestione degli errori — pattern consigliato
Per le route async, usa un wrapper che cattura le Promise rejected e le passa a next(err) automaticamente:
// helper: avvolge un handler async e passa gli errori al gestore Express const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); // utilizzo nella route (quando aggiungeremo il database) router.get('/', asyncHandler(async (req, res) => { const notes = await db.find({}); // se lancia, asyncHandler lo cattura res.json(notes); }));
Nel lab costruirai il server Express completo: struttura cartelle, middleware, router CRUD, test con curl e REST Client, middleware di validazione, nodemon per il reload automatico.
Apri lab-07-express-backend.md su GitHub →- Express è un framework minimalista che aggiunge routing e middleware al modulo HTTP di Node.js.
- I middleware sono funzioni
(req, res, next)che si eseguono in sequenza — l’ordine di registrazione è l’ordine di esecuzione. - CORS è necessario perché il browser blocca per default le richieste cross-origin; il middleware `cors` aggiunge gli header corretti.
- express.json() parsa il body JSON delle richieste e lo espone come
req.body. - Ogni operazione CRUD corrisponde a un metodo HTTP:
GET,POST,PUT,DELETE. - Il gestore degli errori ha firma speciale a 4 parametri
(err, req, res, next)e va registrato per ultimo.