Parte II — Il back-end JavaScript · Capitolo 5

Express, REST API e OpenAPI

~45 min di lettura5 widget interattivi3 tavole

In questo capitolo

  1. Le domande del design di una API REST
  2. Resource modeling: i quattro archetipi
  3. Operazioni su collection e su document
  4. Best practice per gli URI
  5. Il corpo del messaggio e i codici di stato
  6. Documentare l'API: OpenAPI e Swagger
  7. Express: il primo server
  8. Rispondere: JSON, file statici, parametri
  9. Router e middleware
  10. CORS, validazione, sessioni e JWT
  11. La struttura routes, controllers, models
  12. Gli esercizi: la Filmoteca
  13. Verifica le tue conoscenze

1. Le domande del design di una API REST

Nel capitolo 3 abbiamo visto il server Web che serve file, e poi il server con templating che li compone. Con Node.js il server diventa un programma che decide cosa rispondere: e appena si arriva a questo punto, la domanda smette di essere tecnica e diventa di design. Progettare una API REST significa rispondere a un elenco preciso di domande, che l'esercitazione mette in fila senza girarci intorno:

Sono domande che non hanno una risposta imposta dal protocollo: HTTP permette tranquillamente GET /deleteUser?id=1234. È il design a stabilire che quella richiesta è sbagliata, e le prossime sezioni servono esattamente a giustificare perché.

2. Resource modeling: i quattro archetipi

Quando si modellano le risorse di una API si può partire da alcuni archetipi di risorse di base. Un'API REST è composta da quattro archetipi distinti: document, collection, store e controller.

Document

Una risorsa documento è un concetto simile a un'istanza di oggetto o a un record di database. La rappresentazione del suo stato include in genere sia valori sia collegamenti ad altre risorse collegate. Il tipo documento è l'archetipo di base per gli altri archetipi. Ciascuno di questi URI identifica un documento:

http://api.soccer.restapi.org/leagues/seattle
http://api.soccer.restapi.org/leagues/seattle/teams/trebuchet

Un documento può avere risorse figlio che rappresentano i suoi concetti subordinati; con la sua capacità di riunire molti tipi di risorse diversi sotto un unico genitore, un documento è il candidato logico per la risorsa radice di una API REST — nell'esempio, http://api.soccer.restapi.org identifica il punto di ingresso.

Collection

Una collection è un repository di risorse gestita dal server, usata per raggruppare risorse correlate. I client possono proporre nuove risorse da aggiungere a una collezione, ma spetta alla collezione scegliere se creare o meno la risorsa, cosa vuole contenere, e decidere gli URI delle risorse contenute:

http://api.soccer.restapi.org/leagues
http://api.soccer.restapi.org/leagues/seattle/teams

Store

Uno store è un repository di risorse gestito dal client. Consente al client di inserire risorse, recuperarle ed eliminarle; da soli gli store non creano nuove risorse e non generano nuovi URI. Ogni risorsa memorizzata ha un URI che è stato scelto da un client quando è stata inizialmente inserita. L'esempio mostra un utente (con ID 1234) che inserisce un documento chiamato alonso nel proprio archivio di preferiti:

PUT /users/1234/favorites/alonso

Controller

Un controller modella un concetto procedurale: funzioni eseguibili, con parametri e valori restituiti, di input e di output. Una API REST si basa sulle risorse controller per eseguire azioni specifiche dell'applicazione che non possono essere mappate logicamente su uno dei metodi standard (CRUD). In genere compaiono come ultimo segmento in un percorso URI, senza risorse figlio che li seguono nella gerarchia:

POST /alerts/245743/resend
Idea chiave

La distinzione decisiva è tra collection e store, e sta in chi decide l'URI. In una collection il client fa POST e il server risponde dicendo dove ha messo la risorsa; in uno store il client fa PUT su un URI che ha scelto lui. È la stessa differenza che passa tra "consegnare un documento a un archivio che lo protocolla" e "mettere un file in una cartella che è tua".

3. Operazioni su collection e su document

L'esempio guida dell'esercitazione è una smart city. Il punto di ingresso della API è http://api.smartcity.example.com/api, l'entità City ha un name (stringa) e una Population (numero), e le risorse possibili sono:

