SD
ZodTypeScriptNext.jsNestJSValidation

Zod : validation de données TypeScript dans Next.js et NestJS

Apprenez à utiliser Zod pour valider vos données end-to-end avec TypeScript, dans vos formulaires Next.js et vos DTOs NestJS. Schémas partagés, 0 runtime error.

AMAlexis Mouchon8 min de lecture

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 :


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 des try/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.