Validation avec Zod

Zod est la bibliothèque de validation TypeScript la plus populaire. Elle offre une validation type-safe avec une excellente expérience développeur.

Pourquoi Zod ?

Zod est devenu le standard de l'industrie pour la validation en TypeScript :

  • Type-safe : Les types TypeScript sont automatiquement inférés
  • Zero-dependency : Aucune dépendance externe
  • Petite taille : ~8kb minifié
  • Très populaire : Utilisé par Vercel, Clerk, Prisma, tRPC, etc.
  • Messages d'erreur clairs : Personnalisables et précis

Installation

bash
npm install zod

C'est tout ! Zod n'a aucune dépendance et fonctionne immédiatement avec TypeScript.

Concepts de base

Créer un schéma

Un schéma définit la structure et les règles de validation de vos données.

tsx
import { z } from 'zod';

// Schéma simple
const emailSchema = z.string().email();

// Schéma d'objet
const userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(18)
});

Valider des données

tsx
// ✅ Validation réussie
const result = emailSchema.safeParse('user@example.com');
console.log(result.success); // true
console.log(result.data);    // "user@example.com"

// ❌ Validation échouée
const result2 = emailSchema.safeParse('invalid-email');
console.log(result2.success); // false
console.log(result2.error);   // ZodError avec détails

safeParse vs parse

Utilisez toujours safeParse() plutôt que parse() :

  • safeParse() retourne { success: boolean, data?: T, error?: ZodError }
  • parse() lance une exception si la validation échoue

safeParse() est plus sûr et plus facile à gérer dans React !

Quelle est la différence principale entre safeParse() et parse() ?

Inférence de types TypeScript

Zod peut automatiquement créer les types TypeScript à partir de vos schémas.

tsx
const userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number()
});

// Type automatiquement inféré !
type User = z.infer<typeof userSchema>;
// équivalent à :
// type User = { name: string; email: string; age: number }

const user: User = {
  name: 'Alice',
  email: 'alice@example.com',
  age: 25
};

Que fait z.infer<typeof userSchema> ?

Types de validation

Strings

tsx
const schema = z.string()
  .min(3, { message: 'Au moins 3 caractères' })
  .max(20, { message: 'Maximum 20 caractères' })
  .email({ message: 'Email invalide' })
  .url({ message: 'URL invalide' })
  .regex(/^[a-z]+$/, { message: 'Seulement des lettres minuscules' })
  .trim(); // Supprime les espaces

Validations courantes :

tsx
z.string().email();           // Email valide
z.string().url();             // URL valide
z.string().min(5);            // Minimum 5 caractères
z.string().max(100);          // Maximum 100 caractères

Comment personnaliser les messages d'erreur dans Zod ?

Numbers

tsx
const schema = z.number()
  .min(0, { message: 'Doit être positif' })
  .max(100, { message: 'Maximum 100' })
  .int({ message: 'Doit être un entier' })
  .positive({ message: 'Doit être positif' });

Booleans, Dates et Enums

tsx
// Boolean
z.boolean();

// Date
z.date()
  .min(new Date('2024-01-01'))
  .max(new Date('2025-12-31'));

// Enum (valeurs limitées)
const roleSchema = z.enum(['admin', 'user', 'guest']);
type Role = z.infer<typeof roleSchema>; // 'admin' | 'user' | 'guest'

Arrays et Objects

tsx
// Array
const tagsSchema = z.array(z.string())
  .min(1, { message: 'Au moins un tag' })
  .max(5, { message: 'Maximum 5 tags' });

// Object
const userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(18)
});

Validation avancée

Champs optionnels et valeurs par défaut

tsx
const userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().optional(),        // Peut être undefined
  bio: z.string().nullable(),        // Peut être null
  theme: z.string().default('light') // Valeur par défaut
});

Quelle est la différence entre .optional() et .nullable() ?

Validation personnalisée avec refine()

Utilisez .refine() pour des règles de validation personnalisées.

tsx
// Validation d'un seul champ
const passwordSchema = z.string()
  .min(8)
  .refine(password => /[A-Z]/.test(password), {
    message: 'Doit contenir au moins une majuscule'
  });

// Comparaison entre champs
const signupSchema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
  message: 'Les mots de passe ne correspondent pas',
  path: ['confirmPassword']
});

Utilisation avec React

Formulaire de connexion avec Zod

Voici un exemple complet d'utilisation de Zod dans un formulaire React.

tsx
import { useState } from 'react';
import { z } from 'zod';

// 1. Définir le schéma
const loginSchema = z.object({
  email: z.string()
    .min(1, { message: 'Email requis' })
    .email({ message: 'Email invalide' }),
  password: z.string()
    .min(8, { message: 'Au moins 8 caractères' })
});

// 2. Inférer le type TypeScript
type LoginForm = z.infer<typeof loginSchema>;

function LoginForm() {
  const [formData, setFormData] = useState<LoginForm>({
    email: '',
    password: ''
  });
  const [errors, setErrors] = useState<Record<string, string>>({});

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFormData({ ...formData, [e.target.name]: e.target.value });
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();

    // 3. Valider avec Zod
    const result = loginSchema.safeParse(formData);

    if (!result.success) {
      // Extraire les erreurs
      const fieldErrors: Record<string, string> = {};
      result.error.errors.forEach(err => {
        if (err.path[0]) {
          fieldErrors[err.path[0].toString()] = err.message;
        }
      });
      setErrors(fieldErrors);
      return;
    }

    // 4. Données validées et type-safe !
    console.log('Données valides:', result.data);
    setErrors({});
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      {errors.email && <span className="error">{errors.email}</span>}

      <input
        type="password"
        name="password"
        value={formData.password}
        onChange={handleChange}
        placeholder="Mot de passe"
      />
      {errors.password && <span className="error">{errors.password}</span>}

      <button type="submit">Se connecter</button>
    </form>
  );
}