URIArchetipoSignificato
/citiescollectioncollezione di città
/cities/cesenadocumentdocumento che rappresenta la città di Cesena
/cities/cesena/sensorscollectioncollezione di sensori a Cesena
/cities/cesena/sensors/12345documentsensore con id 12345 a Cesena
/api entry point (document) /cities collection /cities/cesena document /cities/cesena/sensors collection /cities/cesena/sensors/12345 document gli altri due archetipi store: PUT /users/1234/favorites/alonso controller: POST /alerts/245743/resend la barra indica gerarchia: ogni livello contiene il successivo
Tavola 5.1 — La mappa delle risorse della smart city: collection al plurale, document al singolare, e la gerarchia espressa dalla barra.

Le operazioni sulle collezioni sono quattro: GET .../cities elenca tutte le città, POST .../cities crea una nuova città nella collezione, PUT .../cities aggiorna la collezione con il contenuto della richiesta, DELETE .../cities elimina tutte le città. Sui documenti invece: GET .../cities/cesena recupera i dettagli, PUT .../cities/cesena li aggiorna, DELETE .../cities/cesena elimina la città — e POST .../cities/cesena non ha senso.

RisorsaGETPOSTPUTDELETE
/citiesOttiene la lista di cittàCrea una nuova cittàAggiorna tutta la collezioneCancella tutta la collezione
/cities/cesenaOttiene i dettagli della città di Cesenanon applicabileAggiorna i dettagli della città di CesenaCancella la città di Cesena

Lo stesso schema si applica alle relazioni tra risorse: GET .../cities/cesena/sensors elenca tutti i sensori a Cesena, POST .../cities/cesena/sensors ne crea uno nuovo, GET .../cities/cesena/sensors/12345 recupera il contenuto di quel sensore, PUT .../cities/cesena/sensors/12345 ne aggiorna i dettagli. La relazione City has Sensor diventa un percorso.

verbo × risorsa risorsa \ metodo GET POST PUT DELETE /cities collection elenca 200 crea 201 aggiorna tutto cancella tutto /cities/cesena document dettagli 200 / 404 non applicabile la risorsa esiste già aggiorna cancella 204 POST crea un elemento DENTRO una collezione: su un documento singolo non ha destinatario le operazioni che non entrano in questa matrice sono risorse controller: POST /alerts/245743/resend
Tavola 5.2 — La matrice verbo × risorsa: sette caselle sensate e una vietata, che è quella su cui l'esame ama insistere.

4. Best practice per gli URI

Le API REST utilizzano gli Uniform Resource Identifier per indirizzare le risorse, e i designer spaziano tra soluzioni molto diverse: dal parlante http://api.restapi.org/paris/louvre/leonardo-da-vinci/mona-lisa all'opaco http://api.restapi.org/68dd0-a9d3-11e0-9f1c-0800200c9a66. Le regole raccolte a lezione sono queste.

RegolaEsempio
La barra indica una relazione gerarchica, e non va inclusa in fondo agli URI/leagues/seattle/teams
I trattini migliorano la leggibilitàhttp://api.example.org/blogs/mark/this-is-my-first-post
Preferire le lettere minuscole nei percorsihttp://api.example.org/my-folder/my-doc
Le estensioni di file non devono comparirehttp://api.example.org/students/3248234/transcripts/2005/fall
Sostantivo singolare per i document/leagues/seattle/teams/players/claudio
Sostantivo plurale per le collection/leagues/seattle/teams/players
Sostantivo plurale per gli store/artists/mikemassedotcom/playlists
Verbo o frase per i controller/students/morgan/register, /qa/nightly/runTestSuite
I segmenti possono essere sostituiti da valori basati sull'identità/leagues/:leagueId/teams/:teamId
I nomi delle funzioni CRUD non vanno negli URIDELETE /users/1234
La query filtra collection e storeGET /users?role=admin
La query impagina i risultatiGET /users?pageSize=25&pageStartIndex=50

