Inteligentna weryfikacja formularzy Node.js
Opublikowany: 2022-03-11Jednym z podstawowych zadań do wykonania w API jest walidacja danych. W tym artykule chciałbym pokazać, jak dodać kuloodporną walidację danych w sposób, który również zwraca je ładnie sformatowane.
Przeprowadzanie niestandardowej walidacji danych w Node.js nie jest ani łatwe, ani szybkie. Istnieje wiele funkcji, które musisz napisać, aby pokryć wszelkiego rodzaju dane. Chociaż wypróbowałem kilka bibliotek danych Node.js — zarówno dla Express, jak i Koa — nigdy nie spełniły one potrzeb moich projektów. Wystąpiły problemy z rozszerzaniem bibliotek i biblioteki nie pracujące ze złożonymi strukturami danych lub walidacją asynchroniczną.
Walidacja formularzy w Node.js z Datalize
Dlatego w końcu zdecydowałem się napisać własną małą, ale potężną bibliotekę do sprawdzania poprawności formularzy o nazwie datalize. Jest rozszerzalny, dzięki czemu możesz go użyć w dowolnym projekcie i dostosować do swoich wymagań. Weryfikuje treść, zapytanie lub parametry żądania. Obsługuje również filtry async
i złożone struktury JSON, takie jak tablice lub obiekty zagnieżdżone .
Organizować coś
Datalize można zainstalować przez npm:
npm install --save datalize
Aby przeanalizować treść żądania, powinieneś użyć osobnej biblioteki. Jeśli jeszcze go nie używasz, polecam koa-body dla Koa lub body-parser dla Express.
Możesz zastosować ten samouczek do już skonfigurowanego serwera HTTP API lub użyć następującego prostego kodu serwera 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');
Nie jest to jednak konfiguracja produkcyjna (powinieneś użyć logowania, wymusić autoryzację, obsłużyć błędy itp.), ale te kilka linijek kodu będzie działać dobrze w przykładach, które ci pokażę.
Uwaga: Wszystkie przykłady kodu używają kodu Koa, ale kod sprawdzania poprawności danych będzie działał również w przypadku programu Express. Biblioteka datalize zawiera również przykład implementacji ekspresowej walidacji formularzy.
Podstawowy przykład walidacji formularzy Node.js
Załóżmy, że masz serwer WWW Koa lub Express i punkt końcowy w swoim interfejsie API, który tworzy użytkownika z kilkoma polami w bazie danych. Niektóre pola są wymagane, a niektóre mogą mieć tylko określone wartości lub muszą być sformatowane do prawidłowego typu.
Możesz napisać prostą logikę tak:
/** * @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(); });
Teraz przepiszmy ten kod i zweryfikujmy to żądanie za pomocą 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(); });
Krótszy, czystszy, tak czytelny. Dzięki datalize możesz określić listę pól i połączyć z nimi dowolną liczbę reguł (funkcji, które zgłaszają błąd, jeśli dane wejściowe są nieprawidłowe) lub filtrów (funkcji formatujących dane wejściowe).
Reguły i filtry są wykonywane w tej samej kolejności, w jakiej zostały zdefiniowane, więc jeśli chcesz najpierw przyciąć ciąg do białych znaków, a następnie sprawdzić, czy ma on jakąkolwiek wartość, musisz zdefiniować .trim()
przed .required()
.
Datalize następnie utworzy obiekt (dostępny jako .form
w szerszym obiekcie kontekstowym) tylko z określonymi polami, więc nie musisz ich ponownie wymieniać. Właściwość .form.isValid
informuje, czy weryfikacja się powiodła, czy nie.
Automatyczna obsługa błędów
Jeśli nie chcemy przy każdym żądaniu sprawdzać, czy formularz jest poprawny, czy nie, możemy dodać globalne oprogramowanie pośredniczące, które anuluje żądanie, jeśli dane nie przeszły walidacji.
Aby to zrobić, po prostu dodajemy ten fragment kodu do naszego pliku bootstrap, w którym tworzymy naszą instancję aplikacji 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 nie musimy już sprawdzać, czy dane są prawidłowe, ponieważ datalize zrobi to za nas. Jeśli dane są nieprawidłowe, zwróci sformatowany komunikat o błędzie z listą nieprawidłowych pól.
Weryfikacja zapytania
Tak, możesz nawet bardzo łatwo zweryfikować parametry zapytania — nie trzeba tego używać tylko z żądaniami POST
. Używamy po prostu metody pomocniczej .query()
, a jedyną różnicą jest to, że dane są przechowywane w obiekcie .data
zamiast .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; });
Istnieje również metoda pomocnicza do walidacji parametrów, .params()
. Dane zapytania i formularza można sprawdzić razem, przekazując dwa programy pośredniczące datalize w metodzie .post()
routera.
Więcej filtrów, tablic i obiektów zagnieżdżonych
Do tej pory używaliśmy naprawdę prostych danych w naszej walidacji formularzy Node.js. Teraz wypróbujmy bardziej złożone pola, takie jak tablice, obiekty zagnieżdżone itp.:

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, )); });
Jeśli nie ma wbudowanej reguły dla danych, które musimy zweryfikować, możemy utworzyć niestandardową regułę walidacji danych za pomocą metody .custom()
(świetna nazwa, prawda?) i napisać tam niezbędną logikę. W przypadku obiektów zagnieżdżonych istnieje metoda .container()
, w której można określić listę pól w taki sam sposób, jak w funkcji datalize()
. Możesz zagnieżdżać kontenery w kontenerach lub uzupełniać je .array()
, które konwertują wartości na tablice. Gdy filtr .array()
jest używany bez kontenera, określone reguły lub filtry są stosowane do każdej wartości w tablicy.
Tak więc .array().select(['read', 'write'])
sprawdzi, czy każda wartość w tablicy jest 'read'
lub 'write'
a jeśli nie, zwróci listę wszystkich indeksów z błędy. Całkiem fajnie, co?
PUT
/ PATCH
Jeśli chodzi o aktualizację danych za pomocą PUT
/ PATCH
(lub POST
), nie musisz przepisywać całej swojej logiki, reguł i filtrów. Wystarczy dodać dodatkowy filtr, taki jak .optional()
lub .patch()
, który usunie dowolne pole z obiektu kontekstu, jeśli nie zostało ono zdefiniowane w żądaniu. ( .optional()
sprawi, że będzie on zawsze opcjonalny, podczas gdy .patch()
uczyni go opcjonalnym tylko wtedy, gdy metodą żądania HTTP jest PATCH
.) Możesz dodać ten dodatkowy filtr, aby działał zarówno podczas tworzenia, jak i aktualizowania danych w Twojej bazie danych.
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(); });
Dzięki dwóm prostym oprogramowaniu pośredniczącym możemy napisać większość logiki dla wszystkich metod POST
/ PUT
/ PATCH
. Funkcja userEditMiddleware()
sprawdza, czy rekord, który chcemy edytować, istnieje i w przeciwnym razie zgłasza błąd. Następnie userValidator()
wykonuje walidację dla wszystkich punktów końcowych. Na koniec filtr .patch()
usunie wszystkie pola z obiektu .form
, jeśli nie jest ono zdefiniowane i jeśli metodą żądania jest PATCH
.
Dodatki do walidacji formularzy Node.js
W filtrach niestandardowych możesz uzyskać wartości innych pól i na ich podstawie przeprowadzić walidację. Możesz również uzyskać dowolne dane z obiektu kontekstu, takie jak żądanie lub informacje o użytkowniku, ponieważ wszystkie są dostarczane w niestandardowych parametrach wywołania zwrotnego funkcji.
Biblioteka obejmuje podstawowy zestaw reguł i filtrów, ale możesz zarejestrować niestandardowe filtry globalne, których możesz używać z dowolnymi polami, dzięki czemu nie musisz w kółko pisać tego samego kodu:
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); };
Dzięki tym dwóm niestandardowym filtrom możesz połączyć pola z .date()
lub .dateTime()
w celu sprawdzenia poprawności danych wejściowych.
Pliki można również sprawdzać za pomocą datalize: istnieją specjalne filtry tylko dla plików, takich jak .file()
, .mime()
i .size( .size()
, dzięki czemu nie musisz oddzielnie obsługiwać plików.
Zacznij pisać lepsze interfejsy API teraz
Używałem datalize do walidacji formularzy Node.js już w kilku projektach produkcyjnych, zarówno dla małych, jak i dużych interfejsów API. Pomogło mi to w dostarczaniu świetnych projektów na czas i przy mniejszym stresie, jednocześnie czyniąc je bardziej czytelnymi i łatwiejszymi w utrzymaniu. W jednym projekcie użyłem go nawet do walidacji danych dla komunikatów WebSocket, pisząc prosty wrapper wokół Socket.IO, a użycie było prawie takie samo, jak definiowanie tras w Koa, więc było to miłe. Jeśli zainteresowanie jest wystarczające, mogę napisać samouczek również na ten temat.
Mam nadzieję, że ten samouczek ci pomoże i buduję lepsze interfejsy API w Node.js, z doskonale zweryfikowanymi danymi , bez problemów z bezpieczeństwem lub wewnętrznych błędów serwera. A co najważniejsze, mam nadzieję, że zaoszczędzi to mnóstwo czasu, który w przeciwnym razie musiałbyś zainwestować w pisanie dodatkowych funkcji do walidacji formularzy przy użyciu JavaScript.