Intelligente Node.js-Formularvalidierung

Veröffentlicht: 2022-03-11

Eine der grundlegenden Aufgaben, die in einer API ausgeführt werden müssen, ist die Datenvalidierung. In diesem Artikel möchte ich Ihnen zeigen, wie Sie eine kugelsichere Validierung für Ihre Daten so hinzufügen, dass sie auch schön formatiert zurückgegeben werden.

Die benutzerdefinierte Datenvalidierung in Node.js ist weder einfach noch schnell. Es gibt viele Funktionen, die Sie schreiben müssen, um alle Arten von Daten abzudecken. Obwohl ich einige Node.js-Formulardatenbibliotheken ausprobiert habe – sowohl für Express als auch für Koa – haben sie die Anforderungen meiner Projekte nie erfüllt. Es gab Probleme beim Erweitern von Bibliotheken und die Bibliotheken arbeiteten nicht mit komplexen Datenstrukturen oder asynchroner Validierung.

Formularvalidierung in Node.js mit Datalize

Aus diesem Grund habe ich mich schließlich entschieden, meine eigene winzige, aber leistungsstarke Formularvalidierungsbibliothek namens datalize zu schreiben. Es ist erweiterbar, sodass Sie es in jedem Projekt verwenden und an Ihre Anforderungen anpassen können. Es validiert den Text, die Abfrage oder die Parameter einer Anfrage. Es unterstützt auch async Filter und komplexe JSON-Strukturen wie Arrays oder verschachtelte Objekte .

Aufstellen

Datalize kann über npm installiert werden:

 npm install --save datalize

Um den Text einer Anfrage zu parsen, sollten Sie eine separate Bibliothek verwenden. Wenn Sie noch keinen verwenden, empfehle ich koa-body für Koa oder body-parser für Express.

Sie können dieses Tutorial auf Ihren bereits eingerichteten HTTP-API-Server anwenden oder den folgenden einfachen Koa-HTTP-Servercode verwenden.

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

Dies ist jedoch kein Produktions-Setup (Sie sollten die Protokollierung verwenden, die Autorisierung erzwingen, Fehler behandeln usw.), aber diese wenigen Codezeilen werden für die Beispiele, die ich Ihnen zeigen werde, gut funktionieren.

Hinweis: Alle Codebeispiele verwenden Koa, aber der Datenvalidierungscode funktioniert auch für Express. Die datalize-Bibliothek enthält auch ein Beispiel für die Implementierung der Express-Formularvalidierung.

Ein einfaches Node.js-Formularvalidierungsbeispiel

Angenommen, Sie haben einen Koa- oder Express-Webserver und einen Endpunkt in Ihrer API, der einen Benutzer mit mehreren Feldern in der Datenbank erstellt. Einige Felder sind erforderlich, andere können nur bestimmte Werte haben oder müssen für den richtigen Typ formatiert werden.

Sie könnten eine einfache Logik wie folgt schreiben:

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

Lassen Sie uns nun diesen Code umschreiben und diese Anfrage mit datalize validieren:

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

Kürzer, sauberer, so einfach zu lesen. Mit datalize können Sie eine Liste von Feldern angeben und beliebig viele Regeln (Funktionen, die einen Fehler ausgeben, wenn die Eingabe ungültig ist) oder Filter (Funktionen, die die Eingabe formatieren) mit ihnen verketten.

Die Regeln und Filter werden in der gleichen Reihenfolge ausgeführt, in der sie definiert wurden. Wenn Sie also zuerst einen String auf Leerzeichen trimmen und dann prüfen möchten, ob er einen Wert hat, müssen Sie .trim() vor .required() definieren.

Datalize erstellt dann ein Objekt (verfügbar als .form im weiteren Kontextobjekt) mit nur den Feldern, die Sie angegeben haben, sodass Sie sie nicht erneut auflisten müssen. Die Eigenschaft .form.isValid teilt Ihnen mit, ob die Validierung erfolgreich war oder nicht.

Automatische Fehlerbehandlung

Wenn wir nicht bei jeder Anfrage prüfen möchten, ob das Formular gültig ist oder nicht, können wir eine globale Middleware hinzufügen, die die Anfrage storniert, wenn die Daten die Validierung nicht bestanden haben.

Dazu fügen wir einfach diesen Codeabschnitt zu unserer Bootstrap-Datei hinzu, in der wir unsere Koa/Express-App-Instanz erstellen.

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

Und wir müssen nicht mehr prüfen, ob Daten gültig sind, da datalize das für uns erledigt. Wenn die Daten ungültig sind, wird eine formatierte Fehlermeldung mit einer Liste ungültiger Felder zurückgegeben.

Abfragevalidierung

Ja, Sie können Ihre Abfrageparameter sogar sehr einfach validieren – es muss nicht nur mit POST -Anforderungen verwendet werden. Wir verwenden einfach die .query() , und der einzige Unterschied besteht darin, dass die Daten im Objekt .data statt in .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; });

