Validação de formulário inteligente Node.js
Publicados: 2022-03-11Uma das tarefas fundamentais a serem executadas em uma API é a validação de dados. Neste artigo, gostaria de mostrar como adicionar validação à prova de balas para seus dados de uma maneira que também os retorne bem formatados.
Fazer a validação de dados personalizados no Node.js não é fácil nem rápido. Há muitas funcionalidades que você precisa escrever para cobrir qualquer tipo de dados. Embora eu tenha tentado algumas bibliotecas de dados de formulário Node.js — tanto para Express quanto para Koa — elas nunca atenderam às necessidades dos meus projetos. Houve problemas com a extensão de bibliotecas e as bibliotecas não trabalharam com estruturas de dados complexas ou validação assíncrona.
Validação de formulário em Node.js com Datalize
É por isso que finalmente decidi escrever minha própria biblioteca de validação de formulário pequena, mas poderosa, chamada datalize. É extensível, para que você possa usá-lo em qualquer projeto e personalizá-lo de acordo com suas necessidades. Ele valida o corpo, a consulta ou os parâmetros de uma solicitação. Ele também suporta filtros async
e estruturas JSON complexas como arrays ou objetos aninhados .
Configuração
Datalize pode ser instalado via npm:
npm install --save datalize
Para analisar o corpo de uma solicitação, você deve usar uma biblioteca separada. Se você ainda não usa um, recomendo koa-body para Koa ou body-parser para Express.
Você pode aplicar este tutorial ao seu servidor de API HTTP já configurado ou usar o seguinte código de servidor HTTP Koa simples.
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');
No entanto, esta não é uma configuração de produção (você deve usar log, impor autorização, lidar com erros etc.), mas essas poucas linhas de código funcionarão bem para os exemplos que mostrarei a você.
Observação: todos os exemplos de código usam Koa, mas o código de validação de dados também funcionará para o Express. A biblioteca dataize também possui um exemplo de implementação de validação de formulários Express.
Um exemplo básico de validação de formulário Node.js
Digamos que você tenha um servidor web Koa ou Express e um endpoint em sua API que cria um usuário com vários campos no banco de dados. Alguns campos são obrigatórios e alguns podem ter apenas valores específicos ou devem ser formatados para o tipo correto.
Você poderia escrever uma lógica simples como esta:
/** * @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(); });
Agora vamos reescrever esse código e validar essa solicitação 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(); });
Mais curto, mais limpo, tão fácil de ler. Com datalize, você pode especificar uma lista de campos e encadear a eles quantas regras (funções que geram um erro se a entrada for inválida) ou filtros (funções que formatam a entrada) que desejar.
As regras e filtros são executados na mesma ordem em que são definidos, então se você quiser cortar uma string para espaços em branco primeiro e depois verificar se ela tem algum valor, você deve definir .trim()
antes .required()
.
O Datalize então criará um objeto (disponível como .form
no objeto de contexto mais amplo) apenas com os campos que você especificou, para que você não precise listá-los novamente. A propriedade .form.isValid
informa se a validação foi bem-sucedida ou não.
Tratamento automático de erros
Se não quisermos verificar se o formulário é válido ou não a cada solicitação, podemos adicionar um middleware global que cancela a solicitação se os dados não passarem na validação.
Para fazer isso, basta adicionar este pedaço de código ao nosso arquivo bootstrap onde criamos nossa instância do aplicativo 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 não precisamos mais verificar se os dados são válidos, pois o datalize fará isso por nós. Se os dados forem inválidos, ele retornará uma mensagem de erro formatada com uma lista de campos inválidos.
Validação de consulta
Sim, você pode até validar seus parâmetros de consulta com muita facilidade—não precisa ser usado apenas com solicitações POST
. Nós apenas usamos o método auxiliar .query()
, e a única diferença é que os dados são armazenados no objeto .data
em vez 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; });
Há também um método auxiliar para validação de parâmetros, .params()
. Os dados da consulta e do formulário podem ser validados juntos passando dois middlewares datalize no método .post()
do roteador.

Mais filtros, matrizes e objetos aninhados
Até agora, usamos dados muito simples em nossa validação de formulário Node.js. Agora vamos tentar alguns campos mais complexos como arrays, objetos aninhados, 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, )); });
Se não houver uma regra interna para os dados que precisamos validar, podemos criar uma regra de validação de dados personalizada com o método .custom()
(grande nome, certo?) e escrever a lógica necessária lá. Para objetos aninhados, existe o método .container()
no qual você pode especificar uma lista de campos da mesma forma que na função datalize()
. Você pode aninhar contêineres dentro de contêineres ou complementá-los com filtros .array()
, que convertem valores em matrizes. Quando o filtro .array()
é usado sem um contêiner, as regras ou filtros especificados são aplicados a cada valor na matriz.
Então .array().select(['read', 'write'])
verificaria se cada valor no array é 'read'
ou 'write'
e se algum não for, ele retornará uma lista de todos os índices com erros. Bem legal, hein?
PUT
/ PATCH
Quando se trata de atualizar seus dados com PUT
/ PATCH
(ou POST
), você não precisa reescrever toda a sua lógica, regras e filtros. Basta adicionar um filtro extra como .optional()
ou .patch()
, que removerá qualquer campo do objeto de contexto se não tiver sido definido na solicitação. ( .optional()
o tornará sempre opcional, enquanto .patch()
o tornará opcional apenas se o método da solicitação HTTP for PATCH
.) Você pode adicionar esse filtro extra para que ele funcione tanto para criar quanto para atualizar dados em seu banco de dados.
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(); });
Com dois middlewares simples, podemos escrever a maior parte da lógica para todos os métodos POST
/ PUT
/ PATCH
. A função userEditMiddleware()
verifica se o registro que queremos editar existe e gera um erro caso contrário. Em seguida, userValidator()
faz a validação para todos os endpoints. Finalmente, o filtro .patch()
removerá qualquer campo do objeto .form
se não estiver definido e se o método da solicitação for PATCH
.
Extras de validação de formulário Node.js
Em filtros personalizados, você pode obter valores de outros campos e realizar a validação com base nisso. Você também pode obter quaisquer dados do objeto de contexto, como solicitação ou informações do usuário, pois tudo isso é fornecido nos parâmetros de retorno de chamada da função personalizada.
A biblioteca abrange um conjunto básico de regras e filtros, mas você pode registrar filtros globais personalizados que podem ser usados com qualquer campo, para não precisar escrever o mesmo código repetidamente:
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); };
Com esses dois filtros personalizados, você pode encadear seus campos com .date()
ou .dateTime()
para validar a entrada de data.
Arquivos também podem ser validados usando datalize: Existem filtros especiais apenas para arquivos como .file()
, .mime()
e .size()
para que você não precise lidar com arquivos separadamente.
Comece a escrever APIs melhores agora
Eu tenho usado datalize para validação de formulário Node.js em vários projetos de produção já, para APIs pequenas e grandes. Isso me ajudou a entregar ótimos projetos no prazo e com menos estresse, tornando-os mais legíveis e fáceis de manter. Em um projeto eu usei até mesmo para validar dados para mensagens WebSocket escrevendo um wrapper simples em torno de Socket.IO e o uso foi praticamente o mesmo que definir rotas no Koa, então isso foi bom. Se houver interesse suficiente, eu poderia escrever um tutorial para isso também.
Espero que este tutorial ajude você e eu construo APIs melhores em Node.js, com dados perfeitamente validados sem problemas de segurança ou erros internos do servidor. E o mais importante, espero que isso economize muito tempo que você teria que investir na escrita de funções extras para validação de formulários usando JavaScript.