title: "Valider ses données avec Zod v4 dans Next.js App Router" description: "Comment intégrer Zod v4 dans vos Server Actions et Route Handlers Next.js pour une validation typée, sécurisée et performante." date: "2026-05-27" category: "Ingénierie" readingTime: "7 min" author: "Oliwer Fortin, Tech Lead chez Kreio"
La validation des entrées est l'une des premières lignes de défense d'une application web. Avec Zod v4 et les Server Actions Next.js, il est possible de construire un pipeline de validation end-to-end typé, sans duplication de logique entre le client et le serveur.
Pourquoi TypeScript seul ne suffit pas
TypeScript vérifie les types à la compilation. Une Server Action est un point d'entrée HTTP : n'importe qui peut l'appeler directement, en contournant complètement le formulaire React et ses contraintes côté client.
La validation côté client (React Hook Form, attributs HTML required, minLength) protège l'expérience utilisateur. La validation côté serveur protège l'intégrité des données. Les deux niveaux sont complémentaires : on ne choisit pas l'un ou l'autre.
Zod v4 : ce qui change concrètement
Zod v4 est sorti mi-2025 avec des améliorations significatives par rapport à la v3, sur deux axes principaux : les performances de parsing et la taille du bundle.
| Métrique | Zod v3 | Zod v4 | Gain |
|---|---|---|---|
| Parsing d'une chaîne | baseline | 14x plus rapide | x14 |
| Parsing d'un tableau | baseline | 7x plus rapide | x7 |
| Parsing d'un objet (10 champs) | 890 ms / 100k | 210 ms / 100k | x4,2 |
| Taille du bundle | ~12 KB gzip | ~6 KB gzip | -50% |
En production, ces gains sont mesurables sur des APIs à fort volume. Le bundle réduit de moitié allège les cold starts des fonctions serverless sur Vercel ou AWS Lambda.
Zod v4 introduit également Zod Mini, une distribution allégée disponible via @zod/mini, pesant seulement 1,9 KB gzip. Elle est conçue pour les applications frontend où la taille du bundle est critique et supporte le tree-shaking natif. La nouvelle méthode .toJSONSchema() permet de convertir un schéma Zod en JSON Schema, utile pour générer une documentation OpenAPI ou alimenter des formulaires dynamiques sans duplication.
Valider une Server Action avec Zod : le pattern de base
Le pattern central repose sur safeParse plutôt que sur parse. Contrairement à parse, safeParse ne lève jamais d'exception : il retourne un objet discriminant { success: true, data } ou { success: false, error }.
// app/actions/contact.ts
"use server"
import { z } from "zod"
const contactSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
message: z.string().min(10).max(1000),
})
export type ContactFormState = {
errors?: z.inferFlattenedErrors<typeof contactSchema>["fieldErrors"]
success?: boolean
}
export async function submitContact(
_prev: ContactFormState,
formData: FormData
): Promise<ContactFormState> {
const raw = {
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message"),
}
const result = contactSchema.safeParse(raw)
if (!result.success) {
return {
errors: result.error.flatten().fieldErrors,
}
}
// result.data est pleinement typé ici
await saveContact(result.data)
return { success: true }
}
result.error.flatten().fieldErrors retourne un objet de la forme { name: string[], email: string[] }, directement mappable dans le JSX sans traitement supplémentaire. Si on ajoute un champ au schéma, TypeScript signale immédiatement les endroits à mettre à jour côté client, grâce au type ContactFormState dérivé du schéma.
Connecter la validation à useActionState
Côté client, useActionState (disponible depuis React 19) reçoit l'état retourné par la Server Action et expose les erreurs champ par champ, sans état local supplémentaire.
// app/contact/page.tsx
"use client"
import { useActionState } from "react"
import { submitContact, type ContactFormState } from "@/app/actions/contact"
const initialState: ContactFormState = {}
export default function ContactPage() {
const [state, action, isPending] = useActionState(submitContact, initialState)
return (
<form action={action}>
<input name="name" />
{state.errors?.name && (
<p className="text-red-500">{state.errors.name[0]}</p>
)}
<input name="email" type="email" />
{state.errors?.email && (
<p className="text-red-500">{state.errors.email[0]}</p>
)}
<textarea name="message" />
{state.errors?.message && (
<p className="text-red-500">{state.errors.message[0]}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? "Envoi..." : "Envoyer"}
</button>
{state.success && <p>Message envoyé.</p>}
</form>
)
}
Le schéma Zod fait le lien entre les deux couches sans duplication. Pas de type manuel à maintenir séparément, pas de désynchronisation possible entre la validation serveur et les messages d'erreur affichés.
next-safe-action : réduire le boilerplate sur les projets plus larges
Sur un projet avec de nombreuses Server Actions, le pattern safeParse / flatten devient répétitif. next-safe-action encapsule ce boilerplate et ajoute une couche de middleware composable.
// lib/safe-action.ts
import { createSafeActionClient } from "next-safe-action"
export const actionClient = createSafeActionClient()
// app/actions/contact.ts
import { z } from "zod"
import { actionClient } from "@/lib/safe-action"
export const submitContact = actionClient
.schema(z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10),
}))
.action(async ({ parsedInput }) => {
await saveContact(parsedInput)
return { success: true }
})
parsedInput est pleinement typé, la validation est déjà effectuée avant que le code métier s'exécute. La librairie gère séparément les erreurs de validation, les erreurs serveur et les erreurs réseau. Elle est compatible avec Zod, Valibot, ArkType et tout validateur conforme à Standard Schema v1.
Le coût réel de la validation
Un argument courant contre la validation côté serveur est son coût CPU. Avec Zod v4, ce coût est négligeable. On peut l'estimer avec la formule suivante, où est le nombre de requêtes simultanées, le nombre de champs et la constante de parsing par champ (environ 2 µs avec Zod v4 sur un serveur standard) :
Pour 1 000 requêtes simultanées avec 10 champs : secondes. Soit 20 ms de CPU, un coût marginal par rapport au temps d'aller-retour réseau ou aux requêtes base de données.
Ce qu'on en retient
Zod v4 est aujourd'hui le choix par défaut pour la validation à l'exécution dans un projet Next.js. Le pattern safeParse + flatten() + useActionState couvre la grande majorité des besoins sans dépendance supplémentaire. Pour les projets avec de nombreuses Server Actions, next-safe-action réduit le boilerplate et ajoute de la cohérence.
La règle de base reste simple : toute donnée qui traverse une frontière réseau doit être validée côté serveur, indépendamment de ce que le client envoie. TypeScript rend ce travail agréable ; Zod le rend fiable.