zod avec React
Validation runtime dans une application React
On a vu dans le chapitre précédent comment zod permet de valider des données au runtime. Mais dans une vraie application React, où est-ce qu'on branche tout ça concrètement ?
Les deux cas d'usage les plus fréquents sont :
- Les formulaires : valider ce que l'utilisateur saisit avant de l'envoyer au serveur.
- Les appels API : valider ce que le serveur nous renvoie avant de l'afficher dans l'interface.
Valider les réponses d'API
On a vu avec TypeScript qu'on pouvait typer le retour d'un fetch avec un générique :
const users = await fetchData<User[]>('https://api.github.com/users');Le problème, c'est que c'est un acte de foi. On dit à TypeScript : « fais-moi confiance, la réponse aura cette forme ». Si l'API change son contrat demain, TypeScript ne vous protégera pas. L'application plantera au runtime avec un undefined is not a function bien classique.
C'est ici que zod entre en jeu. Au lieu de faire confiance, on vérifie.
import { } from 'zod';
const = .({
: .(),
: .(),
: .(),
: .().(),
});
/* On peut extraire le type TypeScript directement depuis le schéma.
Plus besoin de maintenir un type ET un schéma séparément. */
type = .<typeof >;
const = .();
type = .<typeof >;Intégrer dans un hook de data fetching
On peut brancher zod directement dans notre queryFn de React Query. Si la réponse de l'API ne correspond pas au schéma, la requête échouera proprement au lieu de laisser passer des données corrompues dans toute l'application.
import { useQuery } from '@tanstack/react-query';
import { z } from 'zod';
const ProductSchema = z.object({
id: z.string(),
name: z.string(),
price: z.number(),
imageUrl: z.string().url(),
});
type Product = z.infer<typeof ProductSchema>;
const ProductListSchema = z.array(ProductSchema);
const fetchProducts = async (): Promise<Product[]> => {
const res = await fetch('/api/products');
if (!res.ok) throw new Error('Erreur réseau');
const json = await res.json();
/* Au lieu de retourner json directement, on le valide.
Si le contrat de l'API a changé, on le saura IMMÉDIATEMENT. */
return ProductListSchema.parse(json);
};
export const useProducts = () => {
return useQuery<Product[], Error>({
queryKey: ['products'],
queryFn: fetchProducts,
});
};L'avantage est double :
- Détection immédiate : Si un champ manque ou change de type dans la réponse, zod lève une erreur explicite au lieu de laisser un
undefinedse propager silencieusement dans l'UI. - Single source of truth : Le schéma zod est la source unique. On en dérive le type TypeScript avec
z.infer. Plus de risque de désynchronisation entre le type et la réalité.
Valider les formulaires
L'autre cas d'usage incontournable, c'est la validation des formulaires. En React, la librairie standard pour ça est React Hook Form, et elle s'intègre nativement avec zod via le resolver @hookform/resolvers/zod.
Le principe
Au lieu d'écrire vos règles de validation à la main dans chaque champ (required, minLength, pattern...), vous déclarez un schéma zod unique et React Hook Form s'en sert comme source de vérité.
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
/* 1. On déclare le schéma de validation */
const ContactSchema = z.object({
name: z.string().min(2, 'Le nom doit faire au moins 2 caractères'),
email: z.string().email('Adresse email invalide'),
message: z.string().min(10, 'Le message doit faire au moins 10 caractères'),
});
/* 2. On en déduit le type TypeScript */
type ContactForm = z.infer<typeof ContactSchema>;
export const ContactPage = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<ContactForm>({
/* 3. On branche le schéma zod comme resolver */
resolver: zodResolver(ContactSchema),
});
const onSubmit = (data: ContactForm) => {
/* 'data' est garanti conforme au schéma ici */
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Nom</label>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
</div>
<div>
<label>Email</label>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<label>Message</label>
<textarea {...register('message')} />
{errors.message && <span>{errors.message.message}</span>}
</div>
<button type="submit">Envoyer</button>
</form>
);
};Tout est centralisé dans le schéma : les types, les contraintes de validation, et les messages d'erreur. Si on ajoute un champ au formulaire, on l'ajoute au schéma et tout suit automatiquement.
Validations avancées
zod ne se limite pas aux validations basiques. On peut exprimer des règles métier complexes directement dans le schéma :
const RegisterSchema = z.object({
email: z.string().email(),
password: z.string()
.min(8, 'Le mot de passe doit faire au moins 8 caractères')
.regex(/[A-Z]/, 'Doit contenir au moins une majuscule')
.regex(/[0-9]/, 'Doit contenir au moins un chiffre'),
confirmPassword: z.string(),
}).refine(
(data) => data.password === data.confirmPassword,
{
message: 'Les mots de passe ne correspondent pas',
path: ['confirmPassword'], /* L'erreur sera attachée au bon champ */
}
);La méthode .refine() permet d'ajouter des validations qui dépendent de plusieurs champs en même temps, chose difficile à faire proprement avec des validateurs champ par champ.
Avec FSD
Dans une architecture Feature Sliced Design, les schémas zod ont leur place dans le segment model de vos entités. Le schéma décrit la forme de votre donnée métier, c'est de la logique modèle.
entities/product/
├── api/
│ └── productApi.ts /* fetchProducts avec .parse() */
├── model/
│ ├── productSchema.ts /* Le schéma zod + le type inféré */
│ └── types.ts /* (optionnel) autres types métier */
├── ui/
│ └── ProductCard.tsx
└── index.ts/* entities/product/model/productSchema.ts */
import { z } from 'zod';
export const ProductSchema = z.object({
id: z.string(),
name: z.string(),
price: z.number().positive(),
imageUrl: z.string().url(),
});
export type Product = z.infer<typeof ProductSchema>;/* entities/product/api/productApi.ts */
import { ProductSchema } from '../model/productSchema';
import { z } from 'zod';
export const fetchProducts = async () => {
const res = await fetch('/api/products');
if (!res.ok) throw new Error('Erreur réseau');
const json = await res.json();
return z.array(ProductSchema).parse(json);
};Le schéma devient la seule source de vérité pour la forme de votre donnée produit. L'API l'utilise pour valider, les composants l'utilisent pour le typage, et les formulaires l'utilisent pour la validation côté client.
En résumé
| Cas d'usage | Sans zod | Avec zod |
|---|---|---|
| Réponse d'API | On fait confiance au type (as User[]) | On valide avec .parse() |
| Formulaire | Règles de validation éparpillées dans le JSX | Un schéma unique et centralisé |
| Type TypeScript | Défini manuellement, peut se désynchroniser | Inféré depuis le schéma (z.infer) |
| Erreur de contrat | Crash silencieux au runtime | Erreur explicite à la validation |
zod agit comme un gardien entre le monde extérieur (APIs, utilisateurs) et votre application React. Combiné avec React Hook Form pour les formulaires et React Query pour le data fetching, il forme le trio standard de validation dans les applications React modernes.