Проверка формы Smart Node.js
Опубликовано: 2022-03-11Одной из фундаментальных задач, выполняемых в API, является проверка данных. В этой статье я хотел бы показать вам, как добавить пуленепробиваемую проверку ваших данных таким образом, чтобы они также возвращались в красивом формате.
Выполнение пользовательской проверки данных в Node.js не является ни простым, ни быстрым. Вам нужно написать много функций, чтобы охватить любые данные. Хотя я пробовал несколько библиотек данных форм Node.js — как для Express, так и для Koa — они никогда не удовлетворяли потребности моих проектов. Были проблемы с расширением библиотек, и библиотеки не работали со сложными структурами данных или асинхронной проверкой.
Проверка формы в Node.js с помощью Datalize
Вот почему я в конце концов решил написать свою собственную маленькую, но мощную библиотеку проверки формы под названием datalize. Его можно расширять, поэтому вы можете использовать его в любом проекте и настроить в соответствии со своими требованиями. Он проверяет тело запроса, запрос или параметры. Он также поддерживает async
фильтры и сложные структуры JSON, такие как массивы или вложенные объекты .
Настраивать
Datalize можно установить через npm:
npm install --save datalize
Для разбора тела запроса следует использовать отдельную библиотеку. Если вы его еще не используете, я рекомендую koa-body для Koa или body-parser для Express.
Вы можете применить это руководство к уже настроенному серверу HTTP API или использовать следующий простой код 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');
Однако это не производственная установка (вы должны использовать ведение журнала, принудительную авторизацию, обработку ошибок и т. д.), но эти несколько строк кода отлично подойдут для примеров, которые я вам покажу.
Примечание. Во всех примерах кода используется Koa, но код проверки данных будет работать и для Express. В библиотеке datalize также есть пример реализации экспресс-проверки формы.
Пример базовой проверки формы Node.js
Допустим, у вас есть веб-сервер Koa или Express и конечная точка в вашем API, которая создает пользователя с несколькими полями в базе данных. Некоторые поля являются обязательными, а некоторые могут иметь только определенные значения или должны быть отформатированы для правильного типа.
Вы можете написать простую логику следующим образом:
/** * @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(); });
Теперь давайте перепишем этот код и проверим этот запрос с помощью 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(); });
Короче, чище, так легко читается. С помощью datalize вы можете указать список полей и связать с ними столько правил (функций, которые выдают ошибку, если ввод недействителен) или фильтров (функций, которые форматируют ввод), сколько хотите.
Правила и фильтры выполняются в том же порядке, в котором они определены, поэтому, если вы хотите сначала обрезать строку для пробелов, а затем проверить, имеет ли она какое-либо значение, вы должны определить .trim()
перед .required()
.
Затем Datalize создаст объект (доступный как .form
в объекте более широкого контекста) только с указанными вами полями, поэтому вам не нужно снова их перечислять. Свойство .form.isValid
сообщает вам, была ли проверка успешной или нет.
Автоматическая обработка ошибок
Если мы не хотим проверять, действительна ли форма при каждом запросе, мы можем добавить глобальное промежуточное ПО, которое отменяет запрос, если данные не прошли проверку.
Для этого мы просто добавляем этот фрагмент кода в наш файл начальной загрузки, где мы создаем экземпляр нашего приложения 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'); } });
И нам больше не нужно проверять, действительны ли данные, так как datalize сделает это за нас. Если данные недействительны, он вернет отформатированное сообщение об ошибке со списком недопустимых полей.
Проверка запроса
Да, вы даже можете очень легко проверить параметры своего запроса — его не обязательно использовать только с запросами POST
. Мы просто используем вспомогательный метод .query()
, и единственное отличие состоит в том, что данные хранятся в объекте .data
, а не в .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; });
Существует также вспомогательный метод для проверки параметров, .params()
. Данные запроса и формы можно проверить вместе, передав два компонента промежуточного слоя datalize в метод маршрутизатора .post()
.
Дополнительные фильтры, массивы и вложенные объекты
До сих пор мы использовали очень простые данные для проверки формы Node.js. Теперь давайте попробуем использовать более сложные поля, такие как массивы, вложенные объекты и т. д.:

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, )); });
Если нет встроенного правила для данных, которые нам нужно проверить, мы можем создать собственное правило проверки данных с помощью .custom()
(отличное название, правда?) и написать там необходимую логику. Для вложенных объектов есть метод .container()
, в котором можно указать список полей так же, как и в функции datalize()
. Вы можете вкладывать контейнеры в контейнеры или дополнять их .array()
, которые преобразуют значения в массивы. Когда фильтр .array()
используется без контейнера, указанные правила или фильтры применяются к каждому значению в массиве.
Таким образом .array().select(['read', 'write'])
будет проверять, является ли каждое значение в массиве либо 'read'
либо 'write'
и если нет, он вернет список всех индексов с ошибки. Довольно круто, да?
PUT
/ PATCH
Когда дело доходит до обновления ваших данных с помощью PUT
/ PATCH
(или POST
), вам не нужно переписывать всю свою логику, правила и фильтры. Вы просто добавляете дополнительный фильтр, например .optional()
или .patch()
, который удалит любое поле из объекта контекста, если оно не было определено в запросе. ( .optional()
сделает его всегда необязательным, тогда как .patch()
сделает его необязательным, только если метод HTTP-запроса PATCH
.) Вы можете добавить этот дополнительный фильтр, чтобы он работал как для создания, так и для обновления данных в вашей базе данных.
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(); });
С двумя простыми промежуточными программами мы можем написать большую часть логики для всех методов POST
/ PUT
/ PATCH
. Функция userEditMiddleware()
проверяет, существует ли запись, которую мы хотим отредактировать, и в противном случае выдает ошибку. Затем userValidator()
выполняет проверку для всех конечных точек. Наконец, фильтр .patch()
удалит любое поле из объекта .form
, если оно не определено и если метод запроса — PATCH
.
Дополнительные возможности проверки формы Node.js
В пользовательских фильтрах вы можете получить значения других полей и выполнить проверку на их основе. Вы также можете получить любые данные из объекта контекста, такие как запрос или информацию о пользователе, поскольку все это предоставляется в параметрах обратного вызова пользовательской функции.
Библиотека охватывает базовый набор правил и фильтров, но вы можете зарегистрировать собственные глобальные фильтры, которые можно использовать с любыми полями, чтобы вам не приходилось писать один и тот же код снова и снова:
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); };
С помощью этих двух настраиваемых фильтров вы можете связать свои поля с .date()
или .dateTime()
для проверки ввода даты.
Файлы также можно проверить с помощью datalize: существуют специальные фильтры только для таких файлов, как .file()
, .mime()
и .size()
, поэтому вам не нужно обрабатывать файлы отдельно.
Начните писать лучшие API прямо сейчас
Я уже использовал datalize для проверки формы Node.js в нескольких производственных проектах как для малых, так и для больших API. Это помогло мне выполнять отличные проекты вовремя и с меньшим стрессом, делая их более читабельными и удобными в сопровождении. В одном проекте я даже использовал его для проверки данных для сообщений WebSocket, написав простую оболочку вокруг Socket.IO, и использование было почти таким же, как определение маршрутов в Koa, так что это было приятно. Если будет достаточно интереса, я мог бы написать учебник и для этого.
Я надеюсь, что это руководство поможет вам, и я создам лучшие API-интерфейсы в Node.js с полностью проверенными данными без проблем с безопасностью или внутренних ошибок сервера. И самое главное, я надеюсь, что это сэкономит вам массу времени, которое в противном случае вам пришлось бы потратить на написание дополнительных функций для проверки формы с использованием JavaScript.