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

CriterioServer ActionRoute Handler
Componente React del sitoSì, è il caso previstoPossibile, ma indiretto
Script, cron, client mobileNoSì
URL stabile e documentabileNo, cambia tra buildSì
Metodo HTTP esplicitoSolo POSTGET, POST, PUT, DELETE
Cache o ISR sulla rispostaNon applicabileSì, con revalidate
Webhook di terze partiNoSì

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.