Lezione 6 – API REST

React + Vite + Node.js — Lezione 6 di 10

Lavorare con le API REST

Collega DevNotes a un server reale: fetch, async/await, gestione degli stati di caricamento ed errore, custom hook useFetch.

// obiettivi di apprendimento
Capire come funziona fetch e il modello a Promise di JavaScript
Gestire gli stati loading, data ed error in un componente React
Costruire un custom hook useFetch riutilizzabile
Usare JSONPlaceholder e AbortController per test sicuri
// 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.


Perché i dati vengono da un server

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 formale

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

Promise e async/await — il modello asincrono di JS

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 formale

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:

// 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:

// 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);
  }
}
// attenzione

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.


fetch() in un componente React — il primo tentativo

Il posto naturale dove eseguire una chiamata al caricamento del componente è useEffect. Vediamo la struttura base:

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 mount

  if (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.


Custom hook useFetch

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.

// flusso useFetch
Componente
chiama useFetch(url)
useFetch hook
useState + useEffect
fetch(url)
chiamata HTTP
{ data, loading, error }
ritorna al componente
// 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;

    // AbortController per cancellare la chiamata se il componente viene smontato
    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();

    // cleanup: cancella la richiesta se l'url cambia o il componente si smonta
    return () => controller.abort();
  }, [url]); // riesegui ogni volta che cambia url

  return { data, loading, error };
}

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

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

AbortController — perché è importante

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

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

✓ CON ABORT

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


Chiamate POST — inviare dati al server

Per creare, aggiornare o eliminare risorse, dobbiamo usare metodi diversi da GET e includere un body JSON nella richiesta:

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(); // restituisce la risorsa creata (con il nuovo id)
}

// utilizzo nel componente
const handleSubmit = async (formData) => {
  try {
    const nuovaNota = await creaNota(formData);
    setNotes(prev => [...prev, nuovaNota]);
  } catch (err) {
    setError(err.message);
  }
};

JSONPlaceholder — il tuo server di test

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

Gestione degli stati: 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

Mostra uno skeleton o uno spinner. Non bloccare l’intera UI.

DATA

Mostra i dati ricevuti. Caso normale.

ERROR

Mostra un messaggio chiaro e un pulsante Riprova.

EMPTY

L’array è vuoto. Guida l’utente alla prossima azione.

// 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} />;

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:

// 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' }),
};
// laboratorio pratico — lab-06-api-rest.md

Nel lab integrerai useFetch in DevNotes: cartella hooks/ e services/, LoadingSpinner, ErrorMessage, pagina NoteDetail collegata all’API, esercizi avanzati su useReducer e refetch manuale.

Apri lab-06-api-rest.md su GitHub →
📌 Riepilogo — Punti 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.
  • 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.

Lascia un commento