Capitolo 4 · Sviluppo web con Spring Boot: il corso pratico
Per capire come funziona un'API non ti serve un database: ti basta una lista in memoria, due pazienti finti e un controller.
Quando si impara, il database è il primo ostacolo che si mette tra te e quello che volevi capire. Installazione, configurazione, tabelle, connessioni: tutto lavoro che non c'entra con la domanda di partenza, cioè come un'API riceve una richiesta e risponde.
Cosa fai a quel punto? Lo togli di mezzo. Per questa volta facciamo il contrario di quello che si fa in produzione: niente database, una lista Java dentro il controller, e mettiamo insieme tutto quello che sappiamo su indirizzi, verbi e JSON in un piccolo CRUD (leggi, cerca, aggiungi) per il registro del pronto soccorso.
Il piano: un modello e una lista di pazienti finti
Si parte sempre dal modello, cioè la classe che descrive l'oggetto di cui ti occupi. Qui è un Paziente con quattro campi: id, nome, cognome ed età. Il PazienteController risponde a /api/pazienti (da qui in poi il prefisso api è una consuetudine delle API REST) e tiene dentro una lista con due pazienti già inseriti: Mario Rossi, 36 anni, e Giulia Bianchi, 29. Dati finti, ovviamente: in un sistema vero quelli dei pazienti sono tra i più delicati che esistano.
Il modello è quello del pezzo precedente, con un campo in più:
public class Paziente {
private Integer id;
private String nome;
private String cognome;
private int eta;
// costruttori, getter e setter come nel pezzo precedente
}Hai notato che l'id è un Integer e non un int? Fra poco scoprirai perché è importante. Intanto, la lista dentro il controller:
@RestController
@RequestMapping("api/pazienti")
public class PazienteController {
private final List<Paziente> pazienti = new ArrayList<>(List.of(
new Paziente(1, "Mario", "Rossi", 36),
new Paziente(2, "Giulia", "Bianchi", 29)));
// qui sotto i tre metodi
}Perché pazienti finti? Perché una lista in memoria vive solo finché il server è acceso. Ogni volta che lo fermi e lo riavvii, la lista si svuota. Partire da una lista vuota vorrebbe dire reinserire i dati a mano a ogni prova, e non è il modo migliore di passare il pomeriggio.
Prima versione: un endpoint per ogni azione
La prima idea, quella che viene a tutti, è dare a ogni azione un indirizzo con il suo nome. È lo stile dei vecchi siti procedurali, quelli con indirizzi come /cancellaUtente?id=5 o /getTuttiPazienti:
GET /api/pazienti/listper avere tutti i pazientiGET /api/pazienti/detail/1per averne uno solo, con l'id nell'indirizzo
Funziona, e per capire il meccanismo va benissimo. Ma si porta dietro un problema: devi ricordarti tutti i nomi. Era list, elenco o all? Era detail o dettaglio? Con dieci endpoint diventa un esercizio di memoria, e un ospedale ne ha molti di più.
Seconda versione: l'indirizzo dice cosa, il verbo dice come
Qui si risolve tutto con un'idea che forse ricordi da un altro pezzo: negli URL REST vanno i sostantivi, mai i verbi, e di solito al plurale (pazienti, non paziente). L'azione la dice il verbo HTTP. Risultato:
| Prima | Dopo (stile REST) | Cosa fa |
|---|---|---|
GET /api/pazienti/list | GET /api/pazienti | Restituisce tutti i pazienti |
GET /api/pazienti/detail/1 | GET /api/pazienti/1 | Restituisce il paziente con id 1 |
POST /api/pazienti/insert | POST /api/pazienti | Aggiunge un paziente |
Gli indirizzi si dimezzano, e soprattutto non c'è più niente da ricordare: la risorsa è /api/pazienti, il resto lo decide il verbo (e l'eventuale id in fondo). Un @GetMapping lasciato vuoto prende l'indirizzo del controller così com'è, e un @GetMapping("{id}") aggiunge un segmento.
Lo abbiamo visto nel pezzo precedente: per Spring l'identità di un endpoint è la coppia verbo + indirizzo. È per questo che GET e POST sullo stesso indirizzo convivono senza darsi fastidio.
Nel controller diventano tre metodi, uno per riga della tabella:
@GetMapping
public List<Paziente> elenco() {
return pazienti;
}
@GetMapping("{id}")
public Paziente dettaglio(@PathVariable int id) {
for (int i = 0; i < pazienti.size(); i++) {
if (pazienti.get(i).getId() == id) {
return pazienti.get(i);
}
}
return null;
}
@PostMapping
public Paziente inserisci(@RequestBody Paziente paziente) {
paziente.setId(pazienti.size() + 1);
pazienti.add(paziente);
return paziente;
}Il bug che ti farà impazzire (la prima volta)
Quando scrivi il metodo che cerca un paziente per id, può capitare di dimenticare l'annotazione @PathVariable sul parametro. Il risultato è un 500 Internal Server Error, con un messaggio che parla di un parametro opzionale che non può essere convertito.
Cosa succede? Senza l'annotazione, Spring non sa che quel valore sta nell'indirizzo e lo cerca come parametro normale, che può mancare. Ma un int non può essere "vuoto", e il meccanismo si inceppa. Per questo il 500: non è un errore del client, è il tuo codice che ha un buco. Provando a togliere @PathVariable dal metodo dettaglio e chiamando /api/pazienti/1, il server risponde 500 e nel log compare questo:
IllegalStateException: Optional int parameter 'id' is present but cannot be translated into a null value due to being declared as a primitive type. Consider declaring it as object wrapper for the corresponding primitive type.
Il messaggio, in inglese, dice quasi tutto: il parametro è opzionale, ma essendo un tipo primitivo non può diventare "vuoto". E suggerisce anche la cura alternativa, cioè usare il tipo "contenitore" corrispondente.
La cura è un'annotazione. La lezione è più utile: davanti a un 500 si guarda il codice, non la richiesta.
Cercare e inserire: due metodi di poche righe
Cercare per id. Il metodo scorre la lista, confronta l'id di ogni paziente con quello ricevuto e restituisce il primo che coincide. Se non trova niente restituisce null, e il client riceve una risposta vuota: provando con /api/pazienti/9 il server risponde con status 200 e nessun contenuto. Per un esempio didattico va bene. In un'API vera vorresti qualcosa di più esplicito, perché una risposta vuota non spiega se l'id non esiste o se è successo altro.
Inserire. Il metodo è una POST che legge il paziente dal corpo della richiesta con @RequestBody. Il paziente arriva senza id, perché non tocca al client deciderlo. Il controller glielo assegna prendendo la dimensione della lista e aggiungendo 1, poi aggiunge il paziente e lo restituisce con l'id già compilato. Se nella risposta c'è il nuovo id, l'inserimento è andato a buon fine:
curl -X POST http://localhost:8080/api/pazienti \
-H "Content-Type: application/json" \
-d '{"nome":"Luca","cognome":"Verdi","eta":52}'{"id":3,"nome":"Luca","cognome":"Verdi","eta":52}E adesso la spiegazione dell'Integer. È la stessa lezione del bug di prima, vista dall'altra parte: un int non può essere vuoto, e qui il client l'id non lo manda. Con un int nel modello, Spring Boot 4 rifiuta la POST con un 400, e nel log trovi Cannot map null into type int. Con un Integer, che può essere vuoto, il campo resta a null finché non lo riempie il controller.
Un'ultima precisazione sugli status code. In un'API fatta bene un id inesistente restituirebbe 404 Not Found e una POST andata a buon fine 201 Created. Nel nostro esempio, per semplicità, il server risponde 200 in entrambi i casi, come ho verificato provando.
Quel trucco (dimensione + 1) è un finto auto-incremento, giusto per giocare. Va detto chiaramente: non è così che si fa in un sistema vero. Se un paziente venisse cancellato, un id si ripeterebbe. Qui non cancelliamo niente, quindi non succede.
Per verificare basta rifare la GET sulla lista: il nuovo paziente è lì, insieme agli altri due. È il tuo primo piccolo database in memoria.

