React
Route Handlers vs Server Actions: cosa usare e quando
Server Action per le mutazioni da form, Route Handler per tutto il resto. La differenza è il chiamante: tabella, codice e limiti reali.
In sintesi
- Il chiamante decide: form del sito = Server Action, qualsiasi altro client = Route Handler.
- Una Server Action non ha un URL stabile e non è documentabile come un'API.
- La validazione sta nel server in entrambi i casi: cambia solo il modo di leggere l'input.
- Una Server Action serializza solo valori serializzabili: `Date` e `Map` non passano.
Usa una Server Action per una mutazione che parte da un form del tuo sito; usa un Route Handler quando chi chiama non è un componente React. La scelta dipende dal chiamante, non dal gusto personale.
Il chiamante decide
| Criterio | Server Action | Route Handler |
|---|---|---|
| Componente React del sito | Sì, è il caso previsto | Possibile, ma indiretto |
| Script, cron, client mobile | No | Sì |
| URL stabile e documentabile | No, cambia tra build | Sì |
| Metodo HTTP esplicito | Solo POST | GET, POST, PUT, DELETE |
| Cache o ISR sulla risposta | Non applicabile | Sì, con revalidate |
| Webhook di terze parti | No | Sì |
Un Route Handler è un file `route.ts` che esporta una funzione per ogni metodo HTTP che accetti. Riceve una `Request` standard e restituisce una `Response`: nessun tipo proprietario, nessuna astrazione.
// app/api/ordini/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
import { createOrder } from "@/lib/orders";
const schema = z.object({
sku: z.string().min(1),
qty: z.number().int().positive(),
});
export async function POST(request: Request) {
const body = await request.json();
const parsed = schema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "invalid" }, { status: 422 });
}
const order = await createOrder(parsed.data);
return NextResponse.json(order, { status: 201 });
}
La validazione sta nel handler, non nel client. Lo schema è lo stesso che useresti in una Server Action: `zod`, `valibot` o un parser scritto a mano non cambiano la sostanza.
Server Actions: il caso normale
Una Server Action è una funzione asincrona marcata `"use server"`. Next.js genera un endpoint POST e collega il form: il browser invia i dati senza che tu scriva un `fetch`.
// app/ordini/actions.ts
"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
import { createOrder } from "@/lib/orders";
const schema = z.object({
sku: z.string().min(1),
qty: z.coerce.number().int().min(1),
});
export async function createOrderAction(_prev: unknown, formData: FormData) {
const parsed = schema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return { error: "Compila tutti i campi" };
await createOrder(parsed.data);
revalidatePath("/ordini");
return { error: null };
}
- Il form funziona anche senza JavaScript, perché è un `
- L'aggiornamento dei dati passa da `revalidatePath` o `revalidateTag`, non da un reload manuale.
- Lo stato di errore torna al client come valore di ritorno, non come eccezione.
Quando il Route Handler è obbligatorio
- Webhook: Stripe, GitHub e simili chiamano un URL tuo con una firma negli header.
- Client non React: app mobile, script di import, integrazione di un partner.
- GET pubblico e cacheable: un feed, un JSON che altri leggono, una misura pubblica.
- Upload diretto con URL firmato: il file non passa dal tuo server.
Quello che una Server Action non fa
Una Server Action non è un'API. L'URL è generato a build time, non è documentabile e cambia quando cambi la struttura dei file. Non la chiami da `curl` e non la versioni con un contratto.
curl -X POST http://localhost:3000/api/ordini \
-H "content-type: application/json" \
-d '{"sku":"TS-01","qty":2}'
Il numero che conta
Il catalogo EneUI indicizza 111.985 componenti da 1.472 librerie e 1.556 blocchi, misurati il 1 novembre 2026 su `/api/stats`. Tre categorie coprono i pezzi di questo articolo: form validation, blocchi form e error handling.
Limite dichiarato
Una Server Action serializza solo valori serializzabili: `Date`, `Map` e istanze di classe non passano. Se il payload è grande o binario, il Route Handler resta la strada più semplice.
Per il contesto delle cartelle vedi la struttura di un progetto App Router. Per il confine tra moduli server e client c'è server-only e client-only. Per un confronto tra framework leggi Remix vs Next.js.
Domande frequenti
Posso chiamare una Server Action da un client mobile?
No. La Server Action è raggiungibile solo dal bundle del sito e il suo URL cambia a ogni build. Per un client esterno scrivi un Route Handler con un contratto stabile.
Un Route Handler è più lento di una Server Action?
No, girano sullo stesso runtime. Il costo in più è il codice: parsing, validazione e risposta li scrivi tu invece di lasciarli al framework.
Dove va la validazione?
Nel server, in entrambi i casi. La validazione nel client serve solo all'esperienza d'uso e non sostituisce quella del server.
Una Server Action può restituire un oggetto?
Sì, se l'oggetto è serializzabile. Restituisci dati semplici e usa il valore di ritorno per lo stato di errore, non per payload grandi.
Route Handler e Server Action condividono i cookie?
Sì. Entrambi leggono `cookies()` e `headers()` della richiesta in corso, con la stessa API e gli stessi permessi.