Validando dados com class-validator e class-transformer - NestJS
Validar dados em uma API é essencial para garantir integridade, segurança e confiabilidade. Isso evita o processamento de informações inválidas ou maliciosas. Aprenda a usar class-validator e class-transformer para validar dados de forma segura em APIs desenvolvidas com NestJS.
Uma regra básica que todo desenvolvedor de uma API deve ter clara em sua mente é, nunca confie nos dados do frontend, nunca! Toda informação que chega na sua API deve ser sanitizada e validada, sempre!
Imagine que você está desenvolvendo uma API REST para um e-commerce. A cada requisição, você recebe dados do cliente, como nome, email e endereço. Mas e se esses dados forem inválidos, como um email sem o "@" ou um CEP com menos de 8 dígitos? É aí que a validação entra em cena.
Garantir a integridade da informação recebida, garante o correto funcionamento do código escrito. É nesse contexto que o pacote class-validator
cai como uma luva, facilitando a validação dos dados.
Neste texto será abordado como utilizar o pacote class-validator
em conjunto com o class-transformer
para validar dados de uma forma bem simples e direta em uma API que usa o framework NestJS. Vale destacar que grande parte do conteúdo apresentado pode ser aplicado a outros projetos, mesmo que não utilizem o NestJS.
Instalando e criando o projeto com NestJS
Para criar o projeto foi utilizado o seguinte script:
# É necessário ter Node.js instalado. Para
# este exemplo, utilizei a seguinte versão:
node --version
v22.11.0
# Instalando o CLI do NestJS
npm i -g @nestjs/cli
# Iniciando o projeto
nest new consolelog-class-validator
cd consolelog-class-validator/
npm i --save class-validator class-transformer
# Executando o projeto:
npm run start:dev
Após executar os comando acima, o projeto estará em execução. Para verificar o correto funcionamento, utilize o cURL, um navegador ou qualquer outro cliente TCP (Postman, Insomnia, etc) para consultar o endereço http://localhost:3000. A resposta esperada é Hello World
:
curl localhost:3000
Hello World!%
Validando os dados que chegam na API
Iniciando pelo exemplo mais simples, considere um endpoint que receberá algumas informações de um POST. Essas informações podem ser mapeadas em uma classe e através de decorators do pacote class-validator
podemos configurar a validação desejada.
Dto
, indica Data Transfer Object. Nada mais é do que um objeto simples, sem comportamento, utilizado para transportar dados entre diferentes partes da aplicação.Com os dados que serão recebidos no POST mapeados na classe NovoUsuarioDto
, podemos indicar ao NestJS que o corpo (body) da requisição deverá ser validado. Para fazer isso, precisamos:
- Indicar que o tipo da informação é representada pela classe
NovoUsuarioDto
- aplicar o decorator
@UsePipes(new ValidationPipe())
POST /api/v1/usuarios
.Para iniciar os testes, basta executar o projeto com o comando npm run start:dev
e então utilizar alguma ferramenta como o cURL para efetuar os testes.
Testando o cenário de sucesso
Neste teste os dados atendem aos requisitos de validação:
curl -i -X POST \
--header 'content-type: application/json' \
--data '{ "nome": "teste", "email": "[email protected]", "desejaReceberEmails": true }' \
localhost:3000
# Resultado:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
{"nome":"teste","email":"[email protected]","desejaReceberEmails":true}%
Testando o cenário de erro
Realizando outro teste, ao omitir a propriedade desejaReceberEmails
, os requisitos de validação não serão atendidos e a resposta é HTTP/400 Bad Request:
curl -i -X POST \
--header 'content-type: application/json' \
--data '{ "nome": "teste", "email": "[email protected]" }' \
localhost:3000
# Resultado:
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{"message":["desejaReceberEmails must be a boolean value"],"error":"Bad Request","statusCode":400}%
Aplicando a validação para todo o projeto
Para não ser necessário utilizar o decorator @UsePipes(new ValidationPipe())
em cada método de cada controller que for receber dados, é possível configurar essa validação de forma global, como abaixo:
Removendo informações não desejadas
Um outro cenário passível de tratamento é quando a API recebe mais dados do que o necessário. No exemplo abaixo, o campoExtra
é enviado no corpo da requisição, mas não está mapeado na classe NovoUsuarioDto
. Nesse caso, a validação será aplicada apenas aos campos declarados na classe mencionada, enquanto os campos adicionais permanecerão intactos, exatamente como foram enviados na requisição:
curl -i -X POST \
--header 'content-type: application/json' \
--data '{ "nome": "teste", "email": "[email protected]", "desejaReceberEmails": true, "campoExtra": "extra" }' \
localhost:3000
# Resultado:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
{"nome":"teste","email":"[email protected]","desejaReceberEmails":true,"campoExtra":"extra"}
Uma forma simples de eliminarmos estes campos não mapeados é utilizando a opção whitelist: true
:
@Post()
@UsePipes(new ValidationPipe({ whitelist: true }))
criarUsuario(@Body() body: NovoUsuarioDto) {
return body;
}
Assim, os campos que não estiverem mapeados com decorators na classe serão ignorados. Observe abaixo que o campoExtra
não foi mapeado, portanto ignorado no processo de validação e transformação:
curl -i -X POST \
--header 'content-type: application/json' \
--data '{ "nome": "teste", "email": "[email protected]", "desejaReceberEmails": true, "campoExtra": "extra" }' \
localhost:3000
# Resultado:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
{"nome":"teste","email":"[email protected]","desejaReceberEmails":true}
Transformando dados para o tipo correto
Outro cenário comum, é o ajuste de tipos de dados. Por exemplo, no formato JSON, datas são representadas como strings, como em 2024-12-01T12:00:00.000Z
. Nesse caso, pode ser interessante converter a informação diretamente para o tipo Date
e depois aplicar a validação desejada. Um exemplo similar ocorre com valores booleanos, que podem ser representados de diversas formas: "true"
, "false"
, "1"
ou "0"
. Veja abaixo como esses casos podem ser tratados com o @Type
e @Transform
:
Efetuando um teste com cenário de sucesso:
curl -i -X POST \
--header 'content-type: application/json' \
--data '{ "data": "2024-12-01T12:00:00.000Z", "temMaisDetalhes": "false" }' \
localhost:3000/exemplo-conversao-dados
# Resultado
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
{"data":"2024-12-01T12:00:00.000Z","temMaisDetalhes":false}
# No console da aplicação:
Data é do tipo Date: true
object
boolean
Testando um cenário onde data
e o valor do campo temMaisDetalhes
são inválidos:
curl -i -X POST \
--header 'content-type: application/json' \
--data '{ "data": "20-12-01T12:00:00.000Z", "temMaisDetalhes": "000" }' \
localhost:3000/exemplo-conversao-dados
# Resultado:
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{"message":["data must be a Date instance","temMaisDetalhes must be a boolean value"],"error":"Bad Request","statusCode":400}
Validando subclasses e array
Um outro caso muito comum é quando temos subconjunto de dados e/ou arrays. Por exemplo, ao considerar os dados de um cliente que pode ter múltiplos endereços, podemos usar a seguinte configuração para garantir que todos os dados sejam validados corretamente:
import { Type } from 'class-transformer';
import {
IsArray,
IsNumber,
IsString,
MinLength,
ValidateNested,
} from 'class-validator';
export class Endereco {
@IsString()
@MinLength(3)
logradouro: string;
@IsNumber()
numero: number;
}
export class Cliente {
@IsString()
nome: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => Endereco)
enderecos: Endereco[];
}
// ...app.controller.ts (trecho)
export class AppController {
@Post('exemplo-subclasses')
@UsePipes(new ValidationPipe({ whitelist: true }))
exemploSubclasses(@Body() body: Cliente) {
return body;
}
}
Testando um resultado de sucesso:
curl -i -X POST \
--header 'content-type: application/json' \
--data '{ "nome": "Cliente 123", "enderecos": [{ "logradouro": "aaaaa", "numero": 123 }, { "logradouro": "bbbb", "numero": 444 }] }' \
localhost:3000/exemplo-subclasses
# Resultado:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
{"nome":"Cliente 123","enderecos":[{"logradouro":"aaaaa","numero":123},{"logradouro":"bbbb","numero":444}]}%
Testando um cenário com erro:
curl -i -X POST \
--header 'content-type: application/json' \
--data '{ "nome": "Cliente 123", "enderecos": [{ "logradouro": "", "numero": 123 }, { "logradouro": "bbbb", "numero": "a" }] }' \
localhost:3000/exemplo-subclasses
# Resultado:
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{"message":["enderecos.0.logradouro must be longer than or equal to 3 characters","enderecos.1.numero must be a number conforming to the specified constraints"],"error":"Bad Request","statusCode":400}%
Como validar diferentes classes dependendo de uma informação específica
Em outros cenários, pode ser necessário validar mais de um modelo. Por exemplo, imagine que a API foi projetada para receber dados de diferentes tipos de veículos. Dependendo do tipo de veículo (caminhão, carro ou moto), será preciso validar propriedades específicas para cada caso. Dessa forma, é possível mapear as particularidades de cada tipo de veículo em uma classe específica, com suas próprias regras de validação, e utilizar outra classe como um container. Essa classe (container) conteria um campo responsável por identificar o tipo de veículo, permitindo a seleção e aplicação da classe adequada para realizar o mapeamento, veja abaixo:
import { Type } from 'class-transformer';
import {
IsBoolean,
IsEnum,
IsNotEmptyObject,
IsNumber,
ValidateNested,
} from 'class-validator';
export class CaminhaoDto {
@IsNumber()
cargaMaxima: number;
}
export class CarroDto {
@IsNumber()
quantidadeMaxPassageiros: number;
}
export class MotoDto {
@IsBoolean()
temCompartimentoEmbaixoDoBanco: boolean;
}
export enum VeiculoTipo {
carro = 'carro',
moto = 'moto',
caminhao = 'caminhao',
}
export class VeiculoDto {
@IsNumber()
potencia: number;
@IsEnum(VeiculoTipo)
tipo: string;
@IsNotEmptyObject()
@ValidateNested()
@Type((opts) => {
switch (opts.object.tipo) {
case VeiculoTipo.caminhao:
return CaminhaoDto;
case VeiculoTipo.carro:
return CarroDto;
case VeiculoTipo.moto:
default:
return MotoDto;
}
})
detalhes: MotoDto | CarroDto | CaminhaoDto;
}
Observe na configuração acima que, dependendo do valor do valor do campo tipo
, a propriedade detalhes
é associada a uma classe específica. Assim, é possível enviar o seguinte na requisição:
curl -i -X POST \
--header 'content-type: application/json' \
--data '{ "tipo": "carro", "potencia": 100, "detalhes": { "quantidadeMaxPassageiros": 5 } }' \
localhost:3000/exemplo-classes-validacao
# Resultado:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
{"tipo":"carro","potencia":100,"detalhes":{"quantidadeMaxPassageiros":5}}
# ######
curl -i -X POST \
--header 'content-type: application/json' \
--data '{ "tipo": "moto", "potencia": 100, "detalhes": { "temCompartimentoEmbaixoDoBanco": true } }' \
localhost:3000/exemplo-classes-validacao
# Resultado:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
{"tipo":"moto","potencia":100,"detalhes":{"temCompartimentoEmbaixoDoBanco":true}}
# ######
curl -i -X POST \
--header 'content-type: application/json' \
--data '{ "tipo": "caminhao", "potencia": 100, "detalhes": { "temCompartimentoEmbaixoDoBanco": true } }' \
localhost:3000/exemplo-classes-validacao
# Resultado:
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{"message":["detalhes.cargaMaxima must be a number conforming to the specified constraints"],"error":"Bad Request","statusCode":400}
Teste unitários com Jest
A criação de testes unitários para verificar se as configurações de validação estão corretas é relativamente simples. Veja os exemplos a seguir:
class-validator
e class-transformer
. Durante o desenvolvimento, frequentemente identifiquei erros em cenários não previstos inicialmente, graças aos testes unitários. Por isso, para garantir um código mais consistente, minha recomendação é sempre implementar testes unitários específicos para essas classes.// *****************************
# Classe que está sendo testada:
// *****************************
import { IsBoolean, IsEmail, IsString } from "class-validator";
export class NovoUsuarioDto {
@IsString()
nome: string;
@IsEmail()
email: string;
@IsBoolean()
desejaReceberEmails: boolean;
}
// **************************
// Testes unitários com Jest:
// **************************
import { plainToClass } from 'class-transformer';
import { NovoUsuarioDto } from './novo-usuario.dto';
import { validateSync } from 'class-validator';
describe('NovoUsuarioDto', () => {
it(`deve lançar erro quando o campo "nome"
não for informado`, () => {
const dadosPostados = {
email: '[email protected]',
desejaReceberEmails: true,
};
const dto = plainToClass(NovoUsuarioDto, dadosPostados);
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
});
it(`deve lançar erro quando o campo "desejaReceberEmails"
não for informado`, () => {
const dadosPostados = {
nome: 'Teste',
email: '[email protected]',
};
const dto = plainToClass(NovoUsuarioDto, dadosPostados);
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
});
it(`deve lançar erro quando o campo "desejaReceberEmails"
não for boolean`, () => {
const dadosPostados = {
nome: 'teste',
email: '[email protected]',
desejaReceberEmails: 'true',
};
const dto = plainToClass(NovoUsuarioDto, dadosPostados);
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(
errors.some(
(a) => a.property === 'desejaReceberEmails',
),
).toBeTruthy();
});
it(`deve lançar 3 erros quando nenhum campo
obrigatório for informado`, () => {
const dadosPostados = {};
const dto = plainToClass(NovoUsuarioDto, dadosPostados);
const errors = validateSync(dto);
expect(errors).toHaveLength(3);
const nomeDosCamposQueDeramErro = errors.map(
(a) => a.property,
);
expect(nomeDosCamposQueDeramErro).toContain('nome');
expect(nomeDosCamposQueDeramErro).toContain('email');
expect(nomeDosCamposQueDeramErro).toContain(
'desejaReceberEmails',
);
});
it(`deve remover campos não mapeados quando
whitelist for true`, () => {
const dadosPostados = {
nome: 'teste',
email: '[email protected]',
desejaReceberEmails: true,
campoExtra1: 'teste1',
campoExtra2: 'teste2',
};
const dto = plainToClass(NovoUsuarioDto, dadosPostados);
const errors = validateSync(dto, { whitelist: true });
expect(errors).toHaveLength(0);
expect(dto).toHaveProperty('nome');
expect(dto).toHaveProperty('email');
expect(dto).toHaveProperty('desejaReceberEmails');
expect(dto).not.toHaveProperty('campoExtra1');
expect(dto).not.toHaveProperty('campoExtra2');
});
});
npm run test:watch
enquanto escreve os testes unitários. Este comando inicia um processo de execução de testes que monitora continuamente as alterações no código e executa os testes automaticamente sempre que uma modificação é detectada.Considerações
Neste texto, abordamos exemplos básicos envolvendo validação de dados com os pacotes class-validator
e class-transformer
. Apesar de o projeto de estudo utilizar NestJS, boa parte do conteúdo pode ser reaproveitada em outros projetos que não utilizem necessariamente NestJS. Para aprofundar os estudos, sugiro dar uma olhada na documentação oficial: