Comment valider un formulaire côté serveur

Apprends à valider les formulaires côté serveur avec Zod + Conform : unicité, logique métier, erreurs asynchrones et UX préservée grâce à submission.reply() et superRefine.

4 min read
Déverrouillez votre potentiel

avec React Router 7

Vous en avez marre de...

❌ perdre du temps à chercher des informations éparpillées
❌ ne pas avoir de retour sur votre progression
Assistant IA spécialisé

Posez vos questions 24/7 à notre IA experte en React Router 7

Quiz interactifs

Validez vos acquis avec des quiz personnalisés et un feedback instantané

9 modules
45 leçons
Accès à vie
299.00
-50%

Pourquoi faire une validation serveur ?

La validation côté client (Conform + Zod) protège l’utilisateur des fautes de frappe, mais elle ne peut pas :

  1. vérifier l’unicité d’une donnée (email, slug, …) ;
  2. interroger une base ou une API tierce ;
  3. garantir l’intégrité en cas d’appel API direct (Postman, cURL…).

Bref : la dernière ligne de défense vit sur le serveur. Dans cette leçon tu vas apprendre à :

  • ré-utiliser ton schéma Zod dans l’action() ;
  • renvoyer un objet SubmissionResult interprétable par Conform ;
  • gérer plusieurs intentions (création / mise à jour) avec un discriminated union ;
  • afficher instantanément les messages sans perdre la saisie.

1. Construis un schéma partagé

app/routes/users+/$userSlug.tsx
1
import { z } from "zod"
2
3
export const UserCreateSchema = z.object({
4
action: z.literal("create-user"),
5
firstName: z.string().min(2, "2 caractères minimum"),
6
lastName: z.string().min(2, "2 caractères minimum"),
7
})
8
9
export const UserUpdateSchema = z.object({
10
action: z.literal("update-user"),
11
firstName: z.string().min(2),
12
lastName: z.string().min(2),
13
})
14
15
export const UserActionsSchema = z.discriminatedUnion(
16
"action",
17
[UserCreateSchema, UserUpdateSchema],
18
)

Discriminated union 👉 un seul schéma, plusieurs cas, distingués par la clef action. Conform l’utilisera côté client et nous côté serveur : zéro duplication !


2. Parse le FormData avec Zod côté serveur

app/routes/users+/$userSlug.tsx
1
import { parseWithZod } from "@conform-to/zod";
2
import { data } from "react-router";
3
4
export async function action({ request, params }) {
5
const formData = await request.formData()
6
7
const submission = await parseWithZod(formData, {
8
async: true, // autorise le code asynchrone
9
schema: UserActionsSchema.superRefine(async (values, ctx) => {
10
if (values.action === "create-user") {
11
const slug = createSlug({ firstName: values.firstName })
12
const exists = await getUserBySlug({ slug })
13
if (exists) {
14
ctx.addIssue({
15
path: ["firstName"],
16
code: "custom",
17
message: "Cet utilisateur existe déjà.",
18
})
19
}
20
}
21
}),
22
})
23
24
if (submission.status !== "success") {
25
// ^ renvoie un format compris par Conform
26
return data({ result: submission.reply() }, { status: 400 })
27
}
28
29
/* …traitement en BDD, puis redirect … */
30
}

À retenir

  • parseWithZod(formData) convertit FormData → objet et applique le schéma.
  • superRefine te donne un hook async pour parler à ta base.
  • submission.reply() sérialise les erreurs pour Conform.

3. Exploite le résultat côté client

