Federico Lelli / SchoolNode.js & Express
LABORATORIO FINALE

Progetto completo — API persone con Express

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

1. Preparazione

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

Crea un file server.cjs con il contenuto seguente.

2. Server completo (un solo file)

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

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

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

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

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

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

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

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

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

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

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

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

3. Provalo con il terminale

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

node server.cjs

In un secondo terminale:

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

La stessa sequenza in PowerShell

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

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

4. Risultati e limiti

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

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

5. Il viaggio di una richiesta, passo per passo

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

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

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

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

Controlli negativi con risultati attesi

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.

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