Connettere frontend e backend
Hai il frontend React e il backend Express: finora vivevano in mondi separati. In questa lezione li metti in comunicazione usando il proxy integrato di Vite, costruisci form che parlano col server e gestisci gli errori in modo professionale.
/api/….1. Il problema: due origini diverse
Quando sviluppi in locale, Vite gira su http://localhost:5173 e il tuo
server Express su http://localhost:3001. Da un punto di vista del browser,
sono due origini distinte: host diverso, porta diversa.
Il browser applica la Same-Origin Policy e blocca le richieste cross-origin
a meno che il server non includa gli header CORS corretti.
localhost:5173
localhost:3001
localhost:5173
localhost:5173
localhost:3001
Hai due strategie per risolvere il problema:
| Strategia | Come funziona | Quando usarla |
|---|---|---|
| Proxy Vite | Vite inoltra le richieste /api/* al backend. Per il browser è tutto localhost:5173 | ✅ Sviluppo locale — è la scelta corretta |
| CORS headers lato server | Express risponde con Access-Control-Allow-Origin | ✅ Produzione (frontend e backend su domini diversi) |
2. Configurare il proxy in vite.config.js
Apri vite.config.js nella root del progetto frontend e aggiungi la sezione server.proxy:
// frontend/vite.config.js import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ // plugin React (già presente dal setup) plugins: [react()], server: { proxy: { // Tutte le richieste che iniziano con /api // vengono inoltrate al backend Express '/api': { target: 'http://localhost:3001', changeOrigin: true, // secure: false ← necessario solo se il target usa HTTPS self-signed } } } })
Come funziona il proxy, passo per passo
- Il browser fa
fetch('/api/notes')versolocalhost:5173/api/notes - Vite intercetta la richiesta perché il path inizia con
/api - Vite re-invia la richiesta a
http://localhost:3001/api/notes - Express risponde a Vite, Vite re-invia la risposta al browser
- Per il browser sembra tutto provenire da
localhost:5173→ nessun CORS
changeOrigin: true serve perché Express verifichi l’header Host:
senza questa opzione Vite inoltrerebbe Host: localhost:5173, che potrebbe
creare problemi con certi virtual-host. Con changeOrigin: true il proxy
sostituisce l’header con Host: localhost:3001.
vite.config.js devi riavviare il dev server con
Ctrl+C e poi npm run dev. Le modifiche a vite.config.js
non sono rilevate dall’HMR — richiedono un riavvio completo.3. Struttura del progetto fullstack
Con la L07 il progetto è già organizzato in due cartelle separate. Rivediamo la struttura che useremo in questa lezione:
# struttura delle directory del progetto DevNotes 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 delle chiamate API in un file dedicato (api/notesApi.js)
è una best practice: se in futuro cambi le URL o il modo di fare le richieste,
modifichi solo quel file senza toccare i componenti.
4. Il modulo API: notesApi.js
Crea la cartella src/api/ e al suo interno il file che centralizza
tutte le chiamate al backend:
// frontend/src/api/notesApi.js // URL base dell'API. Grazie al proxy Vite usiamo // percorsi relativi: /api/notes invece di http://localhost:3001/api/notes const API_BASE = '/api/notes' /** * Recupera tutte le note dal backend * @returns {Promise<Array>} */ export async function fetchNotes() { const res = await fetch(API_BASE) if (!res.ok) throw new Error(`Errore GET note: ${res.status}`) return res.json() } /** * Crea una nuova nota * @param {{ titolo: string, contenuto: string, tag: string }} nota * @returns {Promise<Object>} la nota creata con id assegnato dal server */ 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() } /** * Elimina una nota per ID * @param {string|number} id * @returns {Promise<void>} */ 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}`) }
fetch() non lancia un’eccezione per status 4xx/5xx — risolve la Promise
con un Response il cui campo ok è false.
Se non controlli res.ok esplicitamente, un errore 404 o 500 passerà in silenzio.
Sempre: if (!res.ok) throw new Error(...)5. Custom hook: useNotes
Incapsuliamo la logica di stato (loading, error, lista note, operazioni CRUD) in un custom hook. I componenti rimangono così semplici e focalizzati solo sulla UI.
// 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) // Carica le note al mount del componente che usa il hook useEffect(() => { loadNotes() }, []) async function loadNotes() { try { setLoading(true) setError(null) const data = await fetchNotes() setNotes(data) } catch (err) { setError(err.message) } finally { setLoading(false) } } // useCallback evita ricreazioni inutili della funzione ad ogni render const addNote = useCallback(async (formData) => { const nuovaNota = await createNote(formData) // Aggiornamento ottimistico: aggiungi subito alla lista locale 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 } }
Il hook espone tutto il necessario: i dati (notes), gli stati di UI
(loading, error) e le operazioni (addNote,
removeNote, reload). I componenti non sanno nulla di
fetch o del backend — ricevono solo dati pronti all’uso.
6. Il componente NoteForm: form con POST
Costruiamo il form per creare una nuova nota. Gestisce il proprio stato locale
(useState), lo stato di invio e gli errori di validazione prima ancora
di raggiungere il server.
// frontend/src/components/NoteForm.jsx import { useState } from 'react' import styles from './NoteForm.module.css' const TAGS_DISPONIBILI = ['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() // Validazione lato client: non sprecare una richiesta HTTP if (!form.titolo.trim()) { setErrMsg('Il titolo è obbligatorio') return } try { setSubmitting(true) setErrMsg('') await onAdd(form) // delega al genitore (App) tramite prop setForm({ titolo: '', contenuto: '', tag: 'React' }) setSuccess(true) setTimeout(() => setSuccess(false), 2500) } catch (err) { setErrMsg(err.message) } finally { setSubmitting(false) } } return ( <form onSubmit={handleSubmit} className={styles.form}> <h2 className={styles.title}>Nuova nota</h2> <input type="text" name="titolo" placeholder="Titolo nota…" value={form.titolo} onChange={handleChange} className={styles.input} /> <textarea name="contenuto" placeholder="Scrivi qui il contenuto…" value={form.contenuto} onChange={handleChange} rows={4} className={styles.textarea} /> <select name="tag" value={form.tag} onChange={handleChange} className={styles.select}> {TAGS_DISPONIBILI.map(t => <option key={t} value={t}>{t}</option> )} </select> {/* Feedback errore */} {errMsg && <p className={styles.errMsg}>⚠ {errMsg}</p>} {/* Feedback successo */} {success && <p className={styles.okMsg}>✓ Nota salvata!</p>} <button type="submit" disabled={submitting} className={styles.btn} > {submitting ? 'Salvataggio…' : 'Aggiungi nota'} </button> </form> ) }
7. Gestione errori fullstack
In un’applicazione fullstack gli errori possono nascere in punti diversi della catena. È fondamentale capire dove intercettarli e come comunicarli all’utente.
prima di fetch() — gratis, immediato
server non raggiungibile — fetch() rifiuta la promise
400 Bad Request, 404 Not Found, 500 Internal Error
risposta non è JSON valido → .json() rifiuta
Il middleware di errore in Express
Sul backend, Express può centralizzare la gestione degli errori con un middleware speciale
che ha quattro parametri (err, req, res, next). Va messo
dopo tutte le route in server.js:
// backend/server.js — aggiunta middleware errori globale // ... (route già definite) ... // Middleware per route non trovate (404) app.use((req, res) => { res.status(404).json({ messaggio: 'Risorsa non trovata' }) }) // Middleware di errore generico — 4 parametri obbligatori! 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' }) })
err.stack al client: espone dettagli interni.
Usa variabili d’ambiente per distinguere i due contesti:
if (process.env.NODE_ENV === 'development') res.json({ stack: err.stack })Gestire gli stati di loading e errore in React
// frontend/src/App.jsx import { useNotes } from './hooks/useNotes' import NoteList from './components/NoteList' import NoteForm from './components/NoteForm' import styles from './App.module.css' export default function App() { const { notes, loading, error, addNote, removeNote, reload } = useNotes() async function handleAdd(formData) { await addNote(formData) // addNote già aggiorna lo stato locale tramite setNotes } if (loading) return <div className={styles.loading}>Caricamento note…</div> if (error) return ( <div className={styles.errorBox}> <p>⚠ {error}</p> <button onClick={reload}>Riprova</button> </div> ) return ( <div className={styles.layout}> <header className={styles.header}> <h1>DevNotes</h1> <span>{notes.length} note</span> </header> <NoteForm onAdd={handleAdd} /> <NoteList notes={notes} onElimina={removeNote} /> </div> ) }
8. Avviare frontend e backend insieme
Per sviluppare, hai bisogno di avviare entrambi i processi in contemporanea. Hai tre opzioni, dalla più semplice alla più elegante:
Opzione A — Due terminali separati (sempre valida)
# Terminale 1 — backend cd devnotes/backend node server.js # oppure: npx nodemon server.js (riavvio automatico) # Terminale 2 — frontend cd devnotes/frontend npm run dev
Opzione B — Script concurrently nella root
# nella cartella devnotes/ (root del mono-repo) npm init -y npm install -D concurrently
// devnotes/package.json (root) { "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\"" } }
# un solo comando per avviare tutto npm run dev
nodemon nel backend (npm install -D nodemon) e modifica
lo script start: "start": "nodemon server.js". Ogni volta che modifichi un file
.js nel backend, il server si riavvia automaticamente — come HMR per il frontend.9. Verifica l’integrazione: checklist rapida
Prima di procedere al laboratorio, controlla questi punti chiave:
| Verifica | Come testarla | Risultato atteso |
|---|---|---|
| Proxy configurato | Backend avviato, apri http://localhost:5173/api/notes | Risposta JSON dal backend (non 404 da Vite) |
| GET note funziona | Apri l’app, guarda la console Network in DevTools | Richiesta a /api/notes → 200 OK con array JSON |
| POST funziona | Compila il form e invia | Nuova nota appare nella lista senza refresh |
| DELETE funziona | Clicca elimina su una nota | La nota sparisce dalla lista |
| Errore gestito | Ferma il backend, prova a caricare le note | Messaggio di errore con bottone “Riprova” |
Laboratorio L08 — Connettere frontend e backend
Configura il proxy Vite, crea il layer API, integra il form POST e testa l’intera catena di comunicazione fullstack su DevNotes.
→ Apri il laboratorioRiepilogo lezione
- Same-Origin Policy — i browser bloccano le richieste cross-origin; il proxy Vite le aggira in sviluppo
server.proxyinvite.config.js— instrada/api/*versolocalhost:3001conchangeOrigin: true- Layer API separato — centralizzare
fetch()inapi/notesApi.jsrende il codice mantenibile if (!res.ok) throw— pattern fondamentale:fetch()non lancia per 4xx/5xx- Custom hook
useNotes— incapsula loading, error e operazioni CRUD; i componenti restano puliti - Form con POST — validazione client-side, stato
submitting, feedback success/error, reset dopo invio - Middleware errori Express — 4 parametri, dopo tutte le route, risposta JSON strutturata
concurrently— avvia frontend e backend con un solonpm run devdalla root
Risorse
Documentazione ufficiale delle opzioni server.proxy con tutti i parametri disponibili (rewrite, configure, bypass…)
Riferimento completo su fetch(): opzioni, gestione Response, Headers, Body e CORS
Come definire middleware di errore, errori sincroni vs asincroni, gestione in produzione
Perché esiste la Same-Origin Policy, cosa blocca, come il browser la applica per XHR e fetch
Libreria per avviare più processi npm in parallelo con output colorato per terminale