Validation Zod avec Claude Code pour TypeScript
Implémentez Zod avec Claude Code pour formulaires, APIs, variables d'environnement, webhooks et tests.
Pourquoi valider les données à l’exécution
TypeScript protège le code pendant le développement, mais il ne vérifie pas automatiquement les données qui arrivent pendant l’exécution. Un formulaire, une requête API, un Webhook payload, process.env ou un objet juste avant une insertion en base peuvent être incomplets ou mal formés. La validation runtime consiste à contrôler ces valeurs avant de les transmettre à la logique métier. Zod sert à écrire un schema, c’est-à-dire un contrat de données, puis à en déduire le type TypeScript.
Claude Code est efficace pour ce travail parce que la validation est très structurée. Il faut lister les champs, types, limites, messages d’erreur, entrées externes et tests. Une demande vague produit souvent un schema trop simple. Une demande précise comme “formulaire, API request/response, variables d’environnement, Webhook payload, validation avant DB et tests” donne un résultat beaucoup plus exploitable. Pour les détails de comportement, vérifiez la documentation officielle de Zod et la documentation Next.js Route Handlers.
unknown input
-> Zod schema
-> safeParse
-> typed data
-> business logic
-> response schema
-> client
Le point central est de ne pas faire confiance à une entrée externe. Elle reste unknown tant que Zod ne l’a pas acceptée.
Les cas d’usage à donner à Claude Code
Chaque frontière a ses contraintes. Le formulaire doit afficher des erreurs utiles. L’API doit retourner 400. Les variables d’environnement doivent échouer au démarrage. Un webhook doit vérifier la signature avant d’examiner le payload.
| Cas d’usage | Entrée | Ce que Zod protège |
|---|---|---|
| Formulaire | Saisie navigateur | Champs vides, email, longueur, consentement |
| API request/response | request.json() et JSON retourné | Payload invalide, contrat de réponse, états |
| Variables d’environnement | process.env | Secrets manquants, URLs, port |
| Webhook payload | POST d’un service externe | Type d’événement, IDs, montants, signature |
| Avant insertion DB | Objet transformé par l’app | Champs persistables, enums, IDs requis |
Donnez ce tableau à Claude Code. Ne forcez pas un schema unique pour toutes les couches. Un formulaire contient parfois des champs de confirmation; une insertion DB a besoin d’un contrat différent. Réutilisez de petites briques comme emailSchema ou idSchema, mais évitez de partager tout l’objet sans réflexion. Pour les formulaires, consultez aussi le guide interne React Hook Form. Pour les APIs typées, voyez le guide tRPC.
Créer un schema Zod de base
Voici un exemple de formulaire de contact. Il est copiable et peut être adapté à votre projet. Demandez à Claude Code de garder les messages côté utilisateur et de ne pas ajouter de champs de base de données dans ce schema de formulaire.
// src/lib/schemas/contact.ts
import { z } from "zod";
export const contactFormSchema = z.object({
name: z
.string()
.trim()
.min(1, "Saisissez votre nom")
.max(80, "Le nom doit contenir 80 caractères ou moins"),
email: z
.string()
.trim()
.email("Saisissez une adresse email valide"),
plan: z.enum(["trial", "team", "enterprise"]),
message: z
.string()
.trim()
.min(10, "Le message doit contenir au moins 10 caractères")
.max(2000, "Le message doit contenir 2000 caractères ou moins"),
agreedToPolicy: z
.boolean()
.refine((value) => value, "L'accord avec la politique de confidentialité est requis"),
});
export type ContactFormInput = z.infer<typeof contactFormSchema>;
trim() empêche un champ rempli seulement d’espaces de passer. z.enum limite une chaîne à des valeurs connues. z.infer évite de maintenir séparément un type TypeScript qui pourrait dériver du schema réel.
Transformer safeParse en erreur HTTP
parse lance une exception si la validation échoue. C’est utile pour la configuration de démarrage. safeParse retourne un résultat contrôlé, pratique pour les formulaires et les APIs qui doivent retourner une erreur 400.
// src/lib/validation.ts
import { z } from "zod";
export type ValidationProblem = {
path: string;
message: string;
};
export function validateInput<TSchema extends z.ZodTypeAny>(
schema: TSchema,
input: unknown,
):
| { ok: true; data: z.infer<TSchema> }
| { ok: false; status: 400; errors: ValidationProblem[] } {
const result = schema.safeParse(input);
if (!result.success) {
return {
ok: false,
status: 400,
errors: result.error.issues.map((issue) => ({
path: issue.path.join(".") || "_root",
message: issue.message,
})),
};
}
return { ok: true, data: result.data };
}
Cette fonction centralise le format des erreurs. Dans un produit multilingue, elle peut retourner des clés de traduction au lieu de messages finaux.
Valider request et response dans Next.js
Une route API doit valider l’entrée avant la logique métier. Valider la réponse est également utile, car cela détecte un changement accidentel du contrat public.
// app/api/contact/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
import {
contactFormSchema,
type ContactFormInput,
} from "@/lib/schemas/contact";
import { validateInput } from "@/lib/validation";
const contactResponseSchema = z.object({
id: z.string().min(1),
status: z.enum(["queued"]),
});
async function saveContact(input: ContactFormInput) {
// Replace this with your database insert.
return {
id: `contact_${Date.now()}`,
status: "queued" as const,
email: input.email,
};
}
export async function POST(request: Request) {
const body: unknown = await request.json();
const validated = validateInput(contactFormSchema, body);
if (!validated.ok) {
return NextResponse.json(
{ message: "Vérifiez les données envoyées", errors: validated.errors },
{ status: validated.status },
);
}
const saved = await saveContact(validated.data);
const response = contactResponseSchema.parse(saved);
return NextResponse.json(response, { status: 201 });
}
Pour les webhooks, l’ordre est important. Vérifiez la signature, validez le payload, puis déclenchez la logique. Demandez à Claude Code de séparer verifySignature, webhookPayloadSchema et handleWebhookEvent.
Valider les variables d’environnement au démarrage
Les variables d’environnement sont des chaînes ou undefined. Si DATABASE_URL manque, l’application doit échouer au démarrage plutôt que pendant la première requête utilisateur.
// src/env.ts
import { z } from "zod";
const envSchema = z.object({
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
DATABASE_URL: z.string().url("DATABASE_URL must be a valid URL"),
NEXT_PUBLIC_APP_URL: z.string().url("NEXT_PUBLIC_APP_URL must be a valid URL"),
WEBHOOK_SECRET: z.string().min(32, "WEBHOOK_SECRET must be at least 32 chars"),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
});
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error(
"Invalid environment variables",
parsed.error.flatten().fieldErrors,
);
throw new Error("Invalid environment variables");
}
export const env = parsed.data;
z.coerce.number() est adapté ici parce que l’entrée est une chaîne. Évitez de l’utiliser partout. Une coercition excessive peut transformer une donnée douteuse en donnée acceptée.
Connecter Zod à react-hook-form
La validation côté client améliore l’expérience, mais elle ne remplace jamais la validation serveur. Le navigateur peut être contourné.
// src/components/contact-form.tsx
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import {
contactFormSchema,
type ContactFormInput,
} from "@/lib/schemas/contact";
export function ContactForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<ContactFormInput>({
resolver: zodResolver(contactFormSchema),
defaultValues: {
name: "",
email: "",
plan: "trial",
message: "",
agreedToPolicy: false,
},
});
async function onSubmit(values: ContactFormInput) {
const response = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
});
if (!response.ok) {
throw new Error("Failed to send contact request");
}
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} aria-invalid={Boolean(errors.name)} />
{errors.name && <p>{errors.name.message}</p>}
<input {...register("email")} aria-invalid={Boolean(errors.email)} />
{errors.email && <p>{errors.email.message}</p>}
<select {...register("plan")}>
<option value="trial">Trial</option>
<option value="team">Team</option>
<option value="enterprise">Enterprise</option>
</select>
<textarea {...register("message")} />
{errors.message && <p>{errors.message.message}</p>}
<label>
<input type="checkbox" {...register("agreedToPolicy")} />
I agree to the privacy policy
</label>
{errors.agreedToPolicy && <p>{errors.agreedToPolicy.message}</p>}
<button type="submit" disabled={isSubmitting}>
Send
</button>
</form>
);
}
Quand Claude Code modifie l’UI, indiquez clairement qu’il ne doit pas supprimer la validation serveur. C’est une régression fréquente.
Prompt de revue pour Claude Code
Après l’implémentation, utilisez Claude Code comme relecteur ciblé.
Review only the Zod validation design in these files.
Check:
1. Every external input is treated as unknown until safeParse or parse succeeds.
2. Route Handlers return 400 with field-level errors for invalid requests.
3. Environment variables fail fast at startup.
4. coerce and transform are used only where the input source is clear.
5. Form schemas, API schemas, and database insert schemas are not over-shared.
6. Error messages are user-facing and ready for localization.
Do not refactor unrelated business logic.
Return findings with file paths, risk level, and a minimal patch suggestion.
Ce prompt vérifie les erreurs les plus coûteuses: confiance excessive dans les types TypeScript, mauvais usage de parse, coercition trop large, effets secondaires dans transform, messages non localisables et réutilisation excessive de schemas.
Tester le contrat
Un schema est un contrat produit. Testez un cas valide et plusieurs cas invalides afin que les changements futurs ne réduisent pas la sécurité.
// src/lib/schemas/contact.test.ts
import { describe, expect, it } from "vitest";
import { contactFormSchema } from "./contact";
describe("contactFormSchema", () => {
it("accepts a valid contact request", () => {
const result = contactFormSchema.safeParse({
name: "Masa",
email: "masa@example.com",
plan: "team",
message: "I want to introduce Claude Code to my team.",
agreedToPolicy: true,
});
expect(result.success).toBe(true);
});
it("rejects invalid email and short message", () => {
const result = contactFormSchema.safeParse({
name: "Masa",
email: "not-an-email",
plan: "team",
message: "short",
agreedToPolicy: true,
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues.map((issue) => issue.path.join("."))).toEqual(
expect.arrayContaining(["email", "message"]),
);
}
});
});
Pour la validation avant DB, testez l’objet final juste avant la persistance. Vous repérerez les changements de noms, d’enums ou de transformation.
Pièges fréquents
Premier piège: les types TypeScript seuls ne valident rien à l’exécution. request.json() as ContactFormInput masque le risque.
Deuxième piège: mélanger parse et safeParse. Les entrées utilisateur appellent souvent safeParse; les variables d’environnement peuvent arrêter le processus.
Troisième piège: abuser de coerce. Il est utile pour query strings et variables d’environnement, mais dangereux si la source n’est pas claire.
Quatrième piège: mettre des effets secondaires dans transform. Une transformation doit rester pure; les écritures DB et emails viennent après.
Cinquième piège: oublier les messages et la localisation. Un produit multilingue doit prévoir des clés de message ou une couche de traduction.
Sixième piège: réutiliser un schema trop large. Formulaire, API, webhook et DB ne représentent pas le même contrat.
Conseil et note de vérification
Si vos validations sont dispersées entre formulaires, webhooks, routes API et insertion DB, Claude Code Lab peut aider à définir les couches de schema, les prompts de revue et les tests. Pour un accompagnement, consultez la page anglaise de consulting et formation.
Les exemples ont été relus le 2026-06-02 avec les documents officiels Zod et Next.js Route Handlers. Ils supposent un projet TypeScript avec zod, react-hook-form, @hookform/resolvers et vitest. En mission réelle, Masa ajoute aussi authentification, CSRF ou signature webhook, contraintes DB et tests d’échec pour chaque frontière externe.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.