Zod v4 avec Next.js 15 : Validation complète des schémas pour les formulaires, APIs et Server Actions

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

Validez tout. Ne faites confiance à rien. Zod v4 est la bibliothèque de validation de schémas TypeScript la plus rapide, et elle se marie parfaitement avec Next.js 15. Dans ce tutoriel, vous construirez une application de gestion de contacts prête pour la production avec une validation robuste à travers les formulaires, les APIs, les Server Actions et les variables d'environnement.

Ce que vous apprendrez

À la fin de ce tutoriel, vous serez capable de :

  • Comprendre ce qui a changé dans Zod v4 et pourquoi cela compte
  • Définir des schémas réutilisables pour toute votre application Next.js
  • Valider les entrées des Server Actions avec Zod et useActionState
  • Sécuriser les API Route Handlers avec la validation du corps de requête et des paramètres
  • Parser et valider les variables d'environnement au démarrage
  • Gérer les erreurs de validation avec des messages clairs pour les utilisateurs
  • Construire des formulaires type-safe qui partagent les schémas entre client et serveur

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ installé (node --version)
  • De l'expérience en TypeScript (types, génériques, inférence)
  • Une connaissance de Next.js 15 (App Router, Server Components, Server Actions)
  • Les bases de React 19 (useActionState, actions de formulaire)
  • Un éditeur de code — VS Code ou Cursor recommandé

Pourquoi Zod v4 ?

Zod est la bibliothèque de validation de schémas de référence pour TypeScript depuis 2022. La version 4 est une réécriture complète qui apporte des améliorations massives de performance et de nouvelles fonctionnalités :

FonctionnalitéZod v3Zod v4
Vitesse de parsingBase de référence2 à 7 fois plus rapide
Taille du bundleenviron 57 Koenviron 13 Ko (77% plus petit)
Tree-shakingLimitéSupport ESM complet
Messages d'erreurBasiquesRiches et structurés
JSON SchemaBibliothèque tierceIntégré z.toJSONSchema()
Template literalsNonz.templateLiteral()
MétadonnéesNonz.registry() pour formulaires/docs

Les gains les plus importants sont la vitesse et la taille. Zod v4 parse les schémas 2 à 7 fois plus vite que v3, ce qui compte quand vous validez chaque requête API et soumission de formulaire. La réduction de 77% du bundle signifie moins de JavaScript envoyé au client.


Étape 1 : Configuration du projet

Créez un nouveau projet Next.js 15 et installez Zod v4 :

npx create-next-app@latest zod-nextjs-demo --typescript --tailwind --eslint --app --src-dir --turbopack
cd zod-nextjs-demo

Installez Zod v4 :

npm install zod@^4

Vérifiez que vous avez Zod v4 :

npx tsx -e "import {z} from 'zod'; console.log(z.version)"
# Devrait afficher 4.x.x

La structure de votre projet ressemblera à ceci :

src/
├── app/
│   ├── layout.tsx
│   ├── page.tsx
│   ├── contacts/
│   │   ├── page.tsx
│   │   ├── new/
│   │   │   └── page.tsx
│   │   └── [id]/
│   │       └── page.tsx
│   └── api/
│       └── contacts/
│           └── route.ts
├── lib/
│   ├── schemas.ts          # Tous les schémas Zod
│   ├── env.ts              # Validation d'environnement
│   └── actions.ts          # Server Actions
└── components/
    └── contact-form.tsx    # Composant de formulaire

Étape 2 : Définir vos schémas

Le principe fondamental de Zod est définir une fois, utiliser partout. Créez un fichier de schémas central que le client et le serveur importent.

Créez src/lib/schemas.ts :

import { z } from "zod";
 
