Validation de formulaire Smart Node.js

Publié: 2022-03-11

L'une des tâches fondamentales à effectuer dans une API est la validation des données. Dans cet article, j'aimerais vous montrer comment ajouter une validation pare-balles pour vos données d'une manière qui les renvoie également bien formatées.

Faire une validation de données personnalisée dans Node.js n'est ni facile ni rapide. Il y a beaucoup de fonctionnalités que vous devez écrire pour couvrir tout type de données. Bien que j'aie essayé quelques bibliothèques de données de formulaire Node.js, à la fois pour Express et Koa, elles n'ont jamais répondu aux besoins de mes projets. Il y avait des problèmes avec l'extension des bibliothèques et les bibliothèques ne fonctionnant pas avec des structures de données complexes ou une validation asynchrone.

Validation de formulaire dans Node.js avec Datalize

C'est pourquoi j'ai finalement décidé d'écrire ma propre bibliothèque de validation de formulaires minuscule mais puissante appelée datalize. Il est extensible, vous pouvez donc l'utiliser dans n'importe quel projet et le personnaliser selon vos besoins. Il valide le corps, la requête ou les paramètres d'une requête. Il prend également en charge les filtres async et les structures JSON complexes comme les tableaux ou les objets imbriqués .

Installer

Datalize peut être installé via npm :

 npm install --save datalize

Pour analyser le corps d'une requête, vous devez utiliser une bibliothèque distincte. Si vous n'en utilisez pas déjà un, je vous recommande koa-body pour Koa ou body-parser pour Express.

Vous pouvez appliquer ce didacticiel à votre serveur d'API HTTP déjà configuré ou utiliser le code de serveur HTTP Koa simple suivant.

 const Koa = require('koa'); const bodyParser = require('koa-body'); const app = new Koa(); const router = new (require('koa-router'))(); // helper for returning errors in routes app.context.error = function(code, obj) { this.status = code; this.body = obj; }; // add koa-body middleware to parse JSON and form-data body app.use(bodyParser({ enableTypes: ['json', 'form'], multipart: true, formidable: { maxFileSize: 32 * 1024 * 1024, } })); // Routes... // connect defined routes as middleware to Koa app.use(router.routes()); // our app will listen on port 3000 app.listen(3000); console.log(' API listening on 3000');