Cosa manca a questo CRUD?
Hai notato che abbiamo fatto soltanto due lettere su quattro? C (create, con la POST) e R (read, con le due GET). Restano la U (update, modificare un paziente esistente) e la D (delete, cancellarlo), che nel mondo HTTP corrispondono ai verbi PUT o PATCH e DELETE. Per ora le lasciamo da parte: l'obiettivo di questo esercizio era vedere come si scambiano i dati con il controller, non costruire un gestionale completo.
Il controller sta facendo troppo
Se guardi bene il PazienteController che hai scritto, noti che dentro ci sono tre mestieri diversi:
- Smistare le richieste e gli indirizzi: è il lavoro del controller.
- Cercare un paziente, assegnare gli id, applicare le regole: è la logica applicativa, che spetta al service.
- Tenere la lista, cioè conservare i dati: è la persistenza, che spetta al repository.
Assegnare un id e cercare un paziente sono i primi, piccolissimi esempi di quelle che si chiamano regole di dominio, cioè le regole del problema reale che il software risolve. Più avanti ne arriveranno di vere (per esempio sulle priorità del pronto soccorso), e il posto giusto per tenerle non è certo il controller. Per ora il controller fa tutto da solo, ed è esattamente la strada che porta ai controller enormi che nessuno vuole toccare, i cosiddetti God Object. Per un esempio didattico va bene, in produzione no.
Conclusione
Con un modello, una lista e tre endpoint hai in mano il meccanismo di fondo di una REST API: un indirizzo che dice cosa, un verbo che dice come, un JSON che viaggia in entrambe le direzioni.
Nel prossimo pezzo togliamo al controller i due mestieri che non sono suoi, e scopriamo perché Spring Boot ha un modo tutto suo di decidere chi costruisce gli oggetti.

Dario Amato
Sviluppatore software
Informatico con 15 anni di esperienza nello sviluppo software, guidato da una forte curiosità per le nuove frontiere dell'AI e del Web Marketing.
