Aprendendo a Validar Dados em APIs Node.js com express-validator

vitoroliveira

Vítor Oliveira

Posted on March 12, 2024

Aprendendo a Validar Dados em APIs Node.js com express-validator

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
);
Enter fullscreen mode Exit fullscreen mode
  • 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

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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() });
    }
    }
);
Enter fullscreen mode Exit fullscreen mode

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.'),
Enter fullscreen mode Exit fullscreen mode

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"
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

É 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.'),
Enter fullscreen mode Exit fullscreen mode
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).'),
Enter fullscreen mode Exit fullscreen mode
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;
    }),
Enter fullscreen mode Exit fullscreen mode
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.');
    }),
Enter fullscreen mode Exit fullscreen mode
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.'),
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
vitoroliveira
Vítor Oliveira

Posted on March 12, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related