// Schéma de contact de base — réutilisé dans les formulaires, APIs et actions
export const contactSchema = z.object({
  name: z
    .string()
    .min(2, "Le nom doit contenir au moins 2 caractères")
    .max(100, "Le nom doit contenir moins de 100 caractères")
    .trim(),
 
  email: z
    .string()
    .email("Veuillez entrer une adresse email valide")
    .toLowerCase(),
 
  phone: z
    .string()
    .regex(/^\+?[\d\s-()]{7,15}$/, "Veuillez entrer un numéro de téléphone valide")
    .optional()
    .or(z.literal("")),
 
  company: z
    .string()
    .max(200, "Le nom de l'entreprise est trop long")
    .optional(),
 
  message: z
    .string()
    .min(10, "Le message doit contenir au moins 10 caractères")
    .max(5000, "Le message doit contenir moins de 5000 caractères"),
 
  priority: z.enum(["low", "medium", "high", "urgent"], {
    message: "Veuillez sélectionner un niveau de priorité valide",
  }),
});
 
// Inférer les types TypeScript du schéma
export type Contact = z.infer<typeof contactSchema>;
 
// Schéma de mise à jour (tous les champs optionnels sauf id)
export const contactUpdateSchema = contactSchema.partial().extend({
  id: z.string().uuid("ID de contact invalide"),
});
 
export type ContactUpdate = z.infer<typeof contactUpdateSchema>;
 
// Schéma pour les paramètres de recherche/filtre
export const contactQuerySchema = z.object({
  q: z.string().optional().default(""),
  priority: z.enum(["low", "medium", "high", "urgent"]).optional(),
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  sort: z.enum(["name", "email", "createdAt"]).default("createdAt"),
  order: z.enum(["asc", "desc"]).default("desc"),
});
 
export type ContactQuery = z.infer<typeof contactQuerySchema>;

Patterns Zod v4 clés utilisés

1. Messages d'erreur personnalisés inline :

Dans Zod v4, vous passez les messages d'erreur directement comme second argument aux validateurs :

z.string().min(2, "Le nom doit contenir au moins 2 caractères")

2. Coercition pour les paramètres de requête :

z.coerce.number() convertit automatiquement les paramètres de requête textuels comme "5" en nombre 5. Cela est essentiel pour les paramètres d'URL qui sont toujours des chaînes.

3. Inférence de type avec z.infer :

Vous n'écrivez jamais d'interfaces TypeScript manuellement. Les schémas Zod SONT vos types :

// Ce type est automatiquement :
// {
//   name: string;
//   email: string;
//   phone?: string | undefined;
//   company?: string | undefined;
//   message: string;
//   priority: "low" | "medium" | "high" | "urgent";
// }
export type Contact = z.infer<typeof contactSchema>;

Étape 3 : Valider les variables d'environnement

L'une des utilisations les plus impactantes de Zod est la validation des variables d'environnement au démarrage. Détectez les erreurs de configuration avant que votre application ne serve une seule requête.

Créez src/lib/env.ts :

import { z } from "zod";
 
const envSchema = z.object({
  // Base de données
  DATABASE_URL: z.string().url("DATABASE_URL doit être une URL valide"),
 
  // Authentification
  AUTH_SECRET: z
    .string()
    .min(32, "AUTH_SECRET doit contenir au moins 32 caractères"),
 
  // Application
  NEXT_PUBLIC_APP_URL: z
    .string()
    .url()
    .default("http://localhost:3000"),
 
  NODE_ENV: z
    .enum(["development", "production", "test"])
    .default("development"),
 
  // Email (optionnel en développement)
  SMTP_HOST: z.string().optional(),
  SMTP_PORT: z.coerce.number().default(587),
  SMTP_USER: z.string().optional(),
  SMTP_PASS: z.string().optional(),
});
 
// Parser et valider — lance une erreur au démarrage si invalide
function validateEnv() {
  const result = envSchema.safeParse(process.env);
 
  if (!result.success) {
    console.error("Variables d'environnement invalides :");
    console.error(result.error.format());
    throw new Error("Configuration d'environnement invalide");
  }
 
  return result.data;
}
 
export const env = validateEnv();
 
// Accès type-safe : env.DATABASE_URL est string, env.SMTP_PORT est number

Maintenant importez env partout au lieu d'utiliser process.env directement :

import { env } from "@/lib/env";
 
