React
Server Actions con validazione: dove mettere il controllo
Schema condiviso, `safeParse` e errori per campo: come una Server Action rifiuta i dati sbagliati senza fidarsi del browser.
In sintesi
- Ogni Server Action è un endpoint POST pubblico: la validazione autorevole sta nel server.
- Uno schema condiviso in `lib/validation.ts` evita che client e server divergano.
- `safeParse` restituisce errori per campo; `parse` lancia e ti lascia un messaggio generico.
La validazione di una Server Action va nel server, prima della scrittura: il controllo nel browser è comodità per chi digita, non una difesa. Una Server Action è un endpoint POST pubblico e si tratta come tale.
Il controllo che conta sta nel server
Chi chiama l’azione non è per forza il tuo form. Un fetch verso la stessa pagina basta a inviare campi vuoti, un messaggio da 10 MB o un id che non appartiene a chi scrive. Il client può mostrare errori mentre si digita, ma la decisione di accettare i dati si prende dove i dati vengono scritti.
npm install zod
// lib/validation.ts
import { z } from "zod";
export const contactSchema = z.object({
name: z.string().trim().min(2, "too short"),
email: z.string().trim().email("invalid email"),
message: z.string().trim().min(20, "at least 20 characters"),
});
export type ContactInput = z.infer;
`safeParse` non lancia: restituisce un risultato che leggi. Costruisci una mappa campo → messaggio e rimandala al form. Un’eccezione non gestita dentro l’azione arriva al client come errore generico, e chi compila perde l’unica informazione utile: quale campo correggere.
Uno schema, due lati
Lo schema vive in lib/validation.ts e lo importano sia il server sia il client. Il client lo usa per abilitare il pulsante e mostrare il messaggio subito, il server come unica fonte di verità. Due copie delle regole divergono alla prima modifica.
L’azione riceve FormData, non un oggetto: dal browser arrivano stringhe. Converti una volta, valida una volta, scrivi una volta.
// app/contact/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { contactSchema } from "@/lib/validation";
export type FormState = { errors?: Record; ok?: boolean };
export async function submitContact(_prev: FormState, formData: FormData): Promise {
const parsed = contactSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message"),
});
if (!parsed.success) {
const errors: Record = {};
for (const issue of parsed.error.issues) errors[String(issue.path[0])] = issue.message;
return { errors };
}
await db.contact.create({ data: parsed.data });
revalidatePath("/contact");
return { ok: true };
}
Cosa restituire al form
- Restituisci errori per campo, non un messaggio unico: chi compila deve sapere dove intervenire.
- Non restituire il testo dell’errore del database: rivela schema, nomi di tabella e vincoli.
- Tieni
okcome booleano e svuota il form in un effetto del client, non dentro l’azione. - Se l’azione richiede una sessione, controllala prima di leggere il payload: un utente non autenticato non deve poter far lavorare lo schema.
"use client";
import { useActionState } from "react";
import { submitContact } from "./actions";
export function ContactForm() {
const [state, action, pending] = useActionState(submitContact, {});
return (
Send
);
}
Un blocco pronto aiuta, ma non valida nulla
I blocchi di contatto e di form portano markup, stati di errore e spaziatura. Il record Glassmorphism Login Form mostra come un blocco del catalogo arriva già composto. Nessuno dei tre contiene le tue regole: il codice entra nel repository e la validazione resta tua.
Dove la Server Action non basta
Una Server Action non è una API: non ha un URL stabile da documentare, non risponde a GET e non serve un client esterno. Se lo stesso dato lo consuma un’altra applicazione, scrivi un Route Handler e lascia l’azione al form. Il confronto è in Route Handlers vs Server Actions.
Errori che si vedono in produzione
- Validare solo con
requiredetype="email": si disattivano dal DevTools in due clic. - Mettere
redirect()dentro untry/catch: il redirect funziona lanciando un’eccezione e la catturi per sbaglio. - Restituire l’oggetto di Zod al client: contiene lo schema e i messaggi interni.
- Chiamare
revalidatePathprima della scrittura: la route viene rigenerata con i dati vecchi.
Dove tenere azioni e schema senza gonfiare i componenti è descritto in struttura di un progetto App Router; se lo schema cresce, le librerie di form React spiegano cosa aggiunge la gestione dello stato.
Domande frequenti
Posso validare solo nel client?
No. Il client migliora l’esperienza, ma chiunque può inviare lo stesso payload senza passare dal form: la validazione che protegge il database sta nel server.
Meglio Zod o la validazione manuale?
Zod quando lo schema è condiviso tra client e server e cresce nel tempo; la validazione manuale basta per due campi e nessuna regola composta.
Dove metto lo schema condiviso?
In lib/validation.ts, fuori da app/. Server e client importano lo stesso percorso e non duplicano le regole.
Come mostro gli errori per campo?
L’azione restituisce un oggetto campo-messaggio e il component client lo legge con useActionState, accanto allo stato di invio.
Serve una protezione CSRF con le Server Actions?
Next.js confronta l’header Origin con l’host per le azioni inviate dai form. Se chiami l’azione da un dominio esterno, passa da un Route Handler con il tuo controllo.