Validare inteligentă a formularului Node.js

Publicat: 2022-03-11

Una dintre sarcinile fundamentale de efectuat într-un API este validarea datelor. În acest articol, aș dori să vă arăt cum să adăugați validare antiglonț pentru datele dvs. într-un mod care să le returneze și bine formatate.

Validarea datelor personalizate în Node.js nu este nici ușoară, nici rapidă. Există o mulțime de funcționalități pe care trebuie să le scrieți pentru a acoperi orice tip de date. Deși am încercat câteva biblioteci de date de formulare Node.js – atât pentru Express, cât și pentru Koa – nu au îndeplinit niciodată nevoile proiectelor mele. Au existat probleme cu extinderea bibliotecilor și bibliotecile nu funcționează cu structuri de date complexe sau cu validarea asincronă.

Validarea formularului în Node.js cu Datalize

De aceea, în cele din urmă, am decis să scriu propria mea bibliotecă de validare a formularelor minusculă, dar puternică, numită datalize. Este extensibil, astfel încât îl puteți utiliza în orice proiect și îl puteți personaliza conform cerințelor dumneavoastră. Validează corpul unei cereri, interogarea sau parametrii. De asemenea, acceptă filtre async și structuri JSON complexe, cum ar fi matrice sau obiecte imbricate .

Înființat

Datalize poate fi instalat prin npm:

 npm install --save datalize

Pentru a analiza corpul unei cereri, ar trebui să utilizați o bibliotecă separată. Dacă nu folosiți deja unul, vă recomand koa-body pentru Koa sau body-parser pentru Express.

Puteți aplica acest tutorial pe serverul dvs. API HTTP deja configurat sau puteți utiliza următorul cod simplu de server Koa HTTP.

 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');

Totuși, aceasta nu este o configurare de producție (ar trebui să utilizați înregistrarea, să impuneți autorizarea, să gestionați erorile etc.), dar aceste câteva linii de cod vor funcționa foarte bine pentru exemplele pe care vi le voi arăta.

Notă: Toate exemplele de cod folosesc Koa, dar codul de validare a datelor va funcționa și pentru Express. Biblioteca datalize are și un exemplu pentru implementarea validării formularelor Express.

Un exemplu de bază de validare a formularului Node.js

Să presupunem că aveți un server web Koa sau Express și un punct final în API-ul dvs. care creează un utilizator cu mai multe câmpuri în baza de date. Unele câmpuri sunt obligatorii, iar unele pot avea doar valori specifice sau trebuie formatate pentru a avea tipul corect.

