L06 - Lavorare con le API REST | Prof. Giagnotti
PROF GIAGNOTTI · TORNA A → · GUIDE
📘 GUIDE · React + Vite · Lezione 6 di 10
Docente: Prof. Giagnotti · Sviluppo Web Moderno

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.

⏱ ~22 min lettura 🎯 Livello: Intermedio 💻 Prerequisiti: L05 Routing, useEffect, useState ⚗️ Lab Pratico — lab-06-api-rest.md ↗ 🧩 Serie: DevNotes
✓Capire come funziona fetch e il modello a Promise di JavaScript
✓Gestire gli stati loading, data ed error in un componente React con useEffect
✓Costruire un custom hook useFetch riutilizzabile in tutta l'app
✓Usare JSONPlaceholder e AbortController per test sicuri senza memory leak
✓Centralizzare le chiamate in un file servizio e distinguere GET, POST, PUT, DELETE
🚀 Cosa costruiremo

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.

📌 Definizione da ricordare — 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 HTTPOperazione CRUDEsempio su /notes
GETRead — leggiGET /notes → lista note; GET /notes/3 → nota con id 3
POSTCreate — creaPOST /notes con body JSON → nuova nota
PUTUpdate — sostituisciPUT /notes/3 con body JSON → sostituisce nota 3
PATCHUpdate — modifica parzialePATCH /notes/3 con {"title":"..."} → aggiorna solo il titolo
DELETEDelete — eliminaDELETE /notes/3 → rimuove nota 3
💬 Tip del docente — stateless

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.

📌 Definizione — 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:

JS · Catena .then() / .catch()
// Sintassi .then() / .catch() — più verbosa fetch('https://jsonplaceholder.typicode.com/posts/1') .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error(error));

La seconda, preferita nei componenti React, usa la sintassi async/await:

JS · async/await leggibile
// Sintassi async/await — leggibile come codice sincrono async function caricaDato() { try { const response = await fetch('https://jsonplaceholder.typicode.com/posts/1'); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const data = await response.json(); console.log(data); } catch (error) { console.error('Errore:', error.message); } }
⚠️ Errore comune — fetch non fallisce su 404

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:

React · useEffect + fetch
import { useState, useEffect } from 'react';function NoteList() { const [notes, setNotes] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);useEffect(() => { const fetchNotes = async () => { try { setLoading(true); const res = await fetch('https://jsonplaceholder.typicode.com/posts'); if (!res.ok) throw new Error(`Errore ${res.status}`); const data = await res.json(); setNotes(data); } catch (err) { setError(err.message); } finally { setLoading(false); } };fetchNotes(); }, []); // array vuoto = esegui solo al mountif (loading) return <p>Caricamento...</p>; if (error) return <p>Errore: {error}</p>;return ( <ul> {notes.map(note => ( <li key={note.id}>{note.title}</li> ))} </ul> ); }

Funziona — ma se questo pattern viene ripetuto in cinque componenti diversi, hai duplicato la stessa logica cinque volte. Soluzione: estrarlo in un custom hook.

💬 Tip del docente — dipendenze di useEffect

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.

Componente chiama useFetch(url) useFetch hook useState + useEffect fetch(url) chiamata HTTP { data, loading, error } ritorna al componente
JS · src/hooks/useFetch.js
// src/hooks/useFetch.js import { useState, useEffect } from 'react';export function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);useEffect(() => { if (!url) return; const controller = new AbortController();const fetchData = async () => { setLoading(true); setError(null); try { const res = await fetch(url, { signal: controller.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); setData(json); } catch (err) { if (err.name !== 'AbortError') setError(err.message); } finally { setLoading(false); } };fetchData(); return () => controller.abort(); }, [url]);return { data, loading, error }; }

Ora qualsiasi componente che ha bisogno di dati può usarlo con una sola riga:

JS · uso del hook
const { data: notes, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');

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.

❌ SENZA ABORT

Warning e leak

Il componente si smonta → la risposta arriva → React prova a fare setState su un componente smontato → warning + potenziale memory leak.

✅ CON ABORT

Cleanup sicuro

Il cleanup di useEffect chiama controller.abort() → la richiesta viene annullata → il catch intercetta AbortError e lo ignora.

🔁 PATTERN

Signal nel fetch

Passa sempre { signal: controller.signal } alle opzioni di fetch e gestisci il caso err.name === 'AbortError' separatamente.

⚠️ Errore comune — dimenticare il return cleanup

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".

💬 Tip del docente — testare l'abort

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:

JS · Chiamata POST
async function creaNota(nota) { const res = await fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(nota), });if (!res.ok) throw new Error(`Errore ${res.status}`); return res.json(); }const handleSubmit = async (formData) => { try { const nuovaNota = await creaNota(formData); setNotes(prev => [...prev, nuovaNota]); } catch (err) { setError(err.message); } };

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.

EndpointRispostaUtilizzo in DevNotes
GET /postsArray di 100 postSimula lista note
GET /posts/:idSingolo postDettaglio nota
POST /postsPost creato (id: 101)Crea nota
PUT /posts/:idPost aggiornatoModifica 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:

JS · src/services/notesApi.js
// src/services/notesApi.js const BASE_URL = 'https://jsonplaceholder.typicode.com';export const notesApi = { getAll: () => fetch(`${BASE_URL}/posts`).then(r => r.json()), getById: (id) => fetch(`${BASE_URL}/posts/${id}`).then(r => r.json()), create: (data) => fetch(`${BASE_URL}/posts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }).then(r => r.json()), delete: (id) => fetch(`${BASE_URL}/posts/${id}`, { method: 'DELETE' }), };
💬 Tip del docente — BASE_URL in env

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:

⏳ LOADING

Attesa

Mostra uno skeleton o uno spinner. Non bloccare l'intera UI con un overlay.

✅ DATA

Dati ricevuti

Mostra i dati ricevuti. Caso normale, lista note.

❌ ERROR

Errore di rete

Mostra un messaggio chiaro e un pulsante Riprova che riesegue fetch.

React · Pattern completo 4 stati
// pattern completo nel componente const { data: notes, loading, error } = useFetch(API_URL);if (loading) return <LoadingSpinner />; if (error) return <ErrorMessage message={error} />; if (!notes?.length) return <EmptyState />;return <NoteList notes={notes} />;
🧩 Empty state utile

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.

Checklist · Lab 06
✅ Task 1: Crea cartella src/hooks/ e implementa useFetch.js con AbortController ✅ Task 2: Crea src/services/notesApi.js con BASE_URL e metodi getAll/getById/create/delete ✅ Task 3: Integra useFetch in NoteList con LoadingSpinner, ErrorMessage, EmptyState ✅ Task 4: Aggiungi pagina NoteDetail che carica /posts/:id con useParams ✅ Task 5 (avanzato): aggiungi refetch manuale e useReducer per gestire retry

🧪 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 ↗
  • 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.