// Type-safe, validé, avec les valeurs par défaut appliquées
const dbUrl = env.DATABASE_URL; // string (garanti)
const port = env.SMTP_PORT;     // number (converti depuis string)

N'importez jamais env.ts dans les composants client. Il accède à process.env qui n'existe que côté serveur. Pour les variables d'environnement côté client, utilisez les variables préfixées NEXT_PUBLIC_ directement.


Étape 4 : Server Actions avec validation Zod

Les Server Actions sont la méthode principale pour gérer les soumissions de formulaires dans Next.js 15. Zod les rend type-safe et sécurisées.

Créez src/lib/actions.ts :

"use server";
 
import { contactSchema, type Contact } from "./schemas";
 
// Type d'état d'action pour useActionState
export type ActionState = {
  success: boolean;
  message: string;
  errors?: Record<string, string[]>;
  data?: Contact;
};
 
export async function createContact(
  prevState: ActionState,
  formData: FormData
): Promise<ActionState> {
  // Extraire les données brutes du formulaire
  const rawData = {
    name: formData.get("name"),
    email: formData.get("email"),
    phone: formData.get("phone"),
    company: formData.get("company"),
    message: formData.get("message"),
    priority: formData.get("priority"),
  };
 
  // Valider avec Zod
  const result = contactSchema.safeParse(rawData);
 
  if (!result.success) {
    // Convertir les erreurs Zod en objet plat pour le formulaire
    const fieldErrors: Record<string, string[]> = {};
    for (const issue of result.error.issues) {
      const field = issue.path[0]?.toString() ?? "form";
      if (!fieldErrors[field]) fieldErrors[field] = [];
      fieldErrors[field].push(issue.message);
    }
 
    return {
      success: false,
      message: "Veuillez corriger les erreurs ci-dessous.",
      errors: fieldErrors,
    };
  }
 
  // result.data est entièrement typé comme Contact
  const validatedData = result.data;
 
  try {
    // Dans une vraie application, sauvegarder en base de données
    console.log("Création du contact :", validatedData);
 
    // Simuler une insertion en base
    await new Promise((resolve) => setTimeout(resolve, 500));
 
    return {
      success: true,
      message: `Contact "${validatedData.name}" créé avec succès !`,
      data: validatedData,
    };
  } catch (error) {
    return {
      success: false,
      message: "Une erreur est survenue. Veuillez réessayer.",
    };
  }
}

Pourquoi safeParse plutôt que parse ?

  • parse() lance une ZodError sur une entrée invalide — idéal pour les routes API où vous attrapez et retournez un 400
  • safeParse() retourne { success, data, error } — idéal pour les Server Actions où vous devez retourner un état d'erreur au formulaire

Étape 5 : Construire le formulaire validé

Créez un composant de formulaire qui affiche les erreurs de validation côté serveur avec useActionState.

Créez src/components/contact-form.tsx :

"use client";
 
import { useActionState } from "react";
import { createContact, type ActionState } from "@/lib/actions";
 
const initialState: ActionState = {
  success: false,
  message: "",
};
 