Gli antipattern citati esplicitamente per la penultima riga sono quattro, e sono istruttivi perché sbagliano in modi diversi: GET /deleteUser?id=1234 (verbo nell'URI e metodo sbagliato, con un GET che modifica), GET /deleteUser/1234 (idem), DELETE /deleteUser/1234 (metodo giusto ma verbo ridondante nell'URI), POST /users/1234/delete (l'operazione mascherata da risorsa controller quando esiste già il metodo standard).

Sul versioning l'esercitazione mette a confronto due forme senza imporne una: http://api.soccer.restapi.org/my-app/api/leagues?version=1 e http://api.soccer.restapi.org/my-app/api/v1/leagues.

5. Il corpo del messaggio e i codici di stato

Una API REST utilizza comunemente il corpo del messaggio di risposta per trasmettere lo stato della risorsa identificata dalla richiesta, con un formato basato su testo che rappresenta lo stato come un insieme di campi significativi. Oggi i formati più usati sono XML e JSON:

{
  "firstName": "Mario",
  "lastName": "Rossi",
  "address": {
    "city": "Cesena",
    "postalCode": "47521"
  }
}

Ogni risposta HTTP contiene poi un codice di stato che indica l'esito della richiesta del client: sono numeri a tre cifre, raggruppati per classe.

ClasseRangeSignificato
1xx100-199Informazioni
2xx200-299Successo
3xx300-399Reindirizzamento
4xx400-499Errore lato client
5xx500-599Errore lato server
  • 200 OK — richiesta elaborata correttamente
  • 201 Created — risorsa creata con successo (es. POST)
  • 204 No Content — operazione avvenuta senza restituire corpo (es. DELETE)
  • 301 Moved Permanently — risorsa spostata in modo definitivo
  • 302 Found — reindirizzamento temporaneo
  • 304 Not Modified — risorsa non modificata (usata con la cache)
  • 400 Bad Request — richiesta non valida
  • 401 Unauthorized — autenticazione richiesta
  • 403 Forbidden — accesso negato anche se autenticato
  • 404 Not Found — risorsa non trovata
  • 418 I'm a teapot — easter egg (RFC 2324)
  • 500 Internal Server Error — errore generico lato server
  • 502 Bad Gateway — risposta non valida da un altro server
  • 503 Service Unavailable — servizio temporaneamente non disponibile
  • 504 Gateway Timeout — tempo di risposta scaduto

Si possono utilizzare codici standard e personalizzati, purché rispettino il significato della loro classe. La differenza tra 401 e 403 è la più fraintesa: il primo dice "non so chi sei", il secondo "so chi sei e non puoi".

Attenzione

Ricordate dal capitolo 3 che i codici 500, 502, 503 e 504 sono anche i sintomi tipici dell'overload di un server web. Restituire un 500 per un dato di input sbagliato non è solo impreciso: sposta la colpa dal client al server e rende impossibile distinguere un bug applicativo da un problema di carico quando si leggono i log.

6. Documentare l'API: OpenAPI e Swagger

Una API REST ben progettata deve essere anche ben documentata. La documentazione permette a sviluppatori e utenti di comprendere le risorse disponibili e i loro URI, conoscere i metodi supportati, vedere lo schema di richieste e risposte e conoscere i possibili codici di stato restituiti.

OpenAPI è uno standard per descrivere le API REST in modo strutturato e leggibile da una macchina. È stato creato inizialmente come Swagger Specification ed è ora gestito dalla OpenAPI Initiative (OAI). È un documento JSON o YAML che descrive risorse e path, parametri di input e output, schemi dei dati di richiesta e risposta, codici di stato e autenticazione.

I vantaggi elencati sono sei: documentazione interattiva; generazione automatica di SDK/client in vari linguaggi; creazione di mock server per test rapidi; integrazione in tool come Swagger UI o Postman; comunicazione più chiara tra sviluppatori backend e frontend; possibilità di generare la documentazione a partire dal codice. Swagger UI, in particolare, permette di visualizzare e interagire con le API tramite un'interfaccia web: elenco delle risorse disponibili, possibilità di provare le richieste direttamente dal browser, esempi di input e output generati automaticamente.

La struttura del documento segue cinque blocchi: versione della specifica, informazioni sull'API, definizione dei server, sicurezza, definizione dei path.

openapi: 3.0.0
info:
  title: SmartCity API
  description: API per gestire smart city
  version: 1.0.0

servers:
  - url: https://api.smartcity.example.com/v1

security:
  - bearerAuth: []

paths:
  /cities:
    get:
      summary: Elenca tutte le città
      tags: [Cities]
      responses:
        '200':
          description: Lista delle città
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/City'

Sulla stessa risorsa si descrive poi il post, con il requestBody obbligatorio e le due risposte possibili — 201 per la città creata con successo e 400 per i dati non validi — e infine i components, dove vivono gli schemi di autenticazione e i modelli di dati riutilizzabili:

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    City:
      type: object
      properties:
        id:
          type: string
          example: cesena
        name:
          type: string
          example: Cesena
        population:
          type: integer
          example: 96500
        sensors:
          type: array
          items:
            $ref: '#/components/schemas/Sensor'
    Sensor:
      ...

L'esercizio associato chiede di partire dal file exercises/exercise-01/openapi-film.yaml, definire l'API Filmoteca per gestire dei film e validare il file con Swagger Editor. Un film ha i campi id, title, director, year e genres (un array).

Idea chiave

La sequenza dell'esercitazione non è casuale: prima si scrive la specifica, poi si implementa. Con OpenAPI il contratto dell'API è un artefatto formale che esiste prima del codice, può essere validato, può generare un mock server per il frontend e può generare i client. È l'equivalente, sul confine di rete, di quello che gli schemi Mongoose faranno sul confine della persistenza nel capitolo 7.

7. Express: il primo server

Express si presenta come "Fast, unopinionated, minimalist web framework for Node.js". Permette di creare web server e API REST in modo semplice e veloce, e aggiunge molte funzionalità utili per lo sviluppo web. Le caratteristiche principali sono cinque: semplicità (API intuitive e facili da usare), flessibilità (adatto a esigenze e progetti diversi), middleware (supporto per funzioni che gestiscono richieste e risposte), routing (sistema potente e flessibile), performance (ottimizzato per alte prestazioni).

Le slide dello stack MEAN aggiungono il quadro d'insieme: Express è un web application framework server-side minimale e flessibile che facilita l'uso di Node.js e l'implementazione di API REST, supporta diversi template engine, fornisce routing, middleware e un sistema modulare, e — dettaglio importante — il layer di funzionalità che fornisce non "oscura" le funzionalità di Node.js. È spesso la base per architetture più complesse.

Si installa con npm install express --save (o semplicemente npm install express) dopo aver creato il progetto con npm init. Le modifiche che ne conseguono sono tre: package.json guadagna la dipendenza, compare package-lock.json con l'albero delle dipendenze, e nasce la cartella node_modules con il codice dei moduli.

Esiste anche express-generator, uno strumento che crea velocemente una struttura dell'applicazione: si installa con npm install express-generator e si usa per esempio con express --view=pug myapp.

Exact match e la rotta di fallback

Visitando una rotta non definita — per esempio /asw — la risposta è Cannot GET /asw, perché Express utilizza una logica di exact-match, in ordine di definizione delle rotte. Per gestire il caso si definisce in fondo una rotta che cattura tutto:

app.get('/*thepath', (req, res) => {
    res.setHeader('Content-Type', 'text/plain');
    res.status(404);
    res.send('Page not found ' + req.params.thepath);
});

Le due parole "in ordine di definizione" sono la cosa più importante di questa sezione: la posizione delle rotte nel file è semantica. Una rotta catch-all messa all'inizio intercetta tutto e rende irraggiungibile ogni rotta successiva.

8. Rispondere: JSON, file statici, parametri

Restituire JSON

Dato un file colors.json, per restituirne il contenuto sulla rotta /colors si importano i dati con require e si definisce l'handler. Ci sono due alternative:

const data = require('./colors.json');

// alternativa 1: header esplicito + send
res.header("Content-Type", 'application/json');
res.send(JSON.stringify(data));

// alternativa 2: json fa entrambe le cose
res.json(data);

Con res.json non è necessario specificare il tipo di contenuto: il metodo imposta l'header e serializza da solo. È il motivo per cui nelle API REST si usa praticamente sempre la seconda forma.

Servire file statici

Per restituire il file contacts.html alla rotta /contacts si può usare res.sendFile, che però richiede un path assoluto:

const path = require('path');
app.get('/contacts', (req, res) => {
    res.sendFile(path.join(__dirname, 'contacts.html'));
});

La domanda che l'esercitazione pone subito dopo è la più utile: e quando HTML e CSS sono separati, e la pagina include più file — script, fogli di stile, immagini? Serve una rotta per ogni file statico? No: si definisce una cartella public con dentro tutti i file statici e si servono in blocco con un middleware:

app.use(express.static('public'));

Parametri di rotta e query string

Ci sono due modi di passare un id, e a ciascuno corrisponde un accesso diverso:

Forma dell'URLRotta da definireCome si legge
/user/12345/user/:idreq.params.id
/user?id=12345/userreq.query.id
app.get('/sayhello/:name', (req, res) => {
  res.send("Hello " + req.params.name + "!");
});

La scelta tra i due non è stilistica ma di design, e risponde alle regole della sezione 4: il path parameter identifica una risorsa (fa parte della gerarchia), la query string filtra o impagina una collezione. /films/12 è un film; /films?director=Nolan è un sottoinsieme della collezione dei film.

9. Router e middleware

Il routing gestisce le richieste verso path specifici e permette di organizzare il codice in modo modulare, separando le rotte. Il costrutto è express.Router(), che si comporta come una mini-applicazione esportabile:

const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  res.send('Lista di risorse');
});

