Convalida del modulo Smart Node.js
Pubblicato: 2022-03-11Una delle attività fondamentali da eseguire in un'API è la convalida dei dati. In questo articolo, vorrei mostrarti come aggiungere una convalida a prova di proiettile per i tuoi dati in un modo che li restituisca anche ben formattato.
Eseguire la convalida dei dati personalizzati in Node.js non è né facile né veloce. Ci sono molte funzionalità che devi scrivere per coprire qualsiasi tipo di dati. Anche se ho provato alcune librerie di dati di moduli Node.js, sia per Express che per Koa, non hanno mai soddisfatto le esigenze dei miei progetti. Si sono verificati problemi con l'estensione delle librerie e le librerie non funzionavano con strutture di dati complesse o con la convalida asincrona.
Convalida del modulo in Node.js con Datalize
Ecco perché alla fine ho deciso di scrivere la mia piccola ma potente libreria di convalida dei moduli chiamata datalize. È estensibile, quindi puoi utilizzarlo in qualsiasi progetto e personalizzarlo in base alle tue esigenze. Convalida il corpo, la query o i parametri di una richiesta. Supporta anche filtri async
e strutture JSON complesse come array o oggetti nidificati .
Impostare
Datalize può essere installato tramite npm:
npm install --save datalize
Per analizzare il corpo di una richiesta, dovresti usare una libreria separata. Se non ne usi già uno, ti consiglio koa-body per Koa o body-parser per Express.
Puoi applicare questo tutorial al tuo server API HTTP già configurato o utilizzare il seguente semplice codice del server HTTP Koa.
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');
Tuttavia, questa non è una configurazione di produzione (dovresti usare la registrazione, imporre l'autorizzazione, gestire gli errori, ecc.), Ma queste poche righe di codice funzioneranno perfettamente per gli esempi che ti mostrerò.
Nota: tutti gli esempi di codice utilizzano Koa, ma il codice di convalida dei dati funzionerà anche per Express. La libreria datalize ha anche un esempio per l'implementazione della convalida dei moduli Express.
Un esempio di convalida del modulo Node.js di base
Supponiamo che tu abbia un server Web Koa o Express e un endpoint nella tua API che crea un utente con diversi campi nel database. Alcuni campi sono obbligatori e altri possono avere solo valori specifici o devono essere formattati per correggere il tipo.
Potresti scrivere una logica semplice come questa:
/** * @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(); });
Ora riscriviamo questo codice e convalidiamo questa richiesta usando 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(); });
Più breve, più pulito, così facile da leggere. Con datalize, puoi specificare un elenco di campi e concatenarvi tutte le regole (funzioni che generano un errore se l'input non è valido) o filtri (funzioni che formattano l'input) come desideri.
Le regole e i filtri vengono eseguiti nello stesso ordine in cui sono definiti, quindi se vuoi tagliare prima una stringa per gli spazi bianchi e poi controllare se ha qualche valore, devi definire .trim()
prima .required()
.
Datalize creerà quindi un oggetto (disponibile come .form
nell'oggetto contesto più ampio) con solo i campi che hai specificato, quindi non devi elencarli di nuovo. La proprietà .form.isValid
indica se la convalida è riuscita o meno.
Gestione automatica degli errori
Se non vogliamo verificare se il modulo è valido o meno ad ogni richiesta, possiamo aggiungere un middleware globale che annulla la richiesta se i dati non hanno superato la convalida.
Per fare ciò, aggiungiamo semplicemente questo pezzo di codice al nostro file bootstrap in cui creiamo la nostra istanza dell'app 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'); } });
E non dobbiamo più verificare se i dati sono validi, poiché datalize lo farà per noi. Se i dati non sono validi, verrà restituito un messaggio di errore formattato con un elenco di campi non validi.
Convalida della query
Sì, puoi persino convalidare i parametri della tua query molto facilmente: non deve essere utilizzato solo con le richieste POST
. Usiamo semplicemente il metodo helper .query()
e l'unica differenza è che i dati sono archiviati nell'oggetto .data
invece di .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; });
C'è anche un metodo di supporto per la convalida dei parametri, .params()
. I dati di query e form possono essere convalidati insieme passando due middleware datalize nel metodo .post()
del router.

Più filtri, array e oggetti nidificati
Finora abbiamo utilizzato dati molto semplici nella nostra convalida del modulo Node.js. Ora proviamo alcuni campi più complessi come array, oggetti nidificati, ecc.:
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, )); });
Se non esiste una regola incorporata per i dati che dobbiamo convalidare, possiamo creare una regola di convalida dei dati personalizzata con il metodo .custom()
(ottimo nome, giusto?) e scrivere lì la logica necessaria. Per gli oggetti nidificati, esiste il metodo .container()
in cui è possibile specificare un elenco di campi allo stesso modo della funzione datalize()
. Puoi annidare contenitori all'interno di contenitori o integrarli con filtri .array()
, che convertono i valori in array. Quando il filtro .array()
viene utilizzato senza un contenitore, le regole oi filtri specificati vengono applicati a ogni valore nell'array.
Quindi .array().select(['read', 'write'])
verificherebbe se ogni valore nell'array è 'read'
o 'write'
e se non lo sono, restituirà un elenco di tutti gli indici con errori. Abbastanza bello, eh?
PUT
/ PATCH
Quando si tratta di aggiornare i dati con PUT
/ PATCH
(o POST
), non è necessario riscrivere tutta la logica, le regole e i filtri. Basta aggiungere un filtro aggiuntivo come .optional()
o .patch()
, che rimuoverà qualsiasi campo dall'oggetto di contesto se non è stato definito nella richiesta. ( .optional()
lo renderà sempre opzionale, mentre .patch()
lo renderà opzionale solo se il metodo della richiesta HTTP è PATCH
.) Puoi aggiungere questo filtro aggiuntivo in modo che funzioni sia per la creazione che per l'aggiornamento dei dati nel tuo database.
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(); });
Con due semplici middleware, possiamo scrivere la maggior parte della logica per tutti i metodi POST
/ PUT
/ PATCH
. La funzione userEditMiddleware()
verifica se il record che vogliamo modificare esiste e genera un errore in caso contrario. Quindi userValidator()
esegue la convalida per tutti gli endpoint. Infine, il filtro .patch()
rimuoverà qualsiasi campo dall'oggetto .form
se non è definito e se il metodo della richiesta è PATCH
.
Extra di convalida del modulo Node.js
Nei filtri personalizzati, puoi ottenere i valori di altri campi ed eseguire la convalida in base a quello. Puoi anche ottenere qualsiasi dato dall'oggetto contesto, come richiesta o informazioni sull'utente, poiché è tutto fornito nei parametri di callback della funzione personalizzata.
La libreria copre un insieme di base di regole e filtri, ma puoi registrare filtri globali personalizzati che puoi utilizzare con qualsiasi campo, quindi non devi scrivere lo stesso codice più e più volte:
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); };
Con questi due filtri personalizzati puoi concatenare i tuoi campi con i .date()
o .dateTime()
per convalidare l'inserimento della data.
I file possono anche essere convalidati usando datalize: ci sono filtri speciali solo per file come .file()
, .mime()
e .size()
in modo da non dover gestire i file separatamente.
Inizia subito a scrivere API migliori
Ho già utilizzato datalize per la convalida dei moduli Node.js in diversi progetti di produzione, sia per API piccole che grandi. Mi ha aiutato a realizzare grandi progetti in tempo e con meno stress, rendendoli più leggibili e gestibili. In un progetto l'ho persino usato per convalidare i dati per i messaggi WebSocket scrivendo un semplice wrapper attorno a Socket.IO e l'utilizzo era praticamente lo stesso della definizione di percorsi in Koa, quindi è stato bello. Se c'è abbastanza interesse, potrei scrivere un tutorial anche per quello.
Spero che questo tutorial ti aiuti e io creo API migliori in Node.js, con dati perfettamente convalidati senza problemi di sicurezza o errori interni del server. E, soprattutto, spero che ti farà risparmiare un sacco di tempo che altrimenti dovresti investire nella scrittura di funzioni extra per la convalida dei moduli utilizzando JavaScript.