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
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.
| Parte | Responsabilità | Cosa controllare |
|---|---|---|
| nomeValido | Verifica nome testuale di 2–80 caratteri dopo trim | Non accetta numeri o soli spazi |
| idRichiesto | Legge e valida l’ID numerico nel percorso | Restituisce null dopo una risposta 400 |
| GET elenco | Filtra e limita i risultati | Query ripetute o limite fuori da 1–100 danno 400 |
| POST | Crea e assegna l’identità | Nome richiesto; ruolo predefinito utente se assente |
| PUT | Sostituisce i campi modificabili | Nome e ruolo obbligatori |
| PATCH | Modifica il solo ruolo | Nome resta invariato; gli altri campi sono ignorati |
| DELETE | Rimuove la persona mantenendo gli altri ID | 204 senza body, oppure 404 |
| Error handler | Traduce errori del parser e del server | 400 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=0o?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.