Aprendendo a Validar Dados em APIs Node.js com express-validator
Tags: node, api, expressvalidator
Published: 2024-03-12 17:17:39

##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:
```js
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.
```js
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. 
```js
//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.
```js
//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:
```js
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:
```js
//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:
```json
//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:
```js
//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:
```js
//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:
```js
//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:
```js
//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:
```js
//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!