router.post('/', (req, res) => {
  res.send('Crea nuova risorsa');
});

router.get('/:id', (req, res) => {
  res.send('Dettagli risorsa ' + req.params.id);
});

module.exports = router;

Middleware

Le funzioni middleware sono blocchi che elaborano richieste e risposte. Possono fare quattro cose: eseguire codice, modificare req e res, terminare la richiesta, oppure passare il controllo al middleware successivo chiamando next().

function logger(req, res, next) {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next(); // passa al middleware/rotta successiva
}

app.use(logger);

Express ne fornisce alcuni già pronti: express.json() per il parsing del JSON nel body delle richieste, express.urlencoded() per i dati di form (x-www-form-urlencoded), express.static() per servire file statici — quest'ultimo lo abbiamo già usato senza chiamarlo per nome.

richiesta GET /films/12 logger next() express.json req.body cors header router exact match, in ordine controller model res.json(...) — la catena finisce qui un middleware che non chiama next() blocca tutto l'ordine di app.use(...) è l'ordine di attraversamento
Tavola 5.3 — La catena dei middleware: ogni anello può leggere, modificare, terminare o passare oltre; l'ordine di registrazione è l'ordine di esecuzione.

10. CORS, validazione, sessioni e JWT

CORS

Cross-Origin Resource Sharing è una sicurezza del browser che impedisce richieste a origini diverse. Per abilitare l'accesso da frontend esterni si usa il pacchetto cors:

const cors = require('cors');
app.use(cors());

Questo è il middleware che serve nel momento in cui, dal capitolo 10 in poi, il frontend Vue gira su una porta diversa da quella del backend Express: due porte sono due origini, e senza CORS il browser blocca la chiamata.

Validazione dei dati

express-validator è un insieme di middleware per la validazione e la sanificazione dei dati nelle richieste HTTP:

const { body, validationResult } = require('express-validator');

app.post('/user', body('email').isEmail(), (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    res.send('Utente valido!');
  }
);

Notate la forma: il validatore è passato come middleware nella definizione della rotta, prima dell'handler, e l'handler si limita a leggere il risultato. E notate il codice di stato: 400 Bad Request, perché l'errore è del client.

Autenticazione e sessioni

Due strade. JWT (pacchetto jsonwebtoken) per l'autenticazione stateless: il token viaggia con ogni richiesta e il server non conserva nulla — coerente con la natura stateless di HTTP vista nel capitolo 3. Cookie e sessioni (express-session) per gestire sessioni utente, cioè per aggiungere stato:

const session = require('express-session');
app.use(session({
  secret: 'segreto',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false } // true se HTTPS
}));
Per l'esame

Il commento // true se HTTPS non è un dettaglio di configurazione: cookie.secure a true impone al browser di inviare il cookie di sessione solo su connessione cifrata. In sviluppo su http://localhost va lasciato false, altrimenti la sessione non funziona; in produzione lasciarlo false significa spedire l'identificativo di sessione in chiaro. Sapere perché quel valore cambia tra sviluppo e produzione è una buona risposta da orale.

11. La struttura routes, controllers, models

Arrivati a questo punto un file unico non regge più, e l'esercitazione impone la buona prassi: separare il codice in moduli. Tre cartelle, tre responsabilità:

CartellaContenuto
routesun file js che gestisce tutte le rotte dell'applicazione
controllersun file js che gestisce la logica di ogni chiamata
modelsun file js che definisce la struttura dei dati (per esempio un modello per i film)

Questo approccio permette di separare la gestione delle rotte dalla logica applicativa. Il flusso delle chiamate è a tre livelli:

// index.js utilizza i router definiti in routes
const myRouter = require('./src/routes/myRouter');
app.use('/example', myRouter);

// i router utilizzano la logica definita nei controllers
const myController = require('../controllers/myController');
router.route('/').get(myController.helloWorld);