app/routes/users+/$userSlug.tsx
1
import { useActionData, useLoaderData } from "react-router"
2
import { useForm, getFormProps, getInputProps } from "@conform-to/react"
3
4
export default function UserForm() {
5
const { isNew, user } = useLoaderData<typeof loader>()
6
const actionData = useActionData<typeof action>()
7
8
const [form, fields] = useForm({
9
id: "user-form",
10
lastResult: actionData?.result, // 🔗 lien serveur → client
11
defaultValue: {
12
firstName: user?.firstName,
13
lastName: user?.lastName,
14
action: isNew ? "create-user" : "update-user",
15
},
16
})
17
18
return (
19
<Form method="POST" {...getFormProps(form)} className="space-y-4">
20
<input { ...getInputProps(fields.action, { type: "hidden" }) } />
21
{/* firstname */}
22
<Field
23
labelProps={{ children: "Prénom" }}
24
inputProps={ getInputProps(fields.firstName, { type: "text" }) }
25
errors={fields.firstName.errors}
26
/>
27
{/* lastname */}
28
<Field
29
labelProps={{ children: "Nom" }}
30
inputProps={ getInputProps(fields.lastName, { type: "text" }) }
31
errors={fields.lastName.errors}
32
/>
33
<button className="btn-primary">
34
{isNew ? "Ajouter" : "Modifier"}
35
</button>
36
</Form>
37
)
38
}

Conform mélange :

  1. erreurs client (validations synchrones) ;
  2. erreurs serveur (format Submission.reply()).

Résultat : l’utilisateur reste sur place, ses champs ne sont pas réinitialisés, et le premier input en erreur reprend automatiquement le focus. ✨


4. Router la logique avec switch

Les valeurs validées vivent dans submission.value. Grâce au discriminant, TypeScript sait exactement de quel cas il s’agit !

app/routes/users+/$userSlug.tsx {20-42}
1
switch (submission.value.action) {
2
case "create-user": {
3
const { slug } = await addUser(submission.value)
4
return redirect(href("/users/:userSlug", { userSlug: slug }))
5
}
6
case "update-user": {
7
const { slug } = await updateUser({
8
slug: params.userSlug,
9
userData: submission.value,
10
})
11
return redirect(href("/users/:userSlug", { userSlug: slug }))
12
}
13
}

5. Quand le composant doit-il se re-monter ?

Conform stocke les default values à l’instanciation du hook useForm. Si tu changes de slug sans recréer le composant tu conserves l’ancien état. La parade : clé dynamique sur le composant enfant.

app/routes/users+/$userSlug.tsx
1
export default function User() {
2
const { userSlug } = useParams()
3
return <UserForm key={userSlug} /> // force un remount
4
}

Points clés à mémoriser

  • Validation serveur obligatoire pour tout ce qui touche à la base ou à l’unicité.
  • parseWithZod + superRefine = validation type-safe et async en une ligne.
  • Renvoyez toujours submission.reply() pour que Conform aligne UI et back-end.
  • Le schéma Zod devient la source de vérité unique : typed API, validation, autocomplétion VS Code.
  • Un composant clef dynamique (key={userSlug}) garantit la synchronisation des valeurs par défaut lors d’une navigation interne.
Premium
Quiz interactif
Testez vos connaissances et validez votre compréhension du module avec notre quiz interactif.
1

Comprendre les concepts fondamentaux

Quelle est la principale différence entre les composants client et serveur dans React ?

Les composants client s'exécutent uniquement dans le navigateur
Les composants serveur peuvent utiliser useState
Les composants client sont plus rapides
Il n'y a aucune différence significative
2

Optimisation des performances

Quelle technique est recommandée pour éviter les rendus inutiles dans React ?

Utiliser React.memo pour les composants fonctionnels
Ajouter plus d'états locaux
Éviter d'utiliser les props
Toujours utiliser les class components
3

Architecture des données

Quel hook permet de gérer les effets de bord dans un composant React ?

useEffect
useState
useMemo
useContext
4

Gestion des erreurs

Comment implémenter la gestion des erreurs pour les requêtes API dans React ?

Utiliser try/catch avec async/await
Ignorer les erreurs
Toujours afficher un message d'erreur
Rediriger l'utilisateur
5

Déploiement et CI/CD

Quelle est la meilleure pratique pour déployer une application React en production ?

Utiliser un service CI/CD comme GitHub Actions
Copier les fichiers manuellement via FTP
Envoyer le code source complet
Ne jamais mettre à jour l'application

Débloquez ce quiz et tous les autres contenus premium en achetant ce cours