La validation des données est l'une des tâches les plus répétitives — et les plus critiques — du développement web. Entre les formulaires côté client, les appels API côté serveur et les données qui transitent entre les deux, une seule donnée malformée peut faire planter votre app ou créer une faille de sécurité. Zod résout ce problème élégamment, en s'intégrant nativement dans un écosystème TypeScript comme Next.js + NestJS.
Qu'est-ce que Zod, et pourquoi l'utiliser ?
Zod est une librairie de validation de schémas TypeScript-first. Contrairement à des alternatives comme Yup ou Joi, Zod a été conçu dès le départ pour TypeScript : il infère automatiquement les types à partir de vos schémas, sans duplication de code.
import { z } from 'zod'
const UserSchema = z.object({
name: z.string().min(2, 'Minimum 2 caractères'),
email: z.string().email('Email invalide'),
age: z.number().int().min(18, 'Doit être majeur'),
})
// TypeScript infère automatiquement ce type :
type User = z.infer<typeof UserSchema>
// → { name: string; email: string; age: number }
Plus besoin de déclarer une interface TypeScript et un schéma de validation séparément. Zod fait les deux en une seule déclaration.
Les avantages sont concrets :
- Zéro redondance : un schéma unique, un type inféré automatiquement
- Erreurs explicites : messages d'erreur personnalisables, localisables
- Composable : les schémas se combinent, s'étendent, se transforment
- Léger : ~8kb gzippé, tree-shakeable
- Intégration native avec React Hook Form, TanStack Form, et
class-validatorvia des adaptateurs
Intégration dans Next.js App Router
Validation de formulaire avec React Hook Form
La combinaison React Hook Form + Zod via @hookform/resolvers est devenue un standard dans l'écosystème React. Voici un formulaire de contact complet :
npm install zod @hookform/resolvers react-hook-form
// lib/schemas/contact.schema.ts
import { z } from 'zod'
export const ContactSchema = z.object({
name: z.string().min(2, 'Le nom doit contenir au moins 2 caractères'),
email: z.string().email('Adresse email invalide'),
message: z
.string()
.min(20, 'Le message doit faire au moins 20 caractères')
.max(1000, 'Message trop long'),
})
export type ContactFormData = z.infer<typeof ContactSchema>
// components/ContactForm.tsx
'use client'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { ContactSchema, ContactFormData } from '@/lib/schemas/contact.schema'
export function ContactForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<ContactFormData>({
resolver: zodResolver(ContactSchema),
})
const onSubmit = async (data: ContactFormData) => {
const response = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
})
// Traitement de la réponse...
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<input
{...register('name')}
placeholder="Votre nom"
className="w-full rounded-lg border px-4 py-2"
/>
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name.message}</p>}
</div>
<div>
<input
{...register('email')}
type="email"
placeholder="votre@email.com"
className="w-full rounded-lg border px-4 py-2"
/>
{errors.email && <p className="mt-1 text-sm text-red-500">{errors.email.message}</p>}
</div>
<div>
<textarea
{...register('message')}
placeholder="Votre message..."
rows={5}
className="w-full rounded-lg border px-4 py-2"
/>
{errors.message && <p className="mt-1 text-sm text-red-500">{errors.message.message}</p>}
</div>
<button
type="submit"
disabled={isSubmitting}
className="rounded-lg bg-blue-600 px-6 py-3 text-white hover:bg-blue-700 disabled:opacity-50"
>
{isSubmitting ? 'Envoi en cours...' : 'Envoyer'}
</button>
</form>
)
}
Validation dans une Server Action ou une Route Handler
Côté serveur, le même schéma ContactSchema peut valider les données reçues :
// app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { ContactSchema } from '@/lib/schemas/contact.schema'
import { ZodError } from 'zod'
export async function POST(req: NextRequest) {
try {
const body = await req.json()
// Validation — lève une ZodError si les données sont invalides
const data = ContactSchema.parse(body)
// `data` est maintenant typé et validé
await sendContactEmail(data) // votre logique métier
return NextResponse.json({ success: true })
} catch (error) {
if (error instanceof ZodError) {
return NextResponse.json({ errors: error.flatten().fieldErrors }, { status: 400 })
}
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
}
}
Astuce : utilisez
schema.safeParse(data)pour obtenir un résultat{ success: boolean, data?, error? }sans lever d'exception — plus pratique dans les flux où vous préférez gérer les erreurs avec des conditions plutôt que destry/catch.
Intégration dans NestJS avec class-validator
NestJS utilise nativement class-validator et class-transformer pour la validation des DTOs. Zod peut s'y intégrer de deux façons.
Option 1 : ZodValidationPipe (recommandée)
La librairie nestjs-zod fournit un ZodValidationPipe prêt à l'emploi :
npm install nestjs-zod
// dto/create-user.dto.ts
import { createZodDto } from 'nestjs-zod'
import { z } from 'zod'
const CreateUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
password: z
.string()
.min(8, 'Minimum 8 caractères')
.regex(/[A-Z]/, 'Au moins une majuscule')
.regex(/[0-9]/, 'Au moins un chiffre'),
})
export class CreateUserDto extends createZodDto(CreateUserSchema) {}
// users.controller.ts
import { Controller, Post, Body } from '@nestjs/common'
import { ZodValidationPipe } from 'nestjs-zod'
import { CreateUserDto } from './dto/create-user.dto'
@Controller('users')
export class UsersController {
@Post()
@UsePipes(ZodValidationPipe)
async create(@Body() dto: CreateUserDto) {
// dto est typé et validé automatiquement
return this.usersService.create(dto)
}
}
Option 2 : Schémas partagés dans un monorepo
Dans une architecture monorepo (par exemple avec Turborepo), vous pouvez placer vos schémas Zod dans un package partagé et les réutiliser à la fois dans Next.js et NestJS :
packages/
shared-schemas/
src/
user.schema.ts ← utilisé dans frontend ET backend
contact.schema.ts
apps/
web/ ← Next.js
api/ ← NestJS
C'est l'un des avantages majeurs de Zod sur class-validator : les schémas sont de simples objets JavaScript, sans décorateurs, facilement portables entre environnements.
Fonctionnalités avancées à connaître
Transformation de données
Zod ne se contente pas de valider — il peut aussi transformer les données à la volée :
const DateSchema = z.string().transform((str) => new Date(str))
// Input : "2026-04-08" → Output : Date object
const TrimmedString = z.string().trim().toLowerCase()
// Input : " HELLO " → Output : "hello"
Validation conditionnelle avec refine
const PasswordConfirmSchema = 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'],
})
Unions et types discriminés
const NotificationSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('email'), email: z.string().email() }),
z.object({ type: z.literal('sms'), phone: z.string().regex(/^\+33/) }),
])
Conclusion
Zod s'est imposé comme la référence de validation TypeScript pour une bonne raison : il élimine la friction entre les types statiques et la validation runtime. En le combinant avec React Hook Form côté Next.js et nestjs-zod côté NestJS, vous obtenez une chaîne de validation cohérente, du formulaire utilisateur jusqu'à votre base de données, sans répétition de code.
Si vous travaillez sur un projet fullstack TypeScript en 2026 et que vous ne validez pas encore vos données avec Zod, c'est le bon moment pour l'adopter.
Vous avez un projet web et vous souhaitez une architecture propre, typée et robuste dès le départ ? Contactez-moi pour en discuter — je serai ravi d'étudier votre besoin.