// i controllers implementano la logica e usano i models per i dati
const myModel = require('../models/myModel');
exports.helloWorld = (req, res) => { res.send('Hello World!'); }
exports.getAll = (req, res) => { res.json(myModel.getAll()); }
Idea chiave

Riconoscete lo schema: è MVC lato server, quello del capitolo 3, con il Controller come entry-point della richiesta HTTP e il Model che incapsula dati e business logic. Manca la View perché in una API REST la rappresentazione è JSON — che è precisamente la situazione descritta nella sezione "MVC nei moderni web framework": bundle SPA da una parte, API Controller che rispondono JSON dall'altra.

12. Gli esercizi: la Filmoteca

Il filo rosso delle esercitazioni è un archivio di film, che ricompare in forme sempre più complete. La prima versione parte dalla specifica OpenAPI già scritta (exercises/exercise-02/spec.yaml) e chiede di implementare un server Express con cinque rotte:

RottaEffetto
GET /filmsrestituisce la lista di tutti i film
GET /films/:idrestituisce i dettagli di un film specifico, dato l'id
POST /filmsaggiunge un nuovo film (con dati in formato JSON)
PUT /films/:idaggiorna i dati di un film esistente
DELETE /films/:idelimina un film dall'archivio

Ogni film è un oggetto JSON con id, title, director, year e genres; i film sono memorizzati in un array in memoria come database fittizio — al riavvio del server le modifiche vanno perse, e sarà il capitolo 7 a rimediare con MongoDB. Vanno restituiti i corretti codici di stato HTTP e le API vanno testate con Postman o Swagger UI, generando la documentazione dall'OpenAPI.

Gli extra

Attenzione — un trabocchetto vero

GET /films/stats e GET /films/:id hanno la stessa forma. Poiché Express fa exact match in ordine di definizione, se /films/:id è dichiarata per prima catturerà anche /films/stats, trattando la stringa "stats" come un id. La rotta specifica va dichiarata prima di quella parametrica. È lo stesso principio della rotta catch-all che va messa per ultima.

La seconda versione dell'esercizio (esercitazione 03) rifà lo stesso servizio applicando le buone prassi: niente database per la persistenza, gestione dei dati con un array, il file movies.json con i film già presenti, ma il codice separato in routes, controllers e models.

Per l'esame

Tre cose da avere pronte su questo capitolo. Uno: i quattro archetipi, con la differenza collection/store spiegata in termini di chi sceglie l'URI. Due: la matrice verbo × risorsa, sapendo motivare la casella vuota e sapendo dire quale status code accompagna ciascuna operazione (200, 201 dopo una creazione, 204 dopo una cancellazione, 400 per dati non validi, 404 per risorsa inesistente). Tre: che cos'è un middleware e cosa può fare, con l'ordine di registrazione come parte della semantica.

Verifica le tue conoscenze

Quali sono i quattro archetipi di risorsa di una API REST?

Document: simile a un'istanza di oggetto o a un record, la cui rappresentazione include valori e collegamenti ad altre risorse; è l'archetipo di base ed è il candidato naturale per la risorsa radice. Collection: repository di risorse gestito dal server, che decide se creare le risorse proposte dai client e ne sceglie gli URI. Store: repository di risorse gestito dal client, che inserisce, recupera ed elimina risorse su URI scelti da lui; non crea risorse né genera URI da solo. Controller: modella un concetto procedurale, cioè funzioni eseguibili con parametri e valori di ritorno, per azioni che non si mappano sui metodi CRUD standard; compare come ultimo segmento del percorso.

Perché POST su un documento non è applicabile?

Perché POST significa "aggiungi un elemento dentro questa risorsa", e ha senso solo verso una collezione, che è il contenitore incaricato di creare la nuova risorsa e di assegnarle un URI. Un documento come /cities/cesena è già una risorsa singola e identificata: non ha una collezione interna a cui aggiungere qualcosa. Le operazioni sensate su di esso sono GET (leggi), PUT (aggiorna) e DELETE (cancella). Se serve un'azione ulteriore, si modella come risorsa controller, per esempio POST /alerts/245743/resend.

Quando si usa un path parameter e quando una query string?

Il path parameter identifica una risorsa e fa parte della gerarchia: /user/:id letto con req.params.id. La query string serve a filtrare collection o store e a impaginare i risultati: GET /users?role=admin, GET /users?pageSize=25&pageStartIndex=50, letti con req.query. La regola pratica: se togliendo quel pezzo di URL si ottiene ancora una risorsa sensata (la collezione), allora è una query; se si ottiene una cosa diversa, è parte del percorso.

