# Server Actions with validation: where the check belongs

A shared schema, safeParse and per-field errors: how a Server Action rejects bad input without ever trusting the browser.

> Page: https://ui.eneawork.it/blog/nextjs-server-actions-validazione
> Markdown: https://ui.eneawork.it/blog/nextjs-server-actions-validazione.md

*2026-11-01*

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

"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.

Misurato il 1 novembre 2026: il catalogo conta 1.556 blocchi in 99 categorie: 18 in contact, 10 in authentication e 3 in form (1 novembre 2026). Metodo: conteggio del campo categories di data/blocks.json con uno script Node, eseguito sul repository EneUI.

## 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

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.

## FAQ

### 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.
