Express 03.2 - đ§ Valider la saisie utilisateur
â ïž Avant de commencer cette quĂȘte, nous te conseillons de terminer celle-ci :
Tu peux maintenant POST et PUT des donnĂ©es avec ton application Express. Tout fonctionne bien, tant que la requĂȘte contient des donnĂ©es valides.
MAIS si tu lances npm run test pour exĂ©cuter les tests en l'Ă©tat avec une ressource incomplĂšte, nos tests acceptent une rĂ©ponse avec un statut 500 (erreur serveur). Pourtant la "faute" est du cĂŽtĂ© du client : la requĂȘte est incomplĂšte et donc mal formulĂ©e. C'est un statut de la famille 4xx que tu devrais attendre. Plus prĂ©cisĂ©ment tu dois modifier tes tests pour attendre un statut 422 :
// ...
describe("POST /api/movies", () => {
// ...
it("should return an error", async () => {
const movieWithMissingProps = { title: "Harry Potter" };
const response = await request(app)
.post("/api/movies")
.send(movieWithMissingProps);
expect(response.status).toEqual(422);
});
});
describe("PUT /api/movies/:id", () => {
// ...
it("should return an error", async () => {
const movieWithMissingProps = { title: "Harry Potter" };
const response = await request(app)
.put(`/api/movies/1`)
.send(movieWithMissingProps);
expect(response.status).toEqual(422);
});
// ...
});
// ...
describe("POST /api/users", () => {
// ...
it("should return an error", async () => {
const userWithMissingProps = { firstname: "Harry" };
const response = await request(app)
.post("/api/users")
.send(userWithMissingProps);
expect(response.status).toEqual(422);
});
});
describe("PUT /api/users/:id", () => {
// ...
it("should return an error", async () => {
const userWithMissingProps = { firstname: "Harry" };
const response = await request(app)
.put(`/api/users/1`)
.send(userWithMissingProps);
expect(response.status).toEqual(422);
});
// ...
});
Maintenant que les tests sont modifiĂ©s, ils ne fonctionnent plus : c'est volontaire. C'est une erreur 422 que nous attendons maintenant et ce n'est plus une erreur 500 lors de l'exĂ©cution de la requĂȘte SQL.
Il est temps de vĂ©rifier que les donnĂ©es attendues sont bien prĂ©sentes dans la requĂȘte et qu'elles sont fournies au bon format avant de risquer une mauvaise requĂȘte au serveur de base de donnĂ©es.
Nous allons considérer les éléments suivants :
- VĂ©rifier que les requĂȘtes entrantes contiennent les champs requis
- EmpĂȘcher l'insertion de donnĂ©es invalides dans la base de donnĂ©es
đ€ Dans cette quĂȘte, tu vas apprendre Ă :
â Valider la saisie de l'utilisateur â Retourner les rĂ©ponses appropriĂ©es en cas d'erreur
â Valider la saisie utilisateur
Nous avons une table pour stocker nos films, qui ressemble à ça :
CREATE TABLE movies (
id int primary key NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
director varchar(255) NOT NULL,
year varchar(255) NOT NULL,
color varchar(255) NOT NULL,
duration int NOT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb3;
Ici, tous les champs sont obligatoires (NOT NULL).
Utilise Postman pour faire quelques tests sur le comportement actuel de la route POST /api/movies. Envoi :
- Une requĂȘte contenant tous les champs nĂ©cessaires (
title,director...). - Une requĂȘte dans laquelle l'un des champs requis est manquant. Regarde ton terminal.
La premiĂšre requĂȘte doit rĂ©ussir. Par contre, il est normal que la deuxiĂšme requĂȘte Ă©choue : tous les champs Ă©tant NOT NULL, MySQL gĂ©nĂšre une erreur (ER_BAD_NULL_ERROR) si une valeur manque, ce qui conduit pour l'instant Ă une erreur 500.
Le code d'état 500 indique une "Internal Server Error" : cela signifie généralement que l'erreur qui s'est produite n'était pas voulue par le développeur. Dans cette situation, nous ne devrions idéalement pas retourner une erreur 500 !
Comment aurions-nous pu empĂȘcher l'erreur 500 ? En effectuant les vĂ©rifications nĂ©cessaires sur req.body avant d'envoyer la demande d'insertion !
La validation des entrées est un facteur important lors de la création d'une application Web. L'objectif est de vérifier qu'un client envoie des données valides.
Comment vérifier que les données sont correctement formatées ?
Cette vérification peut avoir lieu à deux endroits :
- Dans l'application cliente (React/Angular/etc.), tu peux vérifier si les données saisies dans un formulaire sont correctes. Par exemple, si tu essaies de créer un compte Facebook avec l'email
foo@bar... le site te dira que le champ est invalide. - Dans l'application serveur, tu peux vĂ©rifier que tous les champs nĂ©cessaires Ă l'insertion de donnĂ©es dans une table sont prĂ©sents - et valides - dans le corps de la requĂȘte (
req.body).
Si tu devais n'en choisir qu'un, tu devrais opter pour la validation cÎté serveur.
Faire les vĂ©rifications cĂŽtĂ© client, avant mĂȘme d'essayer d'envoyer les donnĂ©es au serveur, avertit immĂ©diatement l'utilisateur que quelque chose ne va pas. C'est suffisant pour des considĂ©rations UX, mais pas pour la sĂ©curitĂ© ! Rien n'empĂȘche une personne malintentionnĂ©e de "falsifier" des requĂȘtes invalides, via un outil comme Postman, pour les envoyer Ă l'application serveur, contournant ainsi les vĂ©rifications de l'application cliente.
Une bonne idée ici est de s'appuyer sur l'architecture basée middleware d'Express : une route peut prendre plusieurs middlewares en paramÚtres. Revoyons ça ensemble :
const step1 = (req, res, next) => {
req.message = "I went through step1";
next();
};
const step2 = (req, res, next) => {
req.message += " and step2";
next();
};
const lastStep = (req, res) => {
res.send(req.message);
};
app.get("/justToTest", step1, step2, lastStep);
Ici step1, step2 et lastStep sont des middlewares, et seront exécutés l'un aprÚs l'autre au déclenchement de la route. La fonction next aprÚs les paramÚtres req et res est utilisée pour dire "J'ai fini ma partie, vous pouvez passer à la suivante".
C'est une pratique courante d'utiliser req comme outil pour transmettre des données au middleware suivant. Dans cet exemple, step1 ajoute une propriété message à l'objet req avec une valeur initiale "I went through step1".
Ensuite, step2 complĂšte le message qui est finalement envoyĂ© en rĂ©ponse. Si tu essayais une requĂȘte sur cette route, elle rĂ©pondrait "I went through step1 and step2".
Revenons Ă notre quĂȘte de validation. Nous pouvons crĂ©er un fichier validateMovie.js (dans un dossier fichier dĂ©diĂ© middlewares pour garder notre code organisĂ©) pour dĂ©clarer et exporter notre middlaware. Nous l'ajouterons ensuite Ă la route POST /api/movies dans app.js :
// in middlewares/validateMovie.js
const validateMovie = (req, res, next) => {
// validate req.body then call next() if everything is ok
};
module.exports = validateMovie;
// in app.js
const validateMovie = require("./middlewares/validateMovie");
app.post("/api/movies", validateMovie, movieControllers.postMovie);
Note que ces validations peuvent et doivent ĂȘtre faites pour les requĂȘtes POST et PUT.
Nous allons voir comment vérifier les données entrantes dans validateMovie en utilisant 2 méthodes :
- Vérifier les valeurs trouvées dans
req.body"Ă la main". - Utiliser une bibliothĂšque tierce.
đ§ Validation manuelle
đ€·đ»ââïž Champs manquants
La premiÚre chose à faire est de vérifier si l'un des champs title, director, year, color ou duration manque. Tu peux le faire en utilisant des blocs if/else classiques, comme ceci (title uniquement) :
const validateMovie = (req, res, next) => {
const { title } = req.body;
if (title == null) {
res.status(422).send("The field 'title' is required");
} else {
next();
}
};
C'est un bon dĂ©but. Au lieu d'une erreur 500, le code rĂ©pondra avec le code d'Ă©tat 422 ("Unprocessable Entity"). Ce statut est plus appropriĂ© : la syntaxe de la requĂȘte est correcte, mais la requĂȘte ne peut pas ĂȘtre exĂ©cutĂ©e avec succĂšs. Le code plus gĂ©nĂ©rique 400 ("Bad Request") est parfois utilisĂ©, mais la plupart des API (telles que GitHub) renvoient un 422 si un champ est manquant ou invalide.
ComplÚte le code pour vérifier tous les champs du film.
Afficher solution|||javascript|||Tu as des doutes sur comment faire ?|||0|||Masquer
const validateMovie = (req, res, next) => {
const { title, director, year, color, duration } = req.body;
if (title == null) {
res.status(422).send("The field 'title' is required");
} else if (director == null) {
res.status(422).send("The field 'director' is required");
} else if (year == null) {
res.status(422).send("The field 'year' is required");
} else if (color == null) {
res.status(422).send("The field 'color' is required");
} else if (duration == null) {
res.status(422).send("The field 'duration' is required");
} else {
next();
}
};
Cela fonctionne, mais ici le processus de validation s'arrĂȘtera Ă la premiĂšre erreur : si title et director sont nuls dans req.body, le code ci-dessus n'enverra une erreur que pour le titre (tu peux essayer avec Postman).
Une meilleure implĂ©mentation pourrait ĂȘtre de :
- Préparer un tableau d'erreurs :
const errors = []; - Continuer Ă le remplir pour chaque valeur manquante
if (someField == null) { errors.push({ field: "someField", message: "Some message" }); }. - Vérifier
errors.lengthaprÚs que chaque champ ait été vérifié.
Essaye d'améliorer ton code en suivant cette logique.
Afficher solution|||javascript|||Tu as des doutes sur comment faire ?|||0|||Masquer
const validateMovie = (req, res, next) => {
const { title, director, year, color, duration } = req.body;
const errors = [];
if (title == null) {
errors.push({ field: "title", message: "This field is required" });
}
if (director == null) {
errors.push({ field: "director", message: "This field is required" });
}
if (year == null) {
errors.push({ field: "year", message: "This field is required" });
}
if (color == null) {
errors.push({ field: "color", message: "This field is required" });
}
if (duration == null) {
errors.push({ field: "duration", message: "This field is required" });
}
if (errors.length) {
res.status(422).json({ validationErrors: errors });
} else {
next();
}
};
Vérifie le résultat avec Postman : le problÚme des champs manquants est résolu !
đ€ "Ton titre comporte plus de 255 caractĂšres, vraiment ?"
Qu'en est-il des champs présents, mais invalides ? Essaye de publier un film avec un "titre" contenant plus de 255 caractÚres (Lorem Ipsum est ton ami). Regarde le terminal à partir duquel tu as lancé le serveur.
Tu devrais voir une erreur ER_DATA_TOO_LONG car le champ title de ta table est de type VARCHAR(255). Cette colonne ne peut donc pas stocker plus de 255 caractĂšres !
Essaye d'ajouter un test sur title (quand il est défini) pour résoudre ce problÚme ;)
Afficher solution|||javascript|||Tu as des doutes sur comment faire ?|||0|||Masquer
const validateMovie = (req, res, next) => {
const { title, director, year, color, duration } = req.body;
const errors = [];
if (title == null) {
errors.push({ field: "title", message: "This field is required" });
} else if (title.length >= 255) {
errors.push({ field: "title", message: "Should contain less than 255 characters" });
}
// ...
if (errors.length) {
res.status(422).json({ validationErrors: errors });
} else {
next();
}
};
đ« E-mail invalide
Maintenant, si tu te concentres sur la table users, rien n'empĂȘche le client d'envoyer un e-mail mal formatĂ©, tel que bad@email.
Considére le code suivant pour gérer ce scénario :
const validateUser = (req, res, next) => {
const { email } = req.body;
const errors = [];
// ...
const emailRegex = /[a-z0-9._]+@[a-z0-9-]+\.[a-z]{2,3}/;
if (!emailRegex.test(email)) {
errors.push({ field: 'email', message: 'Invalid email' });
}
// ...
if (errors.length) {
res.status(422).json({ validationErrors: errors });
} else {
next();
}
};
Il utilise une expression rĂ©guliĂšre ou "regex" pour vĂ©rifier que l'e-mail est correctement formatĂ©. Si tu n'as pas l'habitude des regex, elles te permettent de vĂ©rifier qu'une chaĂźne est conforme Ă un certain "modĂšle". Tu peux suivre la quĂȘte Regex : Les expressions rĂ©guliĂšres si tu veux un peu d'entraĂźnement.
Un e-mail est généralement composé de :
- caractÚres alphanumériques et/ou
.et_, - suivi de
@, - suivi de caractÚres alphanumériques et/ou
-, - suivi de
., - suivi de 2 Ă 3 lettres.
La réalité est plus compliquée, mais la regex ci-dessous permet de gérer les cas les plus courants (si tu veux te faire peur : une regex affutée).
Comme tu peux le constater, l'ajout de ces vĂ©rifications est fastidieux et rend le code beaucoup plus lourd. Il vaut mieux utiliser une solution tierce, qui aura l'avantage d'ĂȘtre plus compacte !
âïž Validation Ă l'aide d'un module tiers
De nombreux modules sont disponibles pour gérer la validation des entrées de maniÚre plus élégante et efficace. Prenons quelques exemples ! Puisque tu es de plus en plus à l'aise avec les outils de développement, nous te laisserons creuser la documentation officielle de l'outil que tu choisiras. Suis simplement le code qui t'inspire le plus.
đ§ Joi
Le package joi fournit des outils pour valider les données en fonction d'un schéma défini, avec des options avancées telles que la vérification des e-mails :
const Joi = require("joi");
const userSchema = Joi.object({
email: Joi.string().email().max(255).required(),
firstname: Joi.string().max(255).required(),
lastname: Joi.string().max(255).required(),
});
const validateUser = (req, res, next) => {
const { firstname, lastname, email } = req.body;
const { error } = userSchema.validate(
{ firstname, lastname, email },
{ abortEarly: false }
);
if (error) {
res.status(422).json({ validationErrors: error.details });
} else {
next();
}
};
Qu'est-ce que tu penses ?
đ§ express-validator
Le package express-validator fournit un ensemble de middlewares à combiner sur une route :
// ...
const { body, validationResult } = require('express-validator');
app.post(
'/api/users',
// req.body.email should be a valid email
body('email').isEmail(),
// req.body.firstname should have no more than 255 characters
body('firstname').isLength({ max: 255 }),
// more middlewares
);
// ...
Cela t'aidera ici de savoir que les middlewares peuvent Ă©galement ĂȘtre dĂ©clarĂ©s dans un tableau pour ĂȘtre rĂ©utilisables. Souviens toi du code suivant :
const step1 = (req, res, next) => {
req.message = "I went through step1";
next();
};
const step2 = (req, res, next) => {
req.message += " and step2";
next();
};
const lastStep = (req, res) => {
res.send(req.message);
};
app.get("/justToTest", step1, step2, lastStep);
Il est équivalent au code :
const step1 = (req, res, next) => {
req.message = "I went through step1";
next();
};
const step2 = (req, res, next) => {
req.message += " and step2";
next();
};
const lastStep = (req, res) => {
res.send(req.message);
};
const steps = [step1, step2, lastStep];
app.get("/justToTest", steps);
Ainsi, avec express-validator, tu peux créer tes validateurs sous la forme de tableaux de middleware :
const { body, validationResult } = require('express-validator');
const validateUser = [
body("email").isEmail(),
body("firstname").isLength({ max: 255 }),
body("lastname").isLength({ max: 255 }),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.status(422).json({ validationErrors: errors.array() });
} else {
next();
}
},
];
Qu'est-ce que tu penses ?
đȘ Challenge
Choisis une des solutions ci-dessus, manuelle ou avec une des bibliothĂšques tierces proposĂ©es (ou une autre que tu aurais trouvĂ©e toi mĂȘme !), et crĂ©e tes validateurs :
- Crée les middlewares
validateMovieetvalidateUser. - Utilise les validateurs sur les routes POST et PUT.
- Réponds avec un statut
422lorsque des données non valides sont fournies.
Publie ton application améliorée sur GitHub et partage l'URL en solution.
đ§ CritĂšres de validation
- [ ] Les tests fonctionnent Ă nouveau.
==$==
Ceci est une correction qui utilise express-validator, il en existe d'autres tout Ă fait valables.
Ce que tu devrais avoir dans :
const express = require("express");
const app = express();
app.use(express.json());
const movieControllers = require("./controllers/movieController");
const validateMovie = require("./middlewares/validateMovie");
app.get("/api/movies", movieControllers.getMovies);
app.get("/api/movies/:id", movieControllers.getMovieById);
app.post("/api/movies", validateMovie, movieControllers.createMovie);
app.put("/api/movies/:id", validateMovie, movieControllers.updateMovie);
const userControllers = require("./controllers/userController");
const validateUser = require("./middlewares/validateUser");
app.get("/api/users", userControllers.getUsers);
app.get("/api/users/:id", userControllers.getUserById);
app.post("/api/users", validateUser, userControllers.postUser);
app.put("/api/users/:id", validateUser, movieControllers.updateUser);
module.exports = app;
const { body, validationResult } = require("express-validator");
const validateUser = [
body("email").isEmail(),
body("firstname").isLength({ max: 255 }),
body("lastname").isLength({ max: 255 }),
body("city").isLength({ max: 255 }),
body("language").isLength({ max: 255 }),
(req, res, next) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
res.status(422).json({ validationErrors: errors.array() });
} else {
next();
}
},
];
module.exports = validateUser;
const { body, validationResult } = require("express-validator");
const validateMovie = [
body("title").isLength({ max: 255 }),
body("director").isLength({ max: 255 }),
body("year").isLength({ max: 4 }),
body("color").isBoolean(),
body("duration").isNumeric(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.status(422).json({ validationErrors: errors.array() });
} else {
next();
}
},
];
module.exports = validateMovie;
Ressources partagĂ©es sur cette quĂȘte
- RTFM ;-)https://github.com/sideway/joi/blob/v14.3.1/API.md
- It can be helpful https://www.npmjs.com/package/express-joi-validation