DTO e validação no NestJS com ValidationPipe
Valide o body de uma API NestJS com DTO, class-validator, transformação e whitelist, reproduzindo respostas 400 antes da regra de negócio.
Um DTO com ValidationPipe impede que dados quebrados cheguem à regra da sua
API NestJS. Nesta lição, o POST de mentorias vai aceitar um título válido e de 1
a 20 vagas, converter um número recebido como texto e recusar campos extras com
uma resposta 400 que você consegue conferir.
Use o MentoriasModule criado na lição de
módulos, controllers e providers.
O exemplo ainda guarda dados em memória para que a validação seja a única peça
nova.
A especificação e a inspeção antes da linha de produção
Imagine uma pequena fábrica que recebe pedidos de camisetas. A ficha diz que o modelo precisa ter um nome com pelo menos cinco letras e uma quantidade entre 1 e 20. Antes de a produção começar, a inspeção confere a ficha, converte uma quantidade escrita num formato aceito e barra um campo desconhecido.
No mapa técnico, a classe DTO é a especificação da ficha. Os decorators como
@Min(1) são os critérios. O ValidationPipe é a inspeção executada no valor
que chegou pela rede. class-transformer faz conversões declaradas e
class-validator produz as violações.
A analogia não torna o DTO a regra inteira da fábrica. Confirmar que existem instrutores disponíveis ou que o horário não está ocupado exige consultar estado externo; isso pertence ao service. O DTO protege formato, presença e limites locais na fronteira HTTP.
O tipo do TypeScript não inspeciona JSON
É tentador escrever apenas isto no controller:
criar(@Body() body: { titulo: string; vagas: number }) {
return this.mentoriasService.criar(body.titulo, body.vagas);
}Essa anotação ajuda o editor e o compilador a verificar seu próprio código. O
cliente da API não passa pelo compilador. Ele pode mandar "vagas":"oito",
titulo: null ou admin: true; em runtime, body continua sendo o objeto
produzido a partir do JSON.
DTO no Nest costuma ser uma classe, e não uma interface, porque a classe existe durante a execução. Decorators conseguem anexar metadados a ela; interfaces são apagadas pelo TypeScript.
Instale validação e transformação
Na raiz do projeto, fixe as versões usadas no teste:
npm install class-validator@0.15.1 class-transformer@0.5.1
npm ls --depth=0 class-validator class-transformerO desenho completo da árvore pode incluir as mesmas dependências sob
@nestjs/common. O que precisa ser verificável é a presença dessas versões no
package-lock.json.
Crie um DTO para a entrada de mentoria
Crie src/mentorias/dto/create-mentoria.dto.ts:
import { Type } from 'class-transformer';
import { IsInt, IsString, Max, Min, MinLength } from 'class-validator';
export class CreateMentoriaDto {
@IsString()
@MinLength(5)
titulo!: string;
@Type(() => Number)
@IsInt()
@Min(1)
@Max(20)
vagas!: number;
}@IsString() verifica o tipo real. @MinLength(5) confere o tamanho do título.
@IsInt(), @Min e @Max definem o intervalo das vagas. O ! depois do nome é
o operador de atribuição definida do TypeScript; ele informa ao compilador que
o framework criará e preencherá a instância.
@Type(() => Number) diz ao transformador como converter vagas. Sem uma
conversão explícita, o JSON "8" é string, mesmo que visualmente pareça número.
Ative um pipe global na inicialização
Edite src/main.ts:
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();Global significa que o mesmo contrato básico vale em todos os controllers.
transform: true cria a instância e aplica transformações. whitelist: true
identifica propriedades que não têm decorators. Com
forbidNonWhitelisted: true, a API as rejeita em vez de apenas apagá-las.
Essa combinação é uma decisão explícita: campos desconhecidos podem revelar um cliente desatualizado ou uma tentativa de enviar dados que a rota não deveria aceitar.
Troque o tipo improvisado pelo DTO
Atualize o controller:
import { Body, Controller, Get, Post } from '@nestjs/common';
import { CreateMentoriaDto } from './dto/create-mentoria.dto';
import { MentoriasService } from './mentorias.service';
@Controller('mentorias')
export class MentoriasController {
constructor(private readonly mentoriasService: MentoriasService) {}
@Get()
listar() {
return this.mentoriasService.listar();
}
@Post()
criar(@Body() dto: CreateMentoriaDto) {
return this.mentoriasService.criar(dto.titulo, dto.vagas);
}
}O controller continua traduzindo HTTP e delegando. A novidade é que dto só
chega ao método depois do pipe global. O retorno da validação acontece antes da
chamada a MentoriasService.
Reproduza três problemas numa requisição
Compile, inicie a aplicação e envie título curto, zero vagas e um campo admin
que não existe no DTO:
npm run build
npm run start
curl -s -X POST http://localhost:3000/mentorias \
-H 'Content-Type: application/json' \
-d '{"titulo":"API","vagas":0,"admin":true}'Essa é a resposta real do teste com NestJS 11.2.1. O status 400 significa que a
requisição não satisfaz o contrato de entrada. A lista segue a ordem das
propriedades e decorators encontrados; seu cliente não deve depender da posição
de cada frase, mas pode usar statusCode e message para apresentar o erro.
Remover apenas admin ainda deixa dois problemas. Alterar apenas o título ainda
deixa vagas fora do intervalo. A resposta reúne as violações encontradas para
que o cliente corrija a ficha inteira.
Veja a transformação de string para número
Agora mande uma entrada válida, mas escreva vagas como string:
curl -i -X POST http://localhost:3000/mentorias \
-H 'Content-Type: application/json' \
-d '{"titulo":"APIs com contrato","vagas":"8"}'{“id”:2,“titulo”:“APIs com contrato”,“vagas”:8}
O resultado volta com 8 sem aspas. @Type(() => Number) transformou a entrada
antes de @IsInt() verificar o inteiro. Essa conversão é útil para formulários,
mas precisa ser deliberada: Number('') resulta em zero, e seus limites devem
decidir se isso faz sentido.
Compare whitelist silenciosa e rejeição explícita
Se você mantiver whitelist: true e desativar
forbidNonWhitelisted, um campo desconhecido é removido:
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: false,
transform: true,
});Com a configuração usada na aula, o mesmo campo gera 400:
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
});Eu prefiro rejeição explícita em APIs internas e contratos controlados, porque ela torna incompatibilidade visível. Em endpoints públicos que precisam tolerar clientes de várias gerações, descartar campos pode ser uma decisão válida. O importante é escolher, testar e documentar o comportamento.
Mensagens personalizadas sem esconder a regra
Você pode escrever mensagens em português quando elas serão mostradas ao usuário:
export class CreateMentoriaDto {
@IsString({ message: 'o título precisa ser um texto' })
@MinLength(5, { message: 'o título precisa ter ao menos 5 caracteres' })
titulo!: string;
@Type(() => Number)
@IsInt({ message: 'as vagas precisam formar um número inteiro' })
@Min(1, { message: 'a mentoria precisa ter ao menos 1 vaga' })
@Max(20, { message: 'a mentoria aceita no máximo 20 vagas' })
vagas!: number;
}Não coloque senha, token ou corpo completo nos logs de erro. Mensagem de validação deve explicar como corrigir o campo, sem devolver dado sensível nem detalhe interno do servidor.
Formato não substitui regra de negócio
O DTO consegue provar que vagas é inteiro entre 1 e 20. Ele não consegue
provar sozinho que uma sala tem vinte cadeiras disponíveis naquele horário. Essa
segunda decisão precisa de dados atuais e pertence ao service ou caso de uso.
Uma regra prática: se a validação depende apenas do valor recebido, considere o DTO. Se depende de banco, identidade, tempo ou outra entidade, deixe a decisão na camada de aplicação. A lição anterior de validação de entrada em API aprofunda essa separação fora do framework.
Missão: valide uma data e comprove os dois caminhos
Adicione data: string ao DTO com @IsISO8601() e uma mensagem em português.
Envie primeiro "data":"amanhã"; depois envie
"data":"2026-09-10T19:00:00.000Z".
curl -X POST http://localhost:3000/mentorias \
-H 'Content-Type: application/json' \
-d '{"titulo":"Nest na prática","vagas":10,"data":"amanhã"}'
curl -X POST http://localhost:3000/mentorias \
-H 'Content-Type: application/json' \
-d '{"titulo":"Nest na prática","vagas":10,"data":"2026-09-10T19:00:00.000Z"}'O critério de sucesso é verificável: a primeira requisição responde 400 com a
mensagem da data; a segunda responde 201; retirar data também responde 400.
Depois disso, o contrato externo está protegido e você pode cuidar de quem tem
permissão para criar mentorias em
guards e autenticação. O
guia de NestJS mantém o mapa das camadas disponível.
Perguntas frequentes
O que é DTO no NestJS?
TypeScript já não valida o body?
Qual a diferença entre whitelist e forbidNonWhitelisted?
DTO deve validar regra de negócio?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, NestJS 11.2.1, TypeScript 5.9.3, class-validator 0.15.1 e class-transformer 0.5.1, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- NestJS — validação — docs.nestjs.com
- NestJS — pipes — docs.nestjs.com
- class-validator — repositório oficial — github.com
- class-transformer — repositório oficial — github.com


