L08 - Connettere frontend e backend | Proxy Vite, API Layer, Gestione Errori - Prof. Giagnotti
PROF GIAGNOTTI · TORNA A → · GUIDE
React + Vite + Node.js · L08 di 10 · Fullstack
A cura di Prof. Giagnotti — Sviluppo Web Full-Stack

L08 - Connettere frontend e backend

Hai frontend React e backend Express separati. In questa lezione li colleghi con il proxy di Vite, costruisci un layer API pulito, gestisci form POST, DELETE e stati di loading/errore come in produzione. Niente più chiamate a localhost:3001 nel codice: solo /api/…

📅 28 aprile 2026 ⏱ 22 min lettura ⚙️ Advanced · Full-Stack 🔬 Lab L08 · Fullstack

1. Obiettivi di apprendimento

✓Capire perché Vite :5173 e Express :3001 sono due origini diverse e come la Same-Origin Policy blocca fetch senza proxy o CORS.
✓Configurare server.proxy in vite.config.js con /api → localhost:3001 e changeOrigin, riavviando il dev server correttamente.
✓Creare un layer API dedicato (api/notesApi.js) con GET, POST, DELETE che usa percorsi relativi e controlla sempre res.ok.
✓Costruire useNotes e NoteForm con stati loading, submitting, error/success, validazione client e aggiornamento ottimistico della lista.
✓Gestire errori fullstack con middleware Express a 4 parametri, avvio parallelo con concurrently e checklist di verifica dell'integrazione.

2. Il problema: due origini diverse e CORS

Quando sviluppi in locale, Vite gira su http://localhost:5173 e il server Express su http://localhost:3001. Per il browser sono due origini distinte: host diverso, porta diversa. Il browser applica la Same-Origin Policy e blocca le richieste cross-origin se il server non invia header CORS corretti.

