Accueil / Articles

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.

Éditeur : REACTAPP.TOP10 min de lecture2373 mots

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.
  • Cela réduit votre surface d’attaque. Les entrées non vérifiées constituent un point d’accès courant pour des attaques d’injection telles que l’injection SQL et le scripting跨-site (XSS). La validation et la nettoyage des données diminuent cette exposition, bien qu’elles complètent plutôt que de remplacer les requêtes paramétrées et un encodage adéquat des données affichées.
  • Cela évite des erreurs confuses. De nombreux pannes en environnement de production sont dues à un champ manquant, vide ou mal saisi, utilisé quelques lignes avant l’erreur réelle.
  • Cela génère des messages d’erreur utiles. Au lieu d’un message générique comme « Quelque chose a mal tourné », les clients peuvent savoir exactement ce qu’il faut corriger, par exemple « L’adresse e-mail n’est pas valide » ou « Le mot de passe doit comporter au moins 8 caractères ».
  • 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.
  • La véritable logique métier, qui consiste à enregistrer l’utilisateur, est cachée sous le code de validation.
  • Gérer manuellement chaque cas limite, tels que des chaînes vides, 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.
  • Chaque chaîne de validation s’exécute en tant que middleware.Lorsque 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.
  • Chaque règle a son tour d’être appliquée.Puisqu’une chaîne échouée continue à appeler le middleware suivant, toutes les règles sont exécutées et chaque problème est enregistré, pas seulement le premier.
  • 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é.
  • Votre code détermine le résultat. Si 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() lit req.body, la charge utile JSON ou de formulaire.
    • query() lit req.query, la chaîne de requête après ?, comme dans ?page=2.
    • param() lit req.params, les paramètres de route tels que :id dans /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ête
    • query(field) pour les paramètres de la chaîne de recherche
  • param(field) pour les paramètres de route
  • Vé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 comme status qui 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édente
    • validationResult(req) : rassemble toutes les erreurs relatives à la requête en cours
    • errors.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 slug utilise 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.
  • Champs optionnels qui sont néanmoins vérifiés. Le champ 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.
  • Un gestionnaire ciblé. Le gestionnaire de route a deux tâches : signaler les erreurs de validation et enregistrer le post. Toute la logique relative à l’acceptabilité des données se trouve dans le tableau de règles, ce qui permet de la lire et de la modifier facilement.
  • 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.
  • Valideurs d’unicité en arrière-plan avec des contraintes de base de données.
  • Conserver matchedData(req) plutôt que le corps brut.