Lezione 8 – Connettere frontend e backend

🧱 React + Vite + Node.js — Guida completa · Lezione 08 di 10
← L07 — Backend con Node.js ed Express

LEZIONE 08  ·  FULL-STACK  ·  ADVANCED

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.


// cosa costruiremo DevNotes fullstack: il frontend React chiamerà l’API Express locale tramite proxy Vite. Leggerai le note dal backend, creerai nuove note con un form POST, eliminerai note con DELETE e mostrerai feedback visivi per loading / errori / successo. Nessuna chiamata diretta a localhost:3001 nel codice — solo percorsi relativi /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.

Il problema delle due origini in sviluppo
Browser
localhost:5173
──────
fetch(“/api/notes”)
──→
⛔ CORS Error
localhost:3001
senza proxy o CORS headers
Browser
localhost:5173
──→
Vite Proxy
localhost:5173
──→
Express
localhost:3001
con proxy Vite: stessa origine, nessun problema CORS

Hai due strategie per risolvere il problema:

StrategiaCome funzionaQuando usarla
Proxy ViteVite inoltra le richieste /api/* al backend. Per il browser è tutto localhost:5173✅ Sviluppo locale — è la scelta corretta
CORS headers lato serverExpress risponde con Access-Control-Allow-Origin✅ Produzione (frontend e backend su domini diversi)
⚡ Importante Il proxy Vite funziona solo in sviluppo. In produzione frontend e backend vivono su URL definitivi, quindi ti servirà comunque configurare CORS su Express — lo vedremo in L10. Per ora il proxy ci permette di sviluppare senza toccare la configurazione del server.

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') verso localhost: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.

✅ Riavvio obbligatorio Dopo aver modificato 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}`)
}
⚡ Pattern: controlla sempre res.ok 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.

Dove possono nascere gli errori nella catena fullstack
① Validazione client-side
prima di fetch() — gratis, immediato
② Rete / timeout
server non raggiungibile — fetch() rifiuta la promise
③ Risposta HTTP 4xx/5xx
400 Bad Request, 404 Not Found, 500 Internal Error
④ Parsing JSON fallito
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' })
})
⚠ Attenzione — dettagli errori in produzione In produzione non inviare mai 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 — riavvio automatico Installa 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:

VerificaCome testarlaRisultato atteso
Proxy configuratoBackend avviato, apri http://localhost:5173/api/notesRisposta JSON dal backend (non 404 da Vite)
GET note funzionaApri l’app, guarda la console Network in DevToolsRichiesta a /api/notes → 200 OK con array JSON
POST funzionaCompila il form e inviaNuova nota appare nella lista senza refresh
DELETE funzionaClicca elimina su una notaLa nota sparisce dalla lista
Errore gestitoFerma il backend, prova a caricare le noteMessaggio 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 laboratorio

Riepilogo lezione

  • Same-Origin Policy — i browser bloccano le richieste cross-origin; il proxy Vite le aggira in sviluppo
  • server.proxy in vite.config.js — instrada /api/* verso localhost:3001 con changeOrigin: true
  • Layer API separato — centralizzare fetch() in api/notesApi.js rende 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 solo npm run dev dalla root

Risorse

DOC
Vite — Server Proxy Options

Documentazione ufficiale delle opzioni server.proxy con tutti i parametri disponibili (rewrite, configure, bypass…)

DOC
MDN — Using the Fetch API

Riferimento completo su fetch(): opzioni, gestione Response, Headers, Body e CORS

GUIDE
Express.js — Error Handling

Come definire middleware di errore, errori sincroni vs asincroni, gestione in produzione

REF
MDN — Same-Origin Policy

Perché esiste la Same-Origin Policy, cosa blocca, come il browser la applica per XHR e fetch

REF
concurrently — GitHub

Libreria per avviare più processi npm in parallelo con output colorato per terminale


Lascia un commento