Elencate quattro antipattern negli URI e dite cosa sbagliano.

GET /deleteUser?id=1234 e GET /deleteUser/1234: mettono un verbo CRUD nell'URI e usano GET per un'operazione che modifica lo stato, cosa che rompe la semantica del metodo. DELETE /deleteUser/1234: il metodo è corretto ma il verbo nell'URI è ridondante. POST /users/1234/delete: maschera da risorsa controller un'operazione per cui esiste già il metodo standard. La forma corretta è una sola: DELETE /users/1234.

Che cos'è OpenAPI e quali vantaggi porta?

È uno standard per descrivere le API REST in modo strutturato e leggibile da una macchina, nato come Swagger Specification e oggi gestito dalla OpenAPI Initiative. Il documento, in JSON o YAML, descrive risorse e path, parametri di input e output, schemi di richieste e risposte, codici di stato e autenticazione. I vantaggi sono la documentazione interattiva, la generazione automatica di SDK e client, la creazione di mock server per test rapidi, l'integrazione con Swagger UI e Postman, una comunicazione più chiara tra backend e frontend, e la possibilità di generare la documentazione dal codice.

Che cos'è un middleware in Express e cosa può fare?

È una funzione che elabora richieste e risposte lungo una catena, con firma (req, res, next). Può eseguire codice, modificare req e res, terminare la richiesta inviando una risposta, oppure passare il controllo al middleware successivo chiamando next(). Un middleware che non chiama next() e non risponde blocca la richiesta. Express fornisce middleware built-in: express.json() per il body JSON, express.urlencoded() per i dati di form, express.static() per i file statici.

Perché l'ordine di definizione delle rotte è importante?

Perché Express usa una logica di exact match, in ordine di definizione: la prima rotta che corrisponde vince. Ne derivano due conseguenze pratiche. La rotta catch-all per il 404 (app.get('/*thepath', …)) va messa in fondo, altrimenti intercetta tutto. E una rotta specifica come /films/stats va dichiarata prima della rotta parametrica /films/:id, altrimenti la seconda cattura anche la prima trattando "stats" come un id.

Che differenza c'è tra res.send e res.json?

Con res.send bisogna impostare esplicitamente l'header e serializzare: res.header("Content-Type", 'application/json') seguito da res.send(JSON.stringify(data)). Con res.json(data) il metodo fa entrambe le cose da solo: imposta il tipo di contenuto e serializza l'oggetto. Nelle API REST si usa la seconda forma, perché elimina due possibili errori (dimenticare l'header o serializzare male).

A cosa serve CORS e quando ci si scontra con esso in questo corso?

Cross-Origin Resource Sharing è un meccanismo di sicurezza del browser che impedisce a una pagina di fare richieste verso origini diverse dalla propria. Ci si scontra con esso appena il frontend e il backend girano su porte diverse — la situazione normale quando il progetto Vue è servito dal dev server di Vite e le API stanno su Express: due porte sono due origini. Si abilita l'accesso con il pacchetto cors e la riga app.use(cors()).

Come si organizza un progetto Express secondo le buone prassi dell'esercitazione, e a quale pattern corrisponde?

In tre cartelle: routes con il file che gestisce tutte le rotte, controllers con la logica di ogni chiamata, models con la definizione della struttura dei dati. Il flusso è a tre livelli: index.js monta i router con app.use('/example', myRouter), i router delegano ai controller con router.route('/').get(myController.helloWorld), i controller usano i model per i dati. Corrisponde al pattern MVC lato server, con il Controller come entry-point della richiesta HTTP e il Model che incapsula dati e business logic; la View manca perché la rappresentazione è JSON, esattamente lo scenario "SPA più API Controller" descritto nel capitolo 3.

Quale status code restituireste per: creazione riuscita, cancellazione riuscita, dati non validi, risorsa inesistente?

Creazione riuscita: 201 Created, tipicamente in risposta a un POST su una collezione. Cancellazione riuscita: 204 No Content, operazione avvenuta senza restituire corpo. Dati non validi: 400 Bad Request, classe 4xx perché la colpa è del client — è il codice che express-validator usa nell'esempio. Risorsa inesistente: 404 Not Found. Da evitare la scorciatoia di rispondere 500 a un input sbagliato: la classe 5xx indica un errore del server, ed è anche il sintomo tipico dell'overload.