Aprendendo a Validar Dados em APIs Node.js com express-validator
Vítor Oliveira
Posted on March 12, 2024
Introducação
A validação dos dados de entrada é importante porque garante a integridade e a validade dos dados que recebemos, e isso é crucial para um projeto bem sucedido. Pensando nisso comentei com minha esposa — que também é programadora, sobre minha necessidade. Ela então comentou comigo sobre o DataAnnotation do C# que tem essa função, foi então que comecei a pesquisar uma alternativa para minha stack e encontrei o express-validator, que agora vou compartilhar a implementação das validações na minha API.
1. Por que usar o express-validator
É um conjunto de middlewares popular para express.js que envolve a extensa coleção de validadores e higienizadores oferecidos pelo validator.js com a finalidade de validar solicitações HTTP. Com algumas combinações, é possível:
- Validação e verificação dos dados/campos aninhados ou arrays de objetos.
- Validações em cadeia para um único campo.
- Validações personalizadas, como por exemplo validação assíncrona e até mesmo arquivos. Exemplo:
app.post('/users',
[
// Verifica se o e-mail já está em uso
body('email').custom(async (value) => {
const user = await User.findOne({ email: value });
if (user) {
throw new Error('E-mail já está em uso');
}
}),
],
// Resto da lógica para criação de usuário
);
- Sanitização dos dados, permitindo a manipulação do mesmo para usá-los na aplicação.
- Personalização das mensagens de erro.
É relativamente simples, até pensei em usar o Joi para essas validações, mas para o meu contexto express-validator serviu muito bem!
2. Colocando em Prática
Para começar é bem simples, basta instalar. É a única configuração requerida para começar a usar.
npm install express-validator
No meu contexto as regras de validação não estão diretamente na rota, elas foram encapsuladas em um local separado, dessa forma continuo garantindo a validação dos dados e organizo meu código.
//minutes.routes.js
import { Router } from 'express';
import insertMinutesController from '../../controllers/insertMinutesController.js';
import { dataValidation } from '../middlewares/validations.js';
const minutesRoutes = Router();
minutesRoutes.post('/insert', dataValidation, insertMinutesController.insertMinutes);
export default minutesRoutes;
Agora antes de seguirmos para as validações contidas em dataValidation, qualquer dado em não conformidade com as validações será entendido como um erro e capturado pelo validationResult
.
No meu caso, adicionei essa função no arquivo insertMinutesController.
//insertMinutesController.js
import { validationResult } from 'express-validator';
const insertMinutes = async (req, res, next) => {
// Verifica se está tudo certo com a requisição
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
// Resto da lógica para criação da ata
};
export default {
insertMinutes
}
Também é possível fazer diretamente na rota, logo após o middleware.
Exemplo:
app.post('/users',
[
// Verifica se o e-mail já está em uso
body('email').custom(async (value) => {
const user = await User.findOne({ email: value });
if (user) {
throw new Error('E-mail já está em uso');
}
}),
],
// Resto da lógica para criação de usuário
(req, res) => {
// Verifica se houve erros de validação
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
}
);
Minhas validações
Não vou colocar o código na integra, no final do artigo deixo o link do repositório dessa solução que estou desenvolvendo.
Na minha API até o momento foi necessário validar quatro casos:
- String
- Number
- Data
- 3 Tipos de Validações customizadas
String
Alguns campos na minha API não são obrigatórios mas possuem um tipo de dado específico. Neste caso usei isString()
assinalando que espero um string.
Exemplo:
//validations.js
body('sacramentMeeting.authorities')
.isString().withMessage('The value must be text.'),
Sendo assim eu posso receber uma string vazia, mas sempre deve ter esse tipo. Caso seja enviado null
no campo. O seguinte erro vai aparecer com a mensagem escolhida:
//Estrutura padrão de erros
{
"errors": [
{
"type": "field",
"value": null,
"msg": "The value must be text.",
"path": "sacramentMeeting.authorities",
"location": "body"
}
]
}
É possível modificar essa estrutura.
Number
Esse campo da minha API recebe um número correspondente a música escolhida, então isNumeric()
me ajudou a garantir o tipo.
Exemplo:
//validations.js
body('sacramentMeeting.specialhymn')
.isNumeric().withMessage('The value must be numeric.'),
Data
Além do tipo validado que nesse caso é uma data, aqui também entra mais uma função de validação. O notEmpty()
ajuda bastante também a garantir que o campo não seja vazio.
Exemplo:
//validations.js
body('sacramentMeeting.date')
.notEmpty().withMessage('The "date" is required.')
.isISO8601().withMessage('The data must be in ISO 8601 format (YYYY-MM-DD).'),
1° Validação Customizada
Essa validação me marcou porque esse campo não é tão importante aponto de ter um tabela reservada no banco. Mas também não é tão irrelevante assim, então descobri que no PostgreSQL é possível criar uma coluna varchar como array, isso mesmo que você leu, basta usar VARCHAR(510)[]
. Dessa forma posso trabalhar com os dados dessa coluna usando métodos de percorrer, porque um array de strings será retornado.
Primeiro eu começo confirmando se é um array com isArray()
, se não for a mensagem contida em withMessage()
é lançada.
Na próxima sim, temos a validação personalizada, sinalizada pela função custom()
onde o valor enviado na requisição é capturado com value
e percorrido em seguida para ser validado.
Exemplo:
//validations.js
body('sacramentMeeting.visitors')
.isArray().withMessage('The value must be an array.')
.custom((value) => {
for (let item of value) {
if (typeof item !== 'string') {
throw new Error('All elements in the array must be strings.');
}
}
return true;
}),
2° Validação Customizada
Essa validação foi mais incrementada porque o campo firstspeaker
pode ser opcional apenas no primeiro domingo de cada mês. Então como eu recebo uma data como string ("2024-02-21"), o método createLocalDate() converte esse valor para um objeto do tipo Date
.
É importante observar que o mês é decrementado em 1, pois no JavaScript os meses são indexados a partir de 0 (janeiro é 0, fevereiro é 1 e assim por diante).
Dessa forma posso saber em isFirstSunday() se a data atual é o primeiro domingo do mês. Se for, será retornado true
, se não será false
.
O método getDate() retorna o número do dia do mês, enquanto getDay() retorna o número do dia da semana.
Por último, precisava validar caso não fosse o primeiro domingo do mês, então usando value?.trim()
consigo validar se for enviado null
ou string
vazia, isso porque ambos em um contexto Boleano é entendido como false
no javascript. Então se for enviado o valor corretamente, apenas será removido os espaços do começo e fim do valor.
Exemplo:
//validations.js
body('sacramentMeeting.firstspeaker')
.custom((value, { req }) => {
if (isFirstSunday(createLocalDate(req.body.sacramentMeeting.date)) || value?.trim()) return true;
throw new Error('The "third speaker" is missing.');
}),
3° Validação Customizada
E por último tenho o campo testimonies
, um array com objetos contendo os nomes de quem prestou um testemunho. Sendo assim verifico se o array é null ou se é um array com a estrutura esperada, seguindo: array -> objects -> propriedade name (string).
Exemplo:
//validations.js
body('testimonies')
.custom((array) => array === null || (Array.isArray(array) && array.every((item) => typeof item === 'object' && typeof item.name === 'string')))
.withMessage('The value must be string array.'),
3. Conclusão
Utilizar o express-validator pode ser uma mão na roda para validar os dados de entrada sem muito esforço e ainda garantir segurança. Dependendo do contexto e preferência pessoal, pode ser interessante utilizar o Joi para essa finalidade.
Se chegou até aqui, muito obrigado pelo seu tempo e atenção!
Posted on March 12, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.