export function ContactForm() {
  const [state, formAction, isPending] = useActionState(
    createContact,
    initialState
  );
 
  return (
    <form action={formAction} className="space-y-6 max-w-lg">
      {/* Message de statut */}
      {state.message && (
        <div
          className={`p-4 rounded-lg ${
            state.success
              ? "bg-green-50 text-green-800 border border-green-200"
              : state.errors
              ? "bg-red-50 text-red-800 border border-red-200"
              : ""
          }`}
        >
          {state.message}
        </div>
      )}
 
      {/* Nom */}
      <div>
        <label htmlFor="name" className="block text-sm font-medium mb-1">
          Nom *
        </label>
        <input
          id="name"
          name="name"
          type="text"
          required
          className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
        />
        {state.errors?.name && (
          <p className="mt-1 text-sm text-red-600">{state.errors.name[0]}</p>
        )}
      </div>
 
      {/* Email */}
      <div>
        <label htmlFor="email" className="block text-sm font-medium mb-1">
          Email *
        </label>
        <input
          id="email"
          name="email"
          type="email"
          required
          className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
        />
        {state.errors?.email && (
          <p className="mt-1 text-sm text-red-600">{state.errors.email[0]}</p>
        )}
      </div>
 
      {/* Téléphone */}
      <div>
        <label htmlFor="phone" className="block text-sm font-medium mb-1">
          Téléphone
        </label>
        <input
          id="phone"
          name="phone"
          type="tel"
          className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
        />
        {state.errors?.phone && (
          <p className="mt-1 text-sm text-red-600">{state.errors.phone[0]}</p>
        )}
      </div>
 
      {/* Entreprise */}
      <div>
        <label htmlFor="company" className="block text-sm font-medium mb-1">
          Entreprise
        </label>
        <input
          id="company"
          name="company"
          type="text"
          className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
        />
        {state.errors?.company && (
          <p className="mt-1 text-sm text-red-600">
            {state.errors.company[0]}
          </p>
        )}
      </div>
 
      {/* Message */}
      <div>
        <label htmlFor="message" className="block text-sm font-medium mb-1">
          Message *
        </label>
        <textarea
          id="message"
          name="message"
          rows={4}
          required
          className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
        />
        {state.errors?.message && (
          <p className="mt-1 text-sm text-red-600">
            {state.errors.message[0]}
          </p>
        )}
      </div>
 
      {/* Priorité */}
      <div>
        <label htmlFor="priority" className="block text-sm font-medium mb-1">
          Priorité *
        </label>
        <select
          id="priority"
          name="priority"
          required
          className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
        >
          <option value="">Sélectionner la priorité</option>
          <option value="low">Basse</option>
          <option value="medium">Moyenne</option>
          <option value="high">Haute</option>
          <option value="urgent">Urgente</option>
        </select>
        {state.errors?.priority && (
          <p className="mt-1 text-sm text-red-600">
            {state.errors.priority[0]}
          </p>
        )}
      </div>
 
      {/* Soumettre */}
      <button
        type="submit"
        disabled={isPending}
        className="w-full py-2 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
      >
        {isPending ? "Envoi en cours..." : "Créer le contact"}
      </button>
    </form>
  );
}

Comment ça fonctionne

  1. L'utilisateur remplit le formulaire et clique sur soumettre
  2. formAction envoie le FormData au Server Action createContact
  3. Zod valide tous les champs côté serveur
  4. Si la validation échoue, les erreurs sont retournées au composant via state.errors
  5. Chaque champ affiche son message d'erreur spécifique
  6. Si la validation réussit, le contact est créé et un message de succès apparaît

L'état isPending de useActionState gère automatiquement l'état de chargement — pas besoin de useState manuel.


Étape 6 : Valider les API Route Handlers

Pour les routes API, utilisez Zod pour valider les corps de requête, les paramètres de requête et les paramètres de chemin.

Créez src/app/api/contacts/route.ts :

import { NextRequest, NextResponse } from "next/server";
import { contactSchema, contactQuerySchema } from "@/lib/schemas";
 
// GET /api/contacts?q=john&priority=high&page=1&limit=10
export async function GET(request: NextRequest) {
  const searchParams = Object.fromEntries(
    request.nextUrl.searchParams.entries()
  );
 
  // Valider les paramètres de requête
  const result = contactQuerySchema.safeParse(searchParams);
 
  if (!result.success) {
    return NextResponse.json(
      {
        error: "Paramètres de requête invalides",
        details: result.error.issues.map((i) => ({
          field: i.path.join("."),
          message: i.message,
        })),
      },
      { status: 400 }
    );
  }
 
  const { q, priority, page, limit, sort, order } = result.data;
 
  // Dans une vraie app, requêtez votre base de données
  console.log("Récupération des contacts :", { q, priority, page, limit, sort, order });
 
  return NextResponse.json({
    contacts: [],
    pagination: { page, limit, total: 0 },
  });
}
 