Ai putea scrie o logică simplă ca aceasta:

 /** * @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(); });

Acum să rescriem acest cod și să validăm această solicitare folosind 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(); });

Mai scurt, mai curat, atât de ușor de citit. Cu datalize, puteți specifica o listă de câmpuri și le puteți lega cât mai multe reguli (funcții care aruncă o eroare dacă intrarea este invalidă) sau filtre (funcții care formatează intrarea) cât doriți.

Regulile și filtrele sunt executate în aceeași ordine în care sunt definite, așa că, dacă doriți să tăiați mai întâi un șir pentru spații albe și apoi să verificați dacă are vreo valoare, trebuie să definiți .trim() înainte de .required() .

Datalize va crea apoi un obiect (disponibil ca .form în obiectul de context mai larg) cu doar câmpurile pe care le-ați specificat, astfel încât să nu fie nevoie să le enumerați din nou. Proprietatea .form.isValid vă spune dacă validarea a avut succes sau nu.

Gestionarea automată a erorilor

Dacă nu dorim să verificăm dacă formularul este valid sau nu la fiecare solicitare, putem adăuga un middleware global care anulează cererea dacă datele nu au trecut validarea.

Pentru a face acest lucru, adăugăm doar această bucată de cod în fișierul nostru de bootstrap, unde creăm instanța aplicației 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'); } });

Și nu mai trebuie să verificăm dacă datele sunt valide, deoarece datalize o va face pentru noi. Dacă datele sunt nevalide, va returna un mesaj de eroare formatat cu o listă de câmpuri nevalide.

Validarea interogării

Da, puteți chiar să vă validați parametrii de interogare foarte ușor - nu trebuie să fie utilizat doar cu solicitările POST . Folosim doar metoda de ajutor .query() și singura diferență este că datele sunt stocate în obiectul .data în loc 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; });

Există, de asemenea, o metodă de ajutor pentru validarea parametrilor, .params() . Datele de interogare și formulare pot fi validate împreună prin trecerea a două middleware-uri datalize în metoda .post() a routerului.

Mai multe filtre, matrice și obiecte imbricate

Până acum am folosit date foarte simple în validarea formularului Node.js. Acum să încercăm câteva câmpuri mai complexe, cum ar fi matrice, obiecte imbricate 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, )); });

Dacă nu există o regulă încorporată pentru datele pe care trebuie să le validăm, putem crea o regulă personalizată de validare a datelor cu metoda .custom() (nume grozav, nu?) și putem scrie acolo logica necesară. Pentru obiectele imbricate, există metoda .container() în care puteți specifica o listă de câmpuri la fel ca în funcția datalize() . Puteți imbrica containere în containere sau le puteți completa cu filtre .array() , care convertesc valorile în matrice. Când filtrul .array() este utilizat fără un container, regulile sau filtrele specificate sunt aplicate fiecărei valori din matrice.

Deci .array().select(['read', 'write']) ar verifica dacă fiecare valoare din matrice este fie 'read' fie 'write' şi, dacă nu sunt, va returna o listă a tuturor indecşilor cu erori. Destul de misto, nu?

PUT / PATCH

Când vine vorba de actualizarea datelor cu PUT / PATCH (sau POST ), nu trebuie să vă rescrieți toată logica, regulile și filtrele. Doar adăugați un filtru suplimentar precum .optional() sau .patch() , care va elimina orice câmp din obiectul context dacă nu a fost definit în cerere. ( .optional() îl va face întotdeauna opțional, în timp ce .patch() îl va face opțional numai dacă metoda cererii HTTP este PATCH .) Puteți adăuga acest filtru suplimentar, astfel încât să funcționeze atât pentru crearea, cât și pentru actualizarea datelor în baza de date.

 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(); });

Cu două middleware simple, putem scrie cea mai mare parte a logicii pentru toate metodele POST / PUT / PATCH . Funcția userEditMiddleware() verifică dacă înregistrarea pe care vrem să o edităm există și aruncă o eroare în caz contrar. Apoi userValidator() face validarea pentru toate punctele finale. În cele din urmă, filtrul .patch() va elimina orice câmp din obiectul .form dacă nu este definit și dacă metoda cererii este PATCH .

Suplimente de validare a formularelor Node.js

În filtrele personalizate, puteți obține valori ale altor câmpuri și puteți efectua validarea pe baza acesteia. De asemenea, puteți obține orice date din obiectul context, cum ar fi solicitarea sau informațiile despre utilizator, deoarece toate sunt furnizate în parametrii de apel invers al funcției personalizate.

Biblioteca acoperă un set de bază de reguli și filtre, dar puteți înregistra filtre globale personalizate pe care le puteți utiliza cu orice câmp, astfel încât să nu fie nevoie să scrieți același cod mereu:

 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); };

Cu aceste două filtre personalizate vă puteți lega câmpurile cu filtre .date() sau .dateTime() pentru a valida introducerea datei.

Fișierele pot fi, de asemenea, validate folosind datalize: Există filtre speciale doar pentru fișiere precum .file() , .mime() și .size( .size() , astfel încât să nu trebuie să gestionați fișierele separat.

Începeți acum să scrieți API-uri mai bune

Am folosit deja datalize pentru validarea formularelor Node.js în mai multe proiecte de producție, atât pentru API-uri mici, cât și pentru mari. M-a ajutat să livrez proiecte grozave la timp și cu mai puțin stres, făcându-le mai lizibile și mai ușor de întreținut. Într-un proiect l-am folosit chiar și pentru a valida datele pentru mesajele WebSocket scriind un simplu wrapper în jurul Socket.IO și utilizarea a fost aproape aceeași cu definirea rutelor în Koa, așa că a fost frumos. Dacă există suficient interes, s-ar putea să scriu și un tutorial pentru asta.

Sper că acest tutorial vă va ajuta și eu să construim API-uri mai bune în Node.js, cu date perfect validate, fără probleme de securitate sau erori interne de server. Și, cel mai important, sper că vă va economisi o mulțime de timp pe care altfel ar trebui să îl investiți în scrierea de funcții suplimentare pentru validarea formularelor folosind JavaScript.