Retour à l'accueil

Formulaires dans Next.js en tant que contrat : Zod et règles unifiées

L'article explique comment traiter les formulaires dans Next.js non comme des composants UI, mais comme des contrats entre l'interface, la validation et le serveur. L'utilisation de Zod, safeParse et flatten permet d'unifier les règles, le format d'erreur et les états, garantissant une UX prévisible et sans écarts entre client et serveur.

Comment transformer les formulaires dans Next.js en un contrat fiable avec Zod
Advertisement 728x90

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

Google AdInline article slot

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 :

Google AdInline article slot
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.

Google AdInline article slot

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 fieldErrors et formError — 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

Advertisement 728x90

Lire ensuite