Validation des entrées côté serveur dans Express avec les chaînes express-validator
Découvrez pourquoi les APIs Express doivent valider les entrées sur le serveur, comment les chaînes express-validator enregistrent les erreurs pour validationResult, et comment structurer une route de production.
Sources: Stop Trusting User Input: A Beginner’s Guide to `express-validator` in Express.js
Tôt ou tard, toute API Express qui accepte des entrées reçoit quelque chose comme "age": "banana", et le serveur soit plante, soit, pire encore, enregistre cette valeur. La validation est le mécanisme qui empêche cela, et il est essentiel de la mettre en œuvre correctement au niveau du backend, quel que soit le soin apporté aux formulaires côté frontend. Ce guide explique ce qu’est la validation et pourquoi c’est au serveur de s’en charger, présente l’approche manuelle ainsi que ses limites, puis décrit le fonctionnement interne de express-validator, les méthodes les plus fréquemment utilisées, et la manière dont une route réaliste combine validation, nettoyage des données et vérifications basées sur la base de données.
Que signifie la validation
La validation consiste à vérifier que les données reçues ont la forme et le contenu attendus avant que votre code ne s’en serve. Une analogie utile est le contrôle à l’entrée d’un lieu : peu importe à quel point une personne semble sûre d’elle, le personnel vérifie son identité ; si celle-ci n’est pas valide, elle n’est pas admise. La validation applique ce même contrôle à chaque champ d’une demande. S’agit-il réellement d’une adresse e-mail ? Est-ce vraiment un numéro ? Le champ est-il même présent ?
Prenons l’exemple d’une extrémité de connexion. Un corps de demande bien formaté pourrait ressembler à ceci :
{
"name": "Rahul Sharma",
"email": "rahul@example.com",
"age": 25,
"password": "supersecret123"
}
Le valider signifie confirmer que email est formaté comme une adresse e-mail, que age est un nombre et non du texte, et que password atteint une longueur minimale qui en fait plus qu’une simple formalité.
Pourquoi le backend ne peut pas compter sur les vérifications frontend
Une objection courante est que le formulaire de inscription marque déjà les champs comme obligatoires et vérifie le format e-mail dans le navigateur. Cela ne suffit pas, car du point de vue d’un attaquant, l’interface utilisateur est optionnelle. N’importe qui peut appeler votre API directement avec Postman, curl ou un script pour contourner toutes les règles de votre interface. Si le serveur ne vérifie pas les données lui-même, il fait en réalité confiance à quiconque envoie la demande.
Les avantages concrets de la validation côté serveur :
- Cela protège vos données. Lorsqu’une colonne attend un nombre et reçoit
"twenty-five", vous obtenez soit une erreur, soit, pire encore, un enregistrement mal formaté qui provoquera des bugs plus tard.
La division du travail est simple : la validation frontend existe pour aider les utilisateurs, tandis que la validation backend existe pour protéger l’application. Les deux sont utiles, mais seule la couche backend est obligatoire.
Validation manuelle et raisons de son incapacité à permettre l’expansion
Il vaut la peine de commencer par examiner la version manuelle. Voici le début d’une route de registration qui récupère les champs depuis le corps du message analysé (cela suppose que un analyseur de corps JSON tel que express.json() est enregistré) :
app.post("/register", (req, res) => {
const { name, email, age, password } = req.body;
Chaque champ dispose alors de sa propre clause de protection qui renvoie une réponse 400 :
if (!name || typeof name !== "string" || name.trim().length === 0) {
return res.status(400).json({ error: "Name is required" });
} if (!email || !email.includes("@")) {
return res.status(400).json({ error: "A valid email is required" });
} if (!age || typeof age !== "number" || age < 18) {
return res.status(400).json({ error: "Age must be a number and at least 18" });
} if (!password || password.length < 8) {
return res.status(400).json({ error: "Password must be at least 8 characters" });
} // If we made it here, the data is safe to use
res.json({ message: "User registered successfully" });
});
Pour une petite application, cela est tout à fait acceptable. Cependant, à mesure que le codebase s’agrandit, les problèmes s’accumulent :
- Chaque route qui accepte des entrées a besoin de son propre bloc d’instructions
if. - Les vérifications sont copiées d’une route à l’autre, et une correction appliquée dans l’une peut facilement être oubliée dans les autres.
null, de mauvais types, ainsi que des valeurs trop courtes ou trop longues, devient rapidement fastidieux.La version manuelle présente également des lacunes subtiles. email.includes("@") accepte de nombreux adresses invalides, et l’appel à .includes sur une valeur qui n’est pas une chaîne provoque une erreur. Ce type de vérification répétitive et sujette aux erreurs est précisément ce qu’une bibliothèque devrait gérer.
Ce que propose express-validator
express-validator est un ensemble de middleware Express construit sur la bibliothèque validator.js. Au lieu d’écrire de la logique conditionnelle pour chaque champ, vous déclarez des règles, et la bibliothèque évalue les données de la requête par rapport à celles-ci. Commencez par importer les deux fonctions principales :
const { body, validationResult } = require("express-validator");
La même route de registration, réécrite avec des règles déclaratives, ressemble à ceci :
app.post(
"/register",
[
body("name").trim().notEmpty().withMessage("Name is required"),
body("email").isEmail().withMessage("A valid email is required"),
body("age").isInt({ min: 18 }).withMessage("Age must be at least 18"),
body("password").isLength({ min: 8 }).withMessage("Password must be at least 8 characters"),
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
} res.json({ message: "User registered successfully" });
}
);
Les règles sont maintenant formulées de manière presque courante : le nom ne doit pas être vide après suppression des espaces en trop, l’adresse e-mail doit être valide, l’âge doit être un entier d’au moins 18 ans, et le mot de passe doit comporter au moins huit caractères. Le gestionnaire final ne traite qu’une seule question : y a-t-il eu une erreur ?
Que se passe-t-il lorsqu’une requête arrive
La confusion la plus fréquente chez les débutants est de penser que express-validator rejette automatiquement les requêtes incorrectes. Ce n’est pas le cas. Il collecte des informations sur l’erreur et vous laisse entièrement gérer la réponse. La séquence pour chaque requête est la suivante :
- Express lance la chaîne de middleware définie pour la route et exécute chaque élément dans l’ordre.
body("email").isEmail() est exécuté, il lit la valeur de email dans req.body et la vérifie. En cas d’échec, aucune erreur n’est levée et la requête ne s’arrête pas ; la chaîne enregistre simplement l’erreur dans l’objet de requête avant de continuer.validationResult(req) lit les erreurs enregistrées.C’est une fonction ordinaire, pas un middleware, donc vous l’appeliez à l’intérieur de votre gestionnaire de requêtes. Elle ne valide rien elle-même ; elle se contente de rapporter ce que les chaînes ont déjà collecté.errors.isEmpty() renvoie false, vous envoyez une réponse 400 avec des détails. Sinon, vous passez à la véritable tâche.La conception privilégie la flexibilité par rapport à la facilité d’utilisation. Comme le rejet n’est pas automatique, vous contrôlez le format et le moment de la réponse, et vous pouvez exécuter d’autres logiques entre les vérifications et la décision finale. Le compromis est que l’oubli d’appeler validationResult() désactive silencieusement la validation pour cette route, c’est pourquoi de nombreuses équipes enveloppent cette vérification dans un petit middleware réutilisable.
Les quatre éléments de base
Lorsque vous connaissez ces composants, la bibliothèque devient simple à utiliser.
Chaînes de validation
Des fonctions telles que body(), query(), param() et check() créent une chaîne : une séquence de règles reliées par des points, comme body("email").trim().isEmail(). Chacune cible une partie différente de la requête :
body()litreq.body, la charge utile JSON ou de formulaire.query()litreq.query, la chaîne de requête après?, comme dans?page=2.param()litreq.params, les paramètres de route tels que:iddans/users/:id.check()examine simultanément toutes les parties de la requête.
Vérificateurs
Les validateurs sont les fonctions qui testent une valeur : isEmail(), isInt(), isLength(), isAlphanumeric() et bien d’autres encore. La plupart font appel à validator.js, qui contient déjà une logique éprouvée et bien testée pour les formats courants.
Les nettoyeurs
Les nettoyeurs transforment les données plutôt que de se contenter de les vérifier. trim() supprime l’espace insécable qui entoure la valeur, normalizeEmail() convertit une adresse en une forme canonique, et escape() remplace les caractères significatifs pour HTML par leurs équivalents en entités afin qu’ils ne puissent pas être interprétés comme du markup. Les nettoyeurs peuvent être combinés avec des validateurs dans la même chaîne, et l’ordre est important : un trim() placé avant notEmpty() signifie que une valeur composée uniquement d’espaces échouera le test.
Le stockage des résultats par requête
Lorsqu’une chaîne est exécutée, ses échecs sont associés à cet objet req spécifique. validationResult(req) lit simplement dans ce stockage, c’est pourquoi il a besoin du même objet de requête que celui utilisé par les chaînes.
En résumé, le flux est le suivant : une requête arrive, les chaînes s’exécutent et enregistrent discrètement les problèmes, le gestionnaire demande les résultats, et vous choisissez la réponse. Si vous comparez cette approche à la validation basée sur des schémas, notre comparaison de Zod et express-validator examine les avantages et inconvénients.
Les méthodes qui couvrent la plupart des projets réels
Vous n’avez pas besoin de toute l’API. Un petit ensemble suffit pour gérer la plupart des cas.
Sélecteurs de position
body(field)pour les champs dans le corps de la requêtequery(field)pour les paramètres de la chaîne de recherche
param(field) pour les paramètres de routeVérificateurs courants
.notEmpty(): la valeur ne doit pas être vide.isEmail(): la valeur doit correspondre à une adresse e-mail valide.isInt({ min, max }): la valeur doit être un entier, éventuellement dans des limites définies.isLength({ min, max }): vérifie la longueur d’une chaîne.isString(): la valeur doit être une chaîne de caractères.isBoolean(): la valeur doit être de type booléen.matches(regex): la valeur doit correspondre à un motif personnalisé.isIn([...values]): la valeur doit faire partie d’un ensemble prédéfini, utile pour des champs commestatusqui peut être soit"active", soit"inactive"
Sanitiseurs courants
.trim(): supprime les espaces en début et en fin de chaîne.escape(): échappe les caractères HTML afin de réduire le risque d’injection.normalizeEmail(): normalise le format des adresses e-mail.toInt()et.toFloat(): convertissent les entrées de type chaîne en nombres
Règles personnalisées
.custom((value, { req }) => { ... }): permet d’implémenter sa propre règle lorsque aucun validateur intégré ne convient, par exemple pour vérifier qu’un nom d’utilisateur n’est pas déjà enregistré
Rapport d’erreurs
.withMessage("text"): définit le message associé à la règle précédentevalidationResult(req): rassemble toutes les erreurs relatives à la requête en courserrors.array(): convertit ces erreurs en un tableau simple adapté à une réponse JSON
Une route de style production
L’exemple suivant correspond davantage à ce qu’une API réelle contient : une route qui crée un article de blog, combinant validation, nettoyage des données et une vérification via la base de données. Tout d’abord, les importations, où Post représente un modèle tel qu’un schéma Mongoose :
const express = require("express");
const { body, validationResult } = require("express-validator");
const Post = require("./models/Post"); // a Mongoose model, for example
Ensuite, le routeur et la route avec son tableau de règles :
const router = express.Router();router.post(
"/posts",
[
body("title")
.trim()
.notEmpty().withMessage("Title is required")
.isLength({ max: 100 }).withMessage("Title cannot exceed 100 characters"), body("slug")
.trim()
.notEmpty().withMessage("Slug is required")
.matches(/^[a-z0-9-]+$/).withMessage("Slug can only contain lowercase letters, numbers, and hyphens")
.custom(async (slug) => {
const existing = await Post.findOne({ slug });
if (existing) {
throw new Error("This slug is already taken");
}
return true;
}), body("content")
.trim()
.notEmpty().withMessage("Content cannot be empty")
.isLength({ min: 20 }).withMessage("Content should be at least 20 characters"), body("tags")
.optional()
.isArray().withMessage("Tags must be an array")
.custom((tags) => tags.every((t) => typeof t === "string"))
.withMessage("Each tag must be a string"),
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
} try {
const post = await Post.create(req.body);
res.status(201).json(post);
} catch (err) {
res.status(500).json({ error: "Something went wrong while saving the post" });
}
}
);module.exports = router;
Plusieurs détails méritent d’être étudiés :
- Règles basées sur la base de données. La chaîne
slugutilise un validateur asynchrone.custom()pour rechercher des articles existants. Une expression régulière peut confirmer le format d’un slug, mais seule une requête peut vérifier s’il est unique. Lancer une exception à l’intérieur de la fonction personnalisée marque ce champ comme invalide avec ce message.
tags est marqué .optional() ; ainsi, un post sans tags est accepté, mais lorsqu’un ensemble de tags est fourni, il doit s’agir d’un tableau de chaînes de caractères.Deux mises en garde avant de copier ce modèle. Premièrement, une vérification d’unicité dans un validateur est sujette à des conflits : deux requêtes avec le même identifiant peuvent toutes deux passer la vérification avant que l’une d’elles ne soit enregistrée, il faut donc conserver un index d’unicité sur la colonne et gérer également l’erreur de clé dupliquée. Deuxièmement, en passant directement req.body à Post.create(), tous les champs supplémentaires fournis par le client sont enregistrés, et non seulement ceux qui ont été validés. express-validator propose la fonction matchedData(req), qui ne renvoie que les champs couverts par vos chaînes de validation, ce qui permet de combler cette faille liée à l’affectation massive de données.
Au fur et à mesure que l’application se développe, déplacez les tableaux de règles dans des modules dédiés, par exemple validators/postValidator.js, et importez-les dans les fichiers de route, afin que ces dernières ne soient pas envahies par un grand nombre de codes de validation.
Conclusion
La validation n’est pas un élément optionnel ; elle détermine si votre application peut faire confiance aux données qu’elle traite. Les vérifications manuelles avec des if conviennent pour un petit nombre de champs, mais leur efficacité diminue rapidement lorsqu’elles sont utilisées sur de nombreuses routes.
express-validator n’est pas magique. Il exécute les règles que vous avez définies en tant que middleware, enregistre les échecs sur la requête et permet à validationResult() de vous fournir un rapport afin que vous puissiez décider de la réponse à donner. Compte tenu de ce flux, le reste de la bibliothèque constitue un ensemble d’outils de validation et de nettoyage que vous choisissez selon vos besoins. Les points les plus importants en pratique sont :
- Considérez la validation côté backend comme obligatoire, quelles que soient les vérifications côté frontend.
- Appelez toujours
validationResult(), de préférence depuis un middleware commun, car la bibliothèque ne rejette jamais une requête de sa propre initiative. - Organisez les outils de nettoyage de manière intentionnelle au sein d’une chaîne.
matchedData(req) plutôt que le corps brut.