Cependant, il ne s'agit pas d'une configuration de production (vous devez utiliser la journalisation, appliquer l'autorisation, gérer les erreurs, etc.), mais ces quelques lignes de code fonctionneront très bien pour les exemples que je vais vous montrer.

Remarque : Tous les exemples de code utilisent Koa, mais le code de validation des données fonctionnera également pour Express. La bibliothèque datalize a également un exemple pour implémenter la validation de formulaire Express.

Un exemple de base de validation de formulaire Node.js

Supposons que vous ayez un serveur Web Koa ou Express et un point de terminaison dans votre API qui crée un utilisateur avec plusieurs champs dans la base de données. Certains champs sont obligatoires, et certains ne peuvent avoir que des valeurs spécifiques ou doivent être formatés pour corriger le type.

Vous pourriez écrire une logique simple comme celle-ci :

 /** * @api {post} / Create a user * ... */ router.post('/', (ctx) => { const data = ctx.request.body; const errors = {}; if (!String(data.name).trim()) { errors.name = ['Name is required']; } if (!(/^[\-0-9a-zA-Z\.\+_]+@[\-0-9a-zA-Z\.\+_]+\.[a-zA-Z]{2,}$/).test(String(data.email))) { errors.email = ['Email is not valid.']; } if (Object.keys(errors).length) { return ctx.error(400, {errors}); } const user = await User.create({ name: data.name, email: data.email, }); ctx.body = user.toJSON(); });

Réécrivons maintenant ce code et validons cette requête en utilisant datalize :

 const datalize = require('datalize'); const field = datalize.field; /** * @api {post} / Create a user * ... */ router.post('/', datalize([ field('name').trim().required(), field('email').required().email(), ]), (ctx) => { if (!ctx.form.isValid) { return ctx.error(400, {errors: ctx.form.errors}); } const user = await User.create(ctx.form); ctx.body = user.toJSON(); });

Plus court, plus propre, donc facile à lire. Avec datalize, vous pouvez spécifier une liste de champs et leur enchaîner autant de règles (fonctions qui génèrent une erreur si l'entrée est invalide) ou de filtres (fonctions qui formatent l'entrée) que vous le souhaitez.

Les règles et les filtres sont exécutés dans le même ordre qu'ils sont définis, donc si vous voulez d'abord couper une chaîne pour les espaces blancs, puis vérifier si elle a une valeur, vous devez définir .trim() avant .required() .

Datalize créera alors un objet (disponible en tant que .form dans l'objet de contexte plus large) avec uniquement les champs que vous avez spécifiés, vous n'avez donc pas à les répertorier. La propriété .form.isValid vous indique si la validation a réussi ou non.

Gestion automatique des erreurs

Si nous ne voulons pas vérifier si le formulaire est valide ou non à chaque requête, nous pouvons ajouter un middleware global qui annule la requête si les données ne passent pas la validation.

Pour ce faire, nous ajoutons simplement ce morceau de code à notre fichier d'amorçage où nous créons notre instance d'application Koa/Express.

 const datalize = require('datalize'); // set datalize to throw an error if validation fails datalize.set('autoValidate', true); // only Koa // add to very beginning of Koa middleware chain app.use(async (ctx, next) => { try { await next(); } catch (err) { if (err instanceof datalize.Error) { ctx.status = 400; ctx.body = err.toJSON(); } else { ctx.status = 500; ctx.body = 'Internal server error'; } } }); // only Express // add to very end of Express middleware chain app.use(function(err, req, res, next) { if (err instanceof datalize.Error) { res.status(400).send(err.toJSON()); } else { res.send(500).send('Internal server error'); } });

Et nous n'avons plus besoin de vérifier si les données sont valides, car datalize le fera pour nous. Si les données sont invalides, il renverra un message d'erreur formaté avec une liste de champs invalides.

Validation de la requête

Oui, vous pouvez même valider vos paramètres de requête très facilement - il n'est pas nécessaire de l'utiliser uniquement avec les requêtes POST . Nous utilisons simplement la méthode d'assistance .query() , et la seule différence est que les données sont stockées dans l'objet .data au lieu de .form .

 const datalize = require('datalize'); const field = datalize.field; /** * @api {get} / List users * ... */ router.post('/', datalize.query([ field('keywords').trim(), field('page').default(1).number(), field('perPage').required().select([10, 30, 50]), ]), (ctx) => { const limit = ctx.data.perPage; const where = { }; if (ctx.data.keywords) { where.name = {[Op.like]: ctx.data.keywords + '%'}; } const users = await User.findAll({ where, limit, offset: (ctx.data.page - 1) * limit, }); ctx.body = users; });

Il existe également une méthode d'assistance pour la validation des paramètres, .params() . Les données de requête et de formulaire peuvent être validées ensemble en passant deux middlewares datalize dans la méthode .post() du routeur.

Plus de filtres, de tableaux et d'objets imbriqués

Jusqu'à présent, nous avons utilisé des données très simples dans notre validation de formulaire Node.js. Essayons maintenant des champs plus complexes comme des tableaux, des objets imbriqués, etc. :

 const datalize = require('datalize'); const field = datalize.field; const DOMAIN_ERROR = "Email's domain does not have a valid MX (mail) entry in its DNS record"; /** * @api {post} / Create a user * ... */ router.post('/', datalize([ field('name').trim().required(), field('email').required().email().custom((value) => { return new Promise((resolve, reject) => { dns.resolve(value.split('@')[1], 'MX', function(err, addresses) { if (err || !addresses || !addresses.length) { return reject(new Error(DOMAIN_ERROR)); } resolve(); }); }); }), field('type').required().select(['admin', 'user']), field('languages').array().container([ field('id').required().id(), field('level').required().select(['beginner', 'intermediate', 'advanced']) ]), field('groups').array().id(), ]), async (ctx) => { const {languages, groups} = ctx.form; delete ctx.form.languages; delete ctx.form.groups; const user = await User.create(ctx.form); await UserGroup.bulkCreate(groups.map(groupId => ({ groupId, userId: user.id, }))); await UserLanguage.bulkCreate(languages.map(item => ({ languageId: item.id, userId: user.id, level: item.level, )); });

S'il n'y a pas de règle intégrée pour les données que nous devons valider, nous pouvons créer une règle de validation de données personnalisée avec la méthode .custom() (excellent nom, n'est-ce pas ?) et y écrire la logique nécessaire. Pour les objets imbriqués, il existe la méthode .container() dans laquelle vous pouvez spécifier une liste de champs de la même manière que dans la fonction datalize() . Vous pouvez imbriquer des conteneurs dans des conteneurs ou les compléter avec des filtres .array() , qui convertissent les valeurs en tableaux. Lorsque le filtre .array() est utilisé sans conteneur, les règles ou filtres spécifiés sont appliqués à chaque valeur du tableau.

'read' .array().select(['read', 'write']) 'write' les erreurs. Plutôt cool, hein ?

PUT / PATCH

Lorsqu'il s'agit de mettre à jour vos données avec PUT / PATCH (ou POST ), vous n'avez pas à réécrire toute votre logique, vos règles et vos filtres. Vous ajoutez simplement un filtre supplémentaire comme .optional() ou .patch() , qui supprimera tout champ de l'objet de contexte s'il n'a pas été défini dans la requête. ( .optional() le rendra toujours facultatif, tandis que .patch() le rendra facultatif uniquement si la méthode de la requête HTTP est PATCH .) Vous pouvez ajouter ce filtre supplémentaire afin qu'il fonctionne à la fois pour la création et la mise à jour des données dans votre base de données.

 const datalize = require('datalize'); const field = datalize.field; const userValidator = datalize([ field('name').patch().trim().required(), field('email').patch().required().email(), field('type').patch().required().select(['admin', 'user']), ]); const userEditMiddleware = async (ctx, next) => { const user = await User.findByPk(ctx.params.id); // cancel request here if user was not found if (!user) { throw new Error('User was not found.'); } // store user instance in the request so we can use it later ctx.user = user; return next(); }; /** * @api {post} / Create a user * ... */ router.post('/', userValidator, async (ctx) => { const user = await User.create(ctx.form); ctx.body = user.toJSON(); }); /** * @api {put} / Update a user * ... */ router.put('/:id', userEditMiddleware, userValidator, async (ctx) => { await ctx.user.update(ctx.form); ctx.body = ctx.user.toJSON(); }); /** * @api {patch} / Patch a user * ... */ router.patch('/:id', userEditMiddleware, userValidator, async (ctx) => { if (!Object.keys(ctx.form).length) { return ctx.error(400, {message: 'Nothing to update.'}); } await ctx.user.update(ctx.form); ctx.body = ctx.user.toJSON(); });

Avec deux middlewares simples, nous pouvons écrire la plupart des logiques pour toutes les méthodes POST / PUT / PATCH . La fonction userEditMiddleware() vérifie si l'enregistrement que nous voulons modifier existe et renvoie une erreur dans le cas contraire. Ensuite, userValidator() effectue la validation pour tous les points de terminaison. Enfin, le filtre .patch() supprimera tout champ de l'objet .form s'il n'est pas défini et si la méthode de la requête est PATCH .

Suppléments de validation de formulaire Node.js

Dans les filtres personnalisés, vous pouvez obtenir les valeurs d'autres champs et effectuer une validation en fonction de cela. Vous pouvez également obtenir toutes les données de l'objet de contexte, telles que les informations de demande ou d'utilisateur, car elles sont toutes fournies dans les paramètres de rappel de fonction personnalisés.

La bibliothèque couvre un ensemble de règles et de filtres de base, mais vous pouvez enregistrer des filtres globaux personnalisés que vous pouvez utiliser avec n'importe quel champ, de sorte que vous n'ayez pas à écrire le même code encore et encore :

 const datalize = require('datalize'); const Field = datalize.Field; Field.prototype.date = function(format = 'YYYY-MM-DD') { return this.add(function(value) { const date = value ? moment(value, format) : null; if (!date || !date.isValid()) { throw new Error('%s is not a valid date.'); } return date.format(format); }); }; Field.prototype.dateTime = function(format = 'YYYY-MM-DD HH:mm') { return this.date(format); };

Avec ces deux filtres personnalisés, vous pouvez enchaîner vos champs avec des .date() ou .dateTime() pour valider la saisie de la date.

Les fichiers peuvent également être validés à l'aide de datalize : il existe des filtres spéciaux uniquement pour les fichiers tels que .file() , .mime() et .size() afin que vous n'ayez pas à gérer les fichiers séparément.

Commencez à écrire de meilleures API maintenant

J'utilise déjà datalize pour la validation de formulaire Node.js dans plusieurs projets de production, pour les petites et les grandes API. Cela m'a aidé à livrer de grands projets à temps et avec moins de stress tout en les rendant plus lisibles et maintenables. Sur un projet, je l'ai même utilisé pour valider les données des messages WebSocket en écrivant un simple wrapper autour de Socket.IO et l'utilisation était à peu près la même que la définition des routes dans Koa, donc c'était bien. S'il y a suffisamment d'intérêt, je pourrais également écrire un tutoriel pour cela.

J'espère que ce tutoriel vous aidera et que je construis de meilleures API dans Node.js, avec des données parfaitement validées sans problèmes de sécurité ni erreurs de serveur internes. Et surtout, j'espère que cela vous fera gagner une tonne de temps que vous auriez autrement dû investir dans l'écriture de fonctions supplémentaires pour la validation de formulaire à l'aide de JavaScript.