Lezione 6 — Lavorare con le API REST
Collega DevNotes a un server reale: fetch, async/await, gestione degli stati di caricamento ed errore, custom hook useFetch, AbortController e struttura dei servizi API.
In questa lezione rendiamo DevNotes capace di parlare con il mondo esterno. Inizieremo consumando l'API pubblica JSONPlaceholder per simulare il caricamento delle note da un server, poi costruiremo il custom hook useFetch che useremo in tutta l'app. Nella prossima lezione scriveremo noi stessi il backend — per ora ci concentriamo sul lato React.
1. Perché i dati vengono da un server — API REST
Fino alla lezione 5, le note di DevNotes vivevano solo in useState: ogni volta che ricaricavi il browser, tutto spariva. Nelle applicazioni reali i dati devono persistere e spesso vengono prodotti o modificati da più utenti contemporaneamente. La soluzione è delegare la persistenza a un server e comunicare con lui tramite una API REST.
REST (Representational State Transfer) è uno stile architetturale per API HTTP. Ogni risorsa ha un URL univoco; le operazioni si esprimono con i metodi HTTP (GET, POST, PUT, DELETE); il server risponde con JSON. Il client non deve sapere nulla dello stato interno del server tra una richiesta e l'altra (stateless).
| Metodo HTTP | Operazione CRUD | Esempio su /notes |
|---|---|---|
| GET | Read — leggi | GET /notes → lista note; GET /notes/3 → nota con id 3 |
| POST | Create — crea | POST /notes con body JSON → nuova nota |
| PUT | Update — sostituisci | PUT /notes/3 con body JSON → sostituisce nota 3 |
| PATCH | Update — modifica parziale | PATCH /notes/3 con {"title":"..."} → aggiorna solo il titolo |
| DELETE | Delete — elimina | DELETE /notes/3 → rimuove nota 3 |
Quando disegno REST alla lavagna insisto su una parola: stateless. Il server non ricorda chi sei tra due richieste. L'autenticazione viaggia ad ogni chiamata. Se provi a salvare dati in RAM sul server aspettandoti di ritrovarli alla chiamata successiva, stai rompendo REST.
2. Promise e async/await — il modello asincrono di JavaScript
Una chiamata HTTP non è istantanea. JavaScript è single-threaded: non può bloccarsi ad aspettare la risposta del server. Il meccanismo che usa sono le Promise.
Una Promise è un oggetto che rappresenta il risultato futuro di un'operazione asincrona. Può trovarsi in tre stati: pending (in attesa), fulfilled (completata con successo) o rejected (fallita).
Ci sono due sintassi per lavorarci. La prima usa la catena di metodi:
La seconda, preferita nei componenti React, usa la sintassi async/await:
fetch rigetta la Promise solo in caso di errore di rete (nessuna connessione, DNS fallito). Se il server risponde con un codice HTTP come 404 o 500, la Promise viene comunque fulfilled. Devi controllare esplicitamente response.ok e lanciare tu un Error.
3. fetch() in un componente React — il primo tentativo
Il posto naturale dove eseguire una chiamata al caricamento del componente è useEffect. Vediamo la struttura base che useremo in DevNotes:
Funziona — ma se questo pattern viene ripetuto in cinque componenti diversi, hai duplicato la stessa logica cinque volte. Soluzione: estrarlo in un custom hook.
Vedo spesso un useEffect senza array di dipendenze che rifetcha all'infinito. Se metti fetch direttamente nel corpo del componente, ad ogni setState rifai la chiamata. L'array vuoto [] è intenzionale: vuoi una sola chiamata al mount. Quando aggiungeremo url come dipendenza nel custom hook, invece, vorrai che si ri-esegua ad ogni cambio di url.
4. Custom hook useFetch — logica riutilizzabile
Un custom hook è semplicemente una funzione JavaScript il cui nome inizia con use e che può chiamare altri hook al suo interno. Non è un componente: non renderizza nulla, si limita a incapsulare logica riutilizzabile.
Ora qualsiasi componente che ha bisogno di dati può usarlo con una sola riga:
5. AbortController — evitare memory leak
Immagina che l'utente navighi rapidamente da una pagina a un'altra. Il componente viene smontato prima che la risposta arrivi. Senza AbortController, quando la risposta arriva React prova comunque ad aggiornare lo stato di un componente che non esiste più → memory leak e warning in console.
Warning e leak
Il componente si smonta → la risposta arriva → React prova a fare setState su un componente smontato → warning + potenziale memory leak.
Cleanup sicuro
Il cleanup di useEffect chiama controller.abort() → la richiesta viene annullata → il catch intercetta AbortError e lo ignora.
Signal nel fetch
Passa sempre { signal: controller.signal } alle opzioni di fetch e gestisci il caso err.name === 'AbortError' separatamente.
In useEffect la funzione che ritorni è il cleanup. Se dimentichi return () => controller.abort(), la richiesta non viene mai cancellata. Nei log del browser vedrai "Can't perform a React state update on an unmounted component".
Per vedere l'effetto dell'AbortController, apri Chrome DevTools → Network → imposta Throttling su Slow 3G. Naviga velocemente tra due pagine di DevNotes. Senza abort vedrai il warning, con abort tutto resta pulito. È un esercizio che faccio sempre nel live coding.
6. POST, JSONPlaceholder e struttura servizi API
Per creare, aggiornare o eliminare risorse, dobbiamo usare metodi diversi da GET e includere un body JSON nella richiesta:
JSONPlaceholder è un'API pubblica gratuita perfetta per testare il frontend prima che il backend sia pronto. Espone risorse fittizie come /posts, /users, /todos. Le chiamate di scrittura (POST/PUT/DELETE) sembrano funzionare ma non persistono dati reali.
| Endpoint | Risposta | Utilizzo in DevNotes |
|---|---|---|
GET /posts | Array di 100 post | Simula lista note |
GET /posts/:id | Singolo post | Dettaglio nota |
POST /posts | Post creato (id: 101) | Crea nota |
PUT /posts/:id | Post aggiornato | Modifica nota |
DELETE /posts/:id | {} | Elimina nota |
Struttura file consigliata per i servizi API
Con la crescita dell'app conviene centralizzare le URL e la logica di fetch in un file servizio separato, così il componente non sa nulla dell'endpoint concreto:
Non hardcodare mai l'URL dell'API nei componenti. Metti BASE_URL in un file service o meglio in una variabile d'ambiente Vite: import.meta.env.VITE_API_URL. Così in sviluppo punti a JSONPlaceholder e in produzione al tuo backend su Render senza cambiare codice.
7. Gestione stati UI: loading, error, empty, data
Una UI ben progettata gestisce quattro stati distinti dopo una chiamata API. Non lasciare l'utente a fissare una pagina bianca:
Attesa
Mostra uno skeleton o uno spinner. Non bloccare l'intera UI con un overlay.
Dati ricevuti
Mostra i dati ricevuti. Caso normale, lista note.
Errore di rete
Mostra un messaggio chiaro e un pulsante Riprova che riesegue fetch.
Non dimenticare il quarto stato: empty. Se l'array è vuoto, non mostrare una pagina bianca. Guida l'utente: "Nessuna nota presente — Crea la prima nota" con CTA. È un dettaglio che differenzia un'app amatoriale da una curata.
8. Verifica / Esercizio pratico
Metti alla prova quanto hai imparato. Apri il lab ufficiale e completa l'esercizio guidato.
🧪 Laboratorio pratico
Trovi tutti gli esercizi passo-passo, soluzioni intermedie e checklist di deploy nel file lab-06-api-rest.md su GitHub. Clona il repo, segui i task e confronta il risultato con la soluzione proposta a fine file.
Apri lab-06-api-rest.md ↗9. Riepilogo e concetti chiave
- fetch è nativo nel browser e restituisce una Promise — usa sempre response.ok per rilevare errori HTTP.
- async/await rende il codice asincrono leggibile come codice sincrono; usalo sempre in coppia con try/catch.
- Il pattern loading / data / error va sempre gestito esplicitamente — non lasciare l'UI in uno stato indefinito, aggiungi anche empty.
- Il custom hook useFetch incapsula fetch + useState + useEffect ed evita la duplicazione del codice.
- AbortController è essenziale nel cleanup di useEffect per evitare memory leak quando il componente si smonta.
- Il file servizio API centralizza URL e logica di fetch separandola dalla logica dei componenti.
- JSONPlaceholder simula un backend reale per testare tutto prima di scrivere Node + Express nella L07.