// POST /api/contacts
export async function POST(request: NextRequest) {
  let body: unknown;
 
  try {
    body = await request.json();
  } catch {
    return NextResponse.json(
      { error: "Corps JSON invalide" },
      { status: 400 }
    );
  }
 
  // Valider le corps de la requête
  const result = contactSchema.safeParse(body);
 
  if (!result.success) {
    return NextResponse.json(
      {
        error: "Échec de la validation",
        details: result.error.issues.map((i) => ({
          field: i.path.join("."),
          message: i.message,
        })),
      },
      { status: 400 }
    );
  }
 
  const contact = result.data;
 
  // Dans une vraie app, insérez en base de données
  console.log("Création du contact via API :", contact);
 
  return NextResponse.json(
    { id: crypto.randomUUID(), ...contact },
    { status: 201 }
  );
}

Helper de validation réutilisable

Si vous avez beaucoup de routes API, extrayez un helper pour réduire le code répétitif :

// src/lib/validate.ts
import { z, type ZodType } from "zod";
import { NextResponse } from "next/server";
 
export function validateBody<T extends ZodType>(schema: T, data: unknown) {
  const result = schema.safeParse(data);
 
  if (!result.success) {
    return {
      success: false as const,
      response: NextResponse.json(
        {
          error: "Échec de la validation",
          details: result.error.issues.map((i) => ({
            field: i.path.join("."),
            message: i.message,
          })),
        },
        { status: 400 }
      ),
    };
  }
 
  return {
    success: true as const,
    data: result.data as z.infer<T>,
  };
}

Étape 7 : Patterns avancés de Zod v4

Pattern 1 : Unions discriminées

Gérez différents types de formulaires avec un seul schéma :

const notificationSchema = z.discriminatedUnion("channel", [
  z.object({
    channel: z.literal("email"),
    email: z.string().email(),
    subject: z.string().min(1),
  }),
  z.object({
    channel: z.literal("sms"),
    phone: z.string().regex(/^\+[\d]{10,15}$/),
  }),
  z.object({
    channel: z.literal("push"),
    deviceToken: z.string().min(1),
    title: z.string().min(1),
  }),
]);
 
// TypeScript connaît la forme exacte basée sur "channel"
type Notification = z.infer<typeof notificationSchema>;

Pattern 2 : Composition de schémas avec .pipe()

Transformez et validez par étapes :

// Parser une chaîne séparée par des virgules en tableau validé
const tagsSchema = z
  .string()
  .transform((val) => val.split(",").map((s) => s.trim()))
  .pipe(z.array(z.string().min(1).max(50)).min(1).max(10));
 
tagsSchema.parse("react, nextjs, typescript");
// Résultat : ["react", "nextjs", "typescript"]

Pattern 3 : Génération de JSON Schema dans Zod v4

Générez un JSON Schema à partir de vos schémas Zod — parfait pour la documentation API ou les spécifications OpenAPI :

import { z } from "zod";
 
const userSchema = z.object({
  name: z.string().describe("Le nom complet de l'utilisateur"),
  email: z.string().email().describe("Adresse email principale"),
  age: z.number().int().min(18).describe("Doit avoir 18 ans ou plus"),
});
 
// Intégré dans Zod v4 — aucune bibliothèque tierce nécessaire
const jsonSchema = z.toJSONSchema(userSchema);
console.log(JSON.stringify(jsonSchema, null, 2));

Pattern 4 : Carte d'erreurs personnalisée

Personnalisez tous les messages d'erreur globalement pour votre application :

z.config({
  customError: (issue) => {
    if (issue.code === "too_small" && issue.minimum === 1) {
      return { message: "Ce champ est requis" };
    }
    if (issue.code === "invalid_type" && issue.expected === "string") {
      return { message: "Veuillez entrer du texte" };
    }
    return { message: issue.message };
  },
});

Pattern 5 : Métadonnées de formulaire avec z.registry()

Zod v4 introduit les registres pour attacher des métadonnées aux schémas — utile pour générer automatiquement des interfaces de formulaires :

const formRegistry = z.registry<{
  label: string;
  placeholder?: string;
  helpText?: string;
}>();
 