Exemple : Schéma d'inscription plus complexe

tsx
const signupSchema = z.object({
  username: z.string()
    .min(3, { message: 'Au moins 3 caractères' })
    .max(20, { message: 'Maximum 20 caractères' }),
  email: z.string().email({ message: 'Email invalide' }),
  password: z.string()
    .min(8, { message: 'Au moins 8 caractères' })
    .regex(/[A-Z]/, { message: 'Au moins une majuscule' }),
  confirmPassword: z.string(),
  age: z.number().int().min(13)
}).refine(data => data.password === data.confirmPassword, {
  message: 'Les mots de passe ne correspondent pas',
  path: ['confirmPassword']
});

type SignupForm = z.infer<typeof signupSchema>;

Zod + React Hook Form (Optionnel)

Section avancée

Cette section est optionnelle pour les débutants. Vous pouvez d'abord maîtriser Zod avec useState avant d'explorer React Hook Form.

React Hook Form s'intègre parfaitement avec Zod via @hookform/resolvers.

bash
npm install react-hook-form @hookform/resolvers
tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
});

type FormData = z.infer<typeof schema>;

function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm<FormData>({
    resolver: zodResolver(schema)
  });

  const onSubmit = (data: FormData) => {
    console.log(data); // Type-safe et validé !
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}

      <input type="password" {...register('password')} />
      {errors.password && <span>{errors.password.message}</span>}

      <button type="submit">Se connecter</button>
    </form>
  );
}

React Hook Form + Zod = Combo parfait

React Hook Form gère les formulaires (performance, re-renders) Zod gère la validation (type-safe, règles)

Ensemble, ils forment la meilleure solution pour les formulaires complexes en React !

Pourquoi utiliser Zod ?

Avantages de Zod par rapport à la validation manuelle

Validation manuelle : Code répétitif, regex manuelles, pas de types automatiques

tsx
// ❌ Validation manuelle
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
  errors.email = 'Email invalide';
}

Avec Zod : Concis, type-safe, facile à maintenir

tsx
// ✅ Avec Zod
const schema = z.object({
  email: z.string().email('Email invalide'),
  password: z.string().min(8)
});

type Form = z.infer<typeof schema>; // Types automatiques !
const result = schema.safeParse(formData);

Avantages clés :

  • ✅ Moins de code
  • ✅ Types TypeScript automatiques
  • ✅ Messages d'erreur structurés
  • ✅ Facile à maintenir

Cas d'usage réels

Valider les données d'une API

Zod est idéal pour valider les réponses d'API et garantir la sécurité des types.

tsx
const apiResponseSchema = z.object({
  users: z.array(z.object({
    id: z.number(),
    name: z.string(),
    email: z.string().email()
  }))
});

const response = await fetch('/api/users');
const data = await response.json();

// Valider les données de l'API
const result = apiResponseSchema.safeParse(data);

if (!result.success) {
  console.error('Données API invalides:', result.error);
  return;
}

// Données type-safe !
const users = result.data.users;

Alternatives à Zod

Autres bibliothèques de validation

Zod n'est pas la seule option. L'alternative principale est :

Yup (https://github.com/jquense/yup)

  • Très populaire, existe depuis longtemps
  • Syntaxe similaire à Zod
  • Moins bon support TypeScript
  • Souvent utilisé avec Formik

Pourquoi choisir Zod ?

  • ✅ Type-safe avec TypeScript
  • ✅ Zero-dependency
  • ✅ Excellent écosystème (tRPC, Prisma, Vercel)
  • ✅ Standard moderne pour React + TypeScript

Bonnes pratiques

Conseils pour bien utiliser Zod

1. Définir les schémas en dehors des composants

tsx
// ✅ Bon : schéma réutilisable
const userSchema = z.object({ ... });

function Component() {
  // Utiliser le schéma ici
}

2. Réutiliser les schémas

tsx
const addressSchema = z.object({
  street: z.string(),
  city: z.string()
});

const userSchema = z.object({
  name: z.string(),
  address: addressSchema // Réutilisation !
});

3. Messages d'erreur personnalisés

tsx
const schema = z.string().min(3, {
  message: 'Le nom doit contenir au moins 3 caractères'
});

4. Utiliser safeParse() au lieu de parse()

tsx
// ✅ Recommandé : safeParse ne lance pas d'exception
const result = schema.safeParse(data);

// ❌ Éviter : parse lance une exception
const data = schema.parse(input);

Récapitulatif

Ce que vous avez appris

Zod - Validation moderne et type-safe :

  1. Installation : npm install zod
  2. Schémas : Définir la structure des données avec z.object(), z.string(), etc.
  3. Validation : Utiliser safeParse() pour valider les données
  4. Types : Inférer automatiquement les types TypeScript avec z.infer
  5. React : Intégrer Zod dans vos formulaires React
  6. React Hook Form : Combo parfait avec @hookform/resolvers/zod
  7. Avantages : Type-safe, concis, maintenable, messages clairs

Pourquoi utiliser Zod ?

  • ✅ Moins de code que la validation manuelle
  • ✅ Types TypeScript automatiques
  • ✅ Validation côté client ET serveur
  • ✅ Standard de l'industrie

Prochaine étape : apprendre à partager l'état entre composants (lifting state up) !