## Les formulaires en Next.js comme un contrat : Zod, fieldErrors et règles unifiées côté client et serveur
Un formulaire n'est pas seulement un ensemble de champs et de boutons — c'est un contrat entre l'UI, la validation et la source de données. Quand ce contrat n'est pas explicitement défini, c'est le chaos : l'UI pense que les données sont valides, le serveur renvoie une erreur, les messages apparaissent de manière aléatoire, et le comportement du formulaire est imprévisible. La solution ? Unifier le schéma de validation, le format des erreurs et les états en utilisant Zod, safeParse et flatten pour garantir des règles identiques côté client et serveur.
Pourquoi les formulaires s'effondrent sans contrat
De nombreux projets commencent simplement : saisie, état, soumission. Ça marche bien pour les petits formulaires. Mais à mesure que la base de code grossit, trois points de divergence critiques émergent :
- Règles de validation incohérentes. Le client vérifie la longueur de la chaîne, mais le serveur signale aussi les mots interdits ou les espaces. Les utilisateurs voient une coche verte, mais la soumission échoue.
- Formats d'erreurs incompatibles. Une fois une chaîne, ensuite un objet, puis un tableau. L'UI doit deviner la structure de la réponse.
- États non définis. En attente, désactivé, succès, formError — chaque composant les gère différemment, ce qui mène à une UX fragmentée.
Sans un contrat unique, chaque formulaire suit ses propres règles locales. Ça ne scale pas.
Le formulaire comme un contrat : éléments clés
Un contrat de formulaire doit répondre clairement à ces questions :
- Quelles données sont considérées comme valides ?
- Quelles erreurs concernent des champs spécifiques (fieldErrors), et lesquelles l'opération globale (formError) ?
- Comment l'état en attente est-il géré ?
- Que retourne-t-on en cas de succès ?
- Le serveur peut-il renvoyer une réponse dans un format différent ?
- Les mêmes règles sont-elles utilisées côté client et serveur ?
Ces réponses doivent être fixées avant d'implémenter l'UI. Chez Workbench, cette approche a standardisé les formulaires autour de schémas Zod et d'une forme de réponse cohérente.
Un schéma Zod, deux usages
Au lieu de dupliquer la logique de validation entre client et serveur, utilisez un schéma Zod unique partout. Exemple de schéma pour une note :
import { z } from "zod";
export const noteSchema = z.object({
title: z
.string()
.trim()
.min(3, "Enter minimum 3 simvola")
.max(80, "Maximum 80 characters"),
description: z
.string()
.trim()
.max(500, "Maximum 500 characters")
.optional()
.or(z.literal("")),
}).refine(
data => data.title.toLowerCase() !== "test",
{
path: ["title"],
message: "Withlishkom tekhnicheskoe name for zametki",
}
);
export type NoteInput = z.infer<typeof noteSchema>;
Ce schéma définit les valeurs valides, les transformations (comme trim) et les règles personnalisées (refine). Il devient la source unique de vérité pour la validation.
Validation sûre avec safeParse
Utiliser safeParse au lieu de parse est crucial pour les formulaires. parse lève une exception en cas d'erreur — mauvais pour l'UX, où une saisie invalide est attendue. safeParse renvoie un résultat gérable :
export function validateNote(input: unknown) {
return noteSchema.safeParse(input);
}
Le résultat inclut un drapeau success et, si nécessaire, un objet error. Cela permet à l'UI et à la logique serveur de gérer le même type de réponse sans try/catch.
Unification des erreurs avec flatten
Zod renvoie un objet d'erreur complexe. Pour les formulaires, deux niveaux suffisent : erreurs de champs et erreur globale. La méthode flatten() est parfaite :
export function formatZodError(error: import("zod").ZodError) {
const flat = error.flatten();
return {
fieldErrors: flat.fieldErrors,
formError: flat.formErrors[0] ?? null,
};
}
Différences d'exemples :
- fieldErrors : "Le champ titre ne peut pas être vide", "La description dépasse 500 caractères".
- formError : "Une note avec ce slug existe déjà", "Serveur temporairement indisponible".
Cette séparation rend l'UX prévisible : les utilisateurs savent exactement où corriger la saisie et quand réessayer ou attendre.
Contrat unifié côté client et serveur
Côté client, le schéma Zod permet une validation précoce et la gestion d'état UI :
const clientResult = useMemo(() => {
return noteSchema.pick({ title: true }).safeParse({ title });
}, [title]);
const titleError = clientResult.success
? null
: clientResult.error.flatten().fieldErrors.title?.[0] ?? null;
const canSubmit = clientResult.success;
Côté serveur, le même schéma agit comme dernière ligne de défense :
export async function createNoteAction(raw: unknown) {
const parsed = noteSchema.safeParse(raw);
if (!parsed.success) {
const flat = parsed.error.flatten();
return {
ok: false,
fieldErrors: flat.fieldErrors,
formError: flat.formErrors[0] ?? null,
};
}
try {
// save to database
return {
ok: true,
fieldErrors: {},
formError: null,
};
} catch {
return {
ok: false,
fieldErrors: {},
formError: "Not succeeded sokhranit zametku",
};
}
}
Une règle, des rôles différents : UX côté client, sécurité côté serveur.
Forme standard de l'état du formulaire
Pour éviter la fragmentation, définissez un type unique pour tous les formulaires :
export type FormState = {
ok: boolean;
fieldErrors: Record<string, string[] | undefined>;
formError: string | null;
};
Cette forme couvre tous les états : succès, erreurs de champs, erreurs générales. Chaque formulaire du projet renvoie la même structure — simplifiant la gestion UI.
Quand utiliser React Hook Form
React Hook Form (RHF) est puissant mais pas universel. Il est justifié pour les formulaires avec :
- Beaucoup de champs avec validation dépendante
- Listes dynamiques ou structures imbriquées
- Validation au flou, états touched/dirty
- Contrôles personnalisés avec gestion du focus
- Besoin d'optimisation de rendu
Pour les formulaires simples avec 1–2 champs et validation basique, RHF ajoute une complexité inutile. Si useMemo + état dérivé Zod suffit — restez simple.
Changement de mentalité : du UI au contrat
Le changement clé ? Arrêtez de voir les formulaires comme des composants UI. Un formulaire est un contrat définissant :
- Schéma de données valide
- Format de réponse d'erreur
- Comportement pour les états (en attente, succès, erreur)
- Limites de responsabilités client-serveur
Avec le contrat en amont, l'implémentation UI devient mécanique. Surtout, chaque formulaire du projet se comporte de manière prévisible en suivant le même accord.
Points clés à retenir
- Utilisez un schéma Zod unique pour client et serveur — élimine les divergences de règles.
- Utilisez toujours
safeParse— il renvoie des résultats gérables au lieu d'exceptions. - Séparez
fieldErrorsetformError— améliore l'UX et simplifie la logique d'affichage. - Définissez une forme standard
FormState— tous les formulaires renvoient la même structure. - Ajoutez React Hook Form seulement quand c'est vraiment nécessaire — pas pour la mode, mais pour les scénarios complexes.
— Editorial Team
Aucun commentaire pour le moment.