Browser localhost:5173 fetch('/api/notes') ⛔ CORS Error localhost:3001 senza proxy / senza header CORS Browser localhost:5173 Vite Proxy localhost:5173 Express localhost:3001 con proxy Vite: stessa origine, nessun problema CORS
StrategiaCome funzionaQuando usarla
Proxy ViteVite inoltra /api/* al backend. Per il browser è tutto localhost:5173✅ Sviluppo locale — scelta corretta
CORS headers lato serverExpress risponde con Access-Control-Allow-Origin✅ Produzione (domini diversi)
📘 Concetto chiave — Same-Origin Policy

La Same-Origin Policy impedisce a uno script su origine A di leggere la risposta di una richiesta verso origine B, a meno che B non autorizzi esplicitamente. Origine = protocollo + host + porta. Per questo 5173 e 3001 sono diverse.

💡 Consiglio del docente

Non mettere mai http://localhost:3001 hardcoded nei componenti. Usa sempre percorsi relativi /api/.... Domani quando cambi porta o vai in produzione, cambi una riga sola nella config, non 20 file.

3. Configurare il proxy in vite.config.js

Apri vite.config.js nella root del frontend e aggiungi server.proxy:

frontend/vite.config.js
import { defineConfig } from 'vite' import react from '@vitejs/plugin-react'export default defineConfig({ plugins: [react()],server: { proxy: { // Tutte le richieste che iniziano con /api // vengono inoltrate al backend Express '/api': { target: 'http://localhost:3001', changeOrigin: true, } } } })

Come funziona il proxy, passo per passo

  1. Il browser fa fetch('/api/notes') verso localhost:5173/api/notes
  2. Vite intercetta perché il path inizia con /api
  3. Vite re-invia a http://localhost:3001/api/notes
  4. Express risponde a Vite, Vite re-invia al browser
  5. Per il browser tutto sembra venire da localhost:5173 → nessun CORS

changeOrigin: true sostituisce l'header Host da localhost:5173 a localhost:3001, evitando problemi con virtual-host.

✅ Riavvio obbligatorio

Dopo aver modificato vite.config.js devi riavviare con Ctrl+C e npm run dev. HMR non rileva modifiche alla config Vite: serve restart completo.

⚠️ Errore comune — Proxy non funziona

Se apri /api/notes e vedi l'HTML di Vite invece del JSON, hai dimenticato di avviare il backend o hai scritto target sbagliato. Controlla che Express sia su 3001 e che il path inizi proprio con /api anche lato Express (app.use('/api/notes', ...)).

4. Struttura del progetto fullstack

Con L07 il progetto è già diviso in due cartelle. Questa è la struttura che usiamo in L08:

Struttura cartelle
devnotes/ ├── backend/ # server Express (L07) │ ├── server.js │ ├── routes/ │ │ └── notes.js │ └── package.json │ └── frontend/ # React + Vite (L01→L07) ├── vite.config.js # ← modifichiamo questo ├── src/ │ ├── api/ │ │ └── notesApi.js # ← nuovo: chiamate al backend │ ├── components/ │ │ ├── NoteCard.jsx │ │ ├── NoteList.jsx │ │ └── NoteForm.jsx # ← nuovo: form POST │ ├── hooks/ │ │ └── useNotes.js # ← nuovo: custom hook │ └── App.jsx # ← aggiornato └── package.json

Separare la logica in api/notesApi.js è best practice: se cambiano URL o header, modifichi un file solo.

💡 Consiglio del docente — Organizzazione

Crea subito la cartella src/api/ anche se il progetto è piccolo. Quando aggiungerai auth, upload, paginazione, avrai già il posto giusto. Un file = una risorsa (notes, auth, users). Evita il file unico api.js da 500 righe.

5. Il modulo API: notesApi.js e il controllo res.ok

Centralizza tutte le chiamate. Usa percorsi relativi grazie al proxy.

frontend/src/api/notesApi.js
const API_BASE = '/api/notes'export async function fetchNotes() { const res = await fetch(API_BASE) if (!res.ok) throw new Error(`Errore GET note: ${res.status}`) return res.json() }export async function createNote(nota) { const res = await fetch(API_BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(nota) }) if (!res.ok) { const errore = await res.json().catch(() => ({})) throw new Error(errore.messaggio || `Errore POST: ${res.status}`) } return res.json() }export async function deleteNote(id) { const res = await fetch(`${API_BASE}/${id}`, { method: 'DELETE' }) if (!res.ok) throw new Error(`Errore DELETE ${id}: ${res.status}`) }
📘 Regola d'oro di fetch

fetch() NON lancia eccezione per 404/500. Risolve la promise con ok: false. Devi sempre controllare: if (!res.ok) throw new Error(...). Altrimenti l'errore passa in silenzio e rompi la UI più avanti.

6. Custom hook useNotes - incapsulare la logica

Il hook espone dati, stati UI e operazioni. I componenti non sanno nulla di fetch.

frontend/src/hooks/useNotes.js
import { useState, useEffect, useCallback } from 'react' import { fetchNotes, createNote, deleteNote } from '../api/notesApi'export function useNotes() { const [notes, setNotes] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null)useEffect(() => { loadNotes() }, [])async function loadNotes() { try { setLoading(true) setError(null) const data = await fetchNotes() setNotes(data) } catch (err) { setError(err.message) } finally { setLoading(false) } }const addNote = useCallback(async (formData) => { const nuovaNota = await createNote(formData) setNotes(prev => [nuovaNota, ...prev]) }, [])const removeNote = useCallback(async (id) => { await deleteNote(id) setNotes(prev => prev.filter(n => n.id !== id)) }, [])return { notes, loading, error, addNote, removeNote, reload: loadNotes } }

7. NoteForm - form con POST e feedback UX

Gestisce stato locale, validazione prima di sprecare una richiesta, submitting, messaggi di successo/errore e reset dopo invio.

frontend/src/components/NoteForm.jsx
import { useState } from 'react'const TAGS = ['React', 'JavaScript', 'CSS', 'Node.js', 'Altro']export default function NoteForm({ onAdd }) { const [form, setForm] = useState({ titolo: '', contenuto: '', tag: 'React' }) const [submitting, setSubmitting] = useState(false) const [errMsg, setErrMsg] = useState('') const [success, setSuccess] = useState(false)function handleChange(e) { setForm(prev => ({ ...prev, [e.target.name]: e.target.value })) }async function handleSubmit(e) { e.preventDefault() if (!form.titolo.trim()) { setErrMsg('Il titolo è obbligatorio') return } try { setSubmitting(true) setErrMsg('') await onAdd(form) setForm({ titolo: '', contenuto: '', tag: 'React' }) setSuccess(true) setTimeout(() => setSuccess(false), 2500) } catch (err) { setErrMsg(err.message) } finally { setSubmitting(false) } }return ( <form onSubmit={handleSubmit}> <input name="titolo" value={form.titolo} onChange={handleChange} placeholder="Titolo…" /> <textarea name="contenuto" value={form.contenuto} onChange={handleChange} rows={4} placeholder="Contenuto…" /> <select name="tag" value={form.tag} onChange={handleChange}> {TAGS.map(t => <option key={t} value={t}>{t}</option>)} </select> {errMsg && <p>⚠ {errMsg}</p>} {success && <p>✓ Nota salvata!</p>} <button disabled={submitting}>{submitting ? 'Salvataggio…' : 'Aggiungi nota'}</button> </form> ) }
💡 Consiglio pratico — UX del form

Disabilita sempre il bottone durante submitting per evitare doppi click. Mostra validazione client-side senza andare in rete, poi gestisci errore server. Reset del form solo dopo successo, mai prima. Così l'utente non perde testo se la rete fallisce.

8. Gestione errori fullstack lato client e server

In fullstack gli errori nascono in 4 punti diversi:

① Validazione client-side prima di fetch() — gratis, immediato ② Rete / timeout server non raggiungibile ③ Risposta HTTP 4xx/5xx 400, 404, 500 — res.ok = false ④ Parsing JSON fallito

Middleware errori Express - 4 parametri

Va messo dopo tutte le route in server.js:

backend/server.js — middleware errori
// ... route già definite ...app.use((req, res) => { res.status(404).json({ messaggio: 'Risorsa non trovata' }) })app.use((err, req, res, next) => { console.error('[SERVER ERROR]', err.stack) res.status(err.status || 500).json({ messaggio: err.message || 'Errore interno del server' }) })
⚠️ Dettagli errori in produzione

In produzione non inviare mai err.stack al client: espone dettagli interni. Logga lato server, rispondi con messaggio generico. Usa NODE_ENV per distinguere sviluppo e produzione.

Gestire loading ed errore in React

frontend/src/App.jsx
import { useNotes } from './hooks/useNotes' import NoteList from './components/NoteList' import NoteForm from './components/NoteForm'export default function App() { const { notes, loading, error, addNote, removeNote, reload } = useNotes()if (loading) return <div>Caricamento note…</div>if (error) return ( <div> <p>⚠ {error}</p> <button onClick={reload}>Riprova</button> </div> )return ( <div> <header><h1>DevNotes</h1><span>{notes.length} note</span></header> <NoteForm onAdd={addNote} /> <NoteList notes={notes} onElimina={removeNote} /> </div> ) }

9. Avviare frontend e backend insieme + verifica e riepilogo

Opzione A — Due terminali separati

Terminale
# Terminale 1 — backend cd devnotes/backend node server.js# Terminale 2 — frontend cd devnotes/frontend npm run dev

Opzione B — Script concurrently nella root

Root mono-repo
# nella cartella devnotes/ (root) npm init -y npm install -D concurrently# package.json { "scripts": { "dev": "concurrently \"npm run dev --prefix frontend\" \"node backend/server.js\"", "dev:watch": "concurrently \"npm run dev --prefix frontend\" \"npx nodemon backend/server.js\"" } }npm run dev
💡 Nodemon — riavvio automatico

Installa nodemon nel backend (npm install -D nodemon) e usa "start": "nodemon server.js". Come HMR per il frontend, ma sul server: ogni modifica .js riavvia automaticamente.

Checklist verifica integrazione

VerificaCome testarlaRisultato atteso
Proxy configuratoBackend avviato, apri http://localhost:5173/api/notesJSON dal backend (non 404 Vite)
GET note funzionaNetwork tab in DevToolsRichiesta a /api/notes → 200 OK con array
POST funzionaCompila form e inviaNuova nota appare senza refresh
DELETE funzionaClicca eliminaNota sparisce dalla lista
Errore gestitoFerma backend, carica noteMessaggio errore + bottone Riprova

🔬 Verifica / Esercizio pratico — Lab 08

Attività dal meta-pill giallo

Apri il laboratorio e completa:

  1. Configura proxy /api → localhost:3001 e verifica che /api/notes da Vite ritorni JSON vero.
  2. Crea notesApi.js con controllo res.ok e testalo con backend spento (deve lanciare errore leggibile).
  3. Implementa useNotes con addNote e removeNote ottimistici, poi integra NoteForm con validazione titolo.
✅ Soluzione ragionata

1. Se apri localhost:5173/api/notes e vedi JSON, proxy OK. Altrimenti errore configurazione o backend spento. 2. Con backend spento fetch rifiuta: il catch in loadNotes imposta error e UI mostra bottone Riprova. 3. Validazione trim() prima di POST evita richiesta inutile; setNotes(prev => [nuova, ...prev]) è aggiornamento ottimistico senza ricaricare dal server.

✓Same-Origin Policy — blocca cross-origin; proxy Vite la aggira in sviluppo, in produzione serve CORS su Express.
✓server.proxy /api → 3001 — changeOrigin:true, riavvio obbligatorio dopo modifica vite.config.js.
✓Layer API separato — centralizza fetch, usa percorsi relativi, controlla sempre !res.ok.
✓useNotes + NoteForm — custom hook per stato e CRUD, form con submitting, err/success, validazione client.
✓Middleware errori Express + concurrently — 4 parametri dopo route, JSON strutturato, avvio unico con npm run dev.

📚 Prossimo passo — L09 Autenticazione JWT

Ora che frontend e backend si parlano, è il momento di proteggerli. Nella prossima lezione aggiungerai login, token JWT, bcrypt e protected routes. Porta DevNotes fullstack funzionante con proxy già configurato.