const nameField = z.string().min(2);
const emailField = z.string().email();
 
formRegistry.register(nameField, {
  label: "Nom complet",
  placeholder: "Jean Dupont",
  helpText: "Entrez votre prénom et nom de famille",
});
 
formRegistry.register(emailField, {
  label: "Adresse email",
  placeholder: "jean@exemple.com",
});
 
// Récupérer les métadonnées pour le rendu du formulaire
const nameMeta = formRegistry.get(nameField);
// { label: "Nom complet", placeholder: "Jean Dupont", helpText: "..." }

Étape 8 : Bonnes pratiques de gestion des erreurs

Aplatir les erreurs pour les formulaires

Zod v4 fournit .flatten() pour des formes d'erreur adaptées aux formulaires :

const result = contactSchema.safeParse(badData);
 
if (!result.success) {
  const flat = result.error.flatten();
 
  // flat.formErrors — tableau d'erreurs de niveau supérieur
  // flat.fieldErrors — { name: string[], email: string[], ... }
 
  console.log(flat.fieldErrors);
  // {
  //   name: ["Le nom doit contenir au moins 2 caractères"],
  //   email: ["Veuillez entrer une adresse email valide"],
  // }
}

Formater les erreurs pour les réponses API

Utilisez .format() pour des structures d'erreur imbriquées :

const formatted = result.error.format();
// {
//   name: { _errors: ["Le nom doit contenir au moins 2 caractères"] },
//   email: { _errors: ["Veuillez entrer une adresse email valide"] },
// }

Étape 9 : Validation côté client (amélioration optionnelle)

Bien que la validation côté serveur soit la source de vérité, vous pouvez ajouter la validation côté client pour un retour instantané. Comme les schémas sont partagés, les règles de validation restent synchronisées automatiquement.

"use client";
 
import { useState } from "react";
import { contactSchema } from "@/lib/schemas";
import type { z } from "zod";
 
export function useFormValidation() {
  const [errors, setErrors] = useState<Record<string, string[]>>({});
 
  function validateField(name: string, value: unknown) {
    const fieldSchema = contactSchema.shape[name as keyof typeof contactSchema.shape];
    if (!fieldSchema) return;
 
    const result = fieldSchema.safeParse(value);
    setErrors((prev) => ({
      ...prev,
      [name]: result.success ? [] : result.error.issues.map((i) => i.message),
    }));
  }
 
  function clearErrors() {
    setErrors({});
  }
 
  return { errors, validateField, clearErrors };
}

Utilisez-le dans votre formulaire pour une validation en temps réel au blur :

const { errors, validateField } = useFormValidation();
 
<input
  name="email"
  onBlur={(e) => validateField("email", e.target.value)}
/>
{errors.email?.length > 0 && (
  <p className="text-red-600">{errors.email[0]}</p>
)}

Étape 10 : Tester vos schémas

Les schémas sont des fonctions pures — c'est la partie la plus facile de votre application à tester.

// __tests__/schemas.test.ts
import { describe, it, expect } from "vitest";
import { contactSchema, contactQuerySchema } from "@/lib/schemas";
 
describe("contactSchema", () => {
  const validContact = {
    name: "Jean Dupont",
    email: "jean@exemple.com",
    message: "Ceci est un message de test pour le formulaire de contact.",
    priority: "medium" as const,
  };
 
  it("accepte des données de contact valides", () => {
    const result = contactSchema.safeParse(validContact);
    expect(result.success).toBe(true);
  });
 
  it("rejette un nom vide", () => {
    const result = contactSchema.safeParse({ ...validContact, name: "" });
    expect(result.success).toBe(false);
  });
 
  it("rejette un email invalide", () => {
    const result = contactSchema.safeParse({
      ...validContact,
      email: "pas-un-email",
    });
    expect(result.success).toBe(false);
  });
 
  it("trim et met en minuscules l'email", () => {
    const result = contactSchema.safeParse({
      ...validContact,
      email: "  JEAN@Exemple.COM  ",
    });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.email).toBe("jean@exemple.com");
    }
  });
 
  it("accepte le téléphone optionnel", () => {
    const result = contactSchema.safeParse({
      ...validContact,
      phone: "+33 6 12 34 56 78",
    });
    expect(result.success).toBe(true);
  });
});
 