Es gibt auch eine Hilfsmethode für die Parametervalidierung, .params() . Abfrage- und Formulardaten können zusammen validiert werden, indem zwei Datalize-Middlewares in der .post() Methode des Routers übergeben werden.

Mehr Filter, Arrays und verschachtelte Objekte

Bisher haben wir wirklich einfache Daten in unserer Node.js-Formularvalidierung verwendet. Lassen Sie uns nun einige komplexere Felder wie Arrays, verschachtelte Objekte usw. ausprobieren:

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

Wenn es keine integrierte Regel für Daten gibt, die wir validieren müssen, können wir mit der Methode .custom() (toller Name, richtig?) eine benutzerdefinierte Datenvalidierungsregel erstellen und dort die erforderliche Logik schreiben. Für verschachtelte Objekte gibt es die Methode .container() , in der Sie eine Liste von Feldern auf die gleiche Weise wie in der Funktion datalize() können. Sie können Container in Containern verschachteln oder sie mit .array() -Filtern ergänzen, die Werte in Arrays umwandeln. Wenn der .array() Filter ohne Container verwendet wird, werden die angegebenen Regeln oder Filter auf jeden Wert im Array angewendet.

.array().select(['read', 'write']) würde also prüfen, ob jeder Wert im Array entweder 'read' oder 'write' ist, und wenn dies nicht der Fall ist, wird eine Liste aller Indizes mit zurückgegeben Fehler. Ziemlich cool, oder?

PUT / PATCH

Wenn es darum geht, Ihre Daten mit PUT / PATCH (oder POST ) zu aktualisieren, müssen Sie nicht Ihre gesamte Logik, Regeln und Filter neu schreiben. Sie fügen einfach einen zusätzlichen Filter wie .optional() oder .patch() , der jedes Feld aus dem Kontextobjekt entfernt, wenn es nicht in der Anfrage definiert wurde. ( .optional() macht es immer optional, während .patch() es nur dann optional macht, wenn die Methode der HTTP-Anforderung PATCH ist.) Sie können diesen zusätzlichen Filter hinzufügen, damit er sowohl zum Erstellen als auch zum Aktualisieren von Daten in Ihrer Datenbank funktioniert.

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

Mit zwei einfachen Middlewares können wir die meiste Logik für alle POST / PUT / PATCH Methoden schreiben. Die Funktion userEditMiddleware() überprüft, ob der Datensatz, den wir bearbeiten möchten, existiert und gibt andernfalls einen Fehler aus. Dann userValidator() die Validierung für alle Endpunkte durch. Schließlich entfernt der Filter .patch() jedes Feld aus dem .form -Objekt, wenn es nicht definiert ist und die Methode der Anfrage PATCH ist.

Node.js Formularvalidierungs-Extras

In benutzerdefinierten Filtern können Sie Werte anderer Felder abrufen und darauf basierend eine Validierung durchführen. Sie können auch beliebige Daten aus dem Kontextobjekt abrufen, z. B. Anforderungs- oder Benutzerinformationen, da sie alle in Callback-Parametern für benutzerdefinierte Funktionen bereitgestellt werden.

Die Bibliothek deckt einen grundlegenden Satz von Regeln und Filtern ab, aber Sie können benutzerdefinierte globale Filter registrieren, die Sie mit beliebigen Feldern verwenden können, sodass Sie nicht immer wieder denselben Code schreiben müssen:

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

Mit diesen beiden benutzerdefinierten Filtern können Sie Ihre Felder mit .date() oder .dateTime() Filtern verketten, um die Datumseingabe zu validieren.

Dateien können auch mit datalize validiert werden: Es gibt spezielle Filter nur für Dateien wie .file() , .mime() und .size() , sodass Sie Dateien nicht separat behandeln müssen.

Beginnen Sie jetzt mit dem Schreiben besserer APIs

Ich habe Datalize bereits in mehreren Produktionsprojekten für die Node.js-Formularvalidierung sowohl für kleine als auch für große APIs verwendet. Es hat mir geholfen, großartige Projekte pünktlich und mit weniger Stress abzuliefern und sie gleichzeitig besser lesbar und wartbar zu machen. Bei einem Projekt habe ich es sogar verwendet, um Daten für WebSocket-Nachrichten zu validieren, indem ich einen einfachen Wrapper um Socket.IO geschrieben habe, und die Verwendung war so ziemlich die gleiche wie beim Definieren von Routen in Koa, das war also nett. Wenn genug Interesse besteht, schreibe ich vielleicht auch ein Tutorial dafür.

Ich hoffe, dieses Tutorial hilft Ihnen und ich baue bessere APIs in Node.js mit perfekt validierten Daten ohne Sicherheitsprobleme oder interne Serverfehler. Und vor allem hoffe ich, dass es Ihnen eine Menge Zeit ersparen wird, die Sie sonst in das Schreiben zusätzlicher Funktionen für die Formularvalidierung mit JavaScript investieren müssten.