describe("contactQuerySchema", () => {
  it("applique les valeurs par défaut pour les paramètres manquants", () => {
    const result = contactQuerySchema.safeParse({});
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.page).toBe(1);
      expect(result.data.limit).toBe(20);
      expect(result.data.sort).toBe("createdAt");
      expect(result.data.order).toBe("desc");
    }
  });
 
  it("convertit les nombres en chaîne", () => {
    const result = contactQuerySchema.safeParse({
      page: "3",
      limit: "50",
    });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.page).toBe(3);
      expect(result.data.limit).toBe(50);
    }
  });
});

Exécutez les tests :

npx vitest run --reporter=verbose

Dépannage

"Cannot find module 'zod'"

Assurez-vous d'avoir installé Zod v4 spécifiquement :

npm install zod@^4

"Type 'ZodObject' is not assignable..."

Zod v4 a des exports de types différents. Si vous mettez à jour depuis v3, mettez à jour vos imports :

// v3 (ancien)
import { ZodType, ZodSchema } from "zod";
 
// v4 (nouveau) — utilisez z.ZodType directement
import { z } from "zod";
type Schema = z.ZodType;

Les données du formulaire retournent null

FormData.get() retourne string | File | null. Zod gère null en le rejetant comme type invalide, ce qui donne des messages d'erreur appropriés. Pas besoin de vérifications null manuelles.


Prochaines étapes

Maintenant que vous maîtrisez la validation Zod v4 dans Next.js, envisagez :

  • Ajouter l'intégration base de données — Utilisez Zod avec Drizzle ORM pour une sécurité de type de bout en bout
  • Construire des APIs type-safe — Combinez Zod avec tRPC pour des couches API entièrement typées
  • Implémenter l'authentification — Validez les formulaires d'auth avec Better Auth
  • Explorer les registres Zod v4 — Construisez des interfaces de formulaires auto-générées à partir des métadonnées
  • Générer des spécifications OpenAPI — Utilisez z.toJSONSchema() pour documenter automatiquement vos APIs

Conclusion

Zod v4 est la couche de validation dont chaque application Next.js a besoin. En définissant les schémas une fois et en les partageant entre client et serveur, vous obtenez :

  • Sécurité des types — Types TypeScript dérivés des schémas, jamais désynchronisés
  • Sécurité — Chaque entrée validée avant traitement
  • Expérience développeur — IntelliSense, autocomplétion et vérifications à la compilation
  • Expérience utilisateur — Messages d'erreur clairs et spécifiques pour chaque champ
  • Performance — Parsing 2 à 7 fois plus rapide et bundle 77% plus petit que Zod v3

Le pattern "définir une fois, valider partout" élimine des catégories entières de bugs. Vos formulaires, APIs, Server Actions et variables d'environnement partagent tous la même source de vérité. Quand vous changez une règle de validation, elle se met à jour partout automatiquement.

Commencez avec z.object() et safeParse(). C'est tout ce dont vous avez besoin pour rendre votre application Next.js à toute épreuve.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Explorer la nouvelle API Responses : Un guide complet.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire une API GraphQL typesafe avec Next.js App Router, Yoga et Pothos

Apprenez à construire une API GraphQL entièrement typesafe avec Next.js 15 App Router, GraphQL Yoga et le constructeur de schémas Pothos. Ce tutoriel pratique couvre la conception de schémas, les requêtes, les mutations, le middleware d'authentification et un client React avec urql.

30 min read·

Créer des APIs Type-Safe de bout en bout avec tRPC et Next.js App Router

Apprenez à créer des APIs entièrement type-safe avec tRPC et Next.js 15 App Router. Ce tutoriel pratique couvre la configuration du routeur, les procédures, le middleware, l'intégration de React Query et les appels côté serveur — le tout sans écrire un seul schéma d'API.

28 min read·