Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

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.

Rodolfo Mori5 min de leitura

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:

ts
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:

bash
npm install class-validator@0.15.1 class-transformer@0.5.1
npm ls --depth=0 class-validator class-transformer
agenda-mentorias@0.0.1 ├── class-transformer@0.5.1 └── class-validator@0.15.1

O 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:

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:

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:

ts
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:

bash
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}'
{"message":["property admin should not exist","titulo must be longer than or equal to 5 characters","vagas must not be less than 1"],"error":"Bad Request","statusCode":400}

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:

bash
curl -i -X POST http://localhost:3000/mentorias \
  -H 'Content-Type: application/json' \
  -d '{"titulo":"APIs com contrato","vagas":"8"}'
HTTP/1.1 201 Created Content-Type: application/json; charset=utf-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:

ts
new ValidationPipe({
  whitelist: true,
  forbidNonWhitelisted: false,
  transform: true,
});

Com a configuração usada na aula, o mesmo campo gera 400:

ts
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:

ts
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".

bash
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.

  • nestjs
  • dto
  • validation pipe
  • class validator
  • api rest
  • typescript

Perguntas frequentes

O que é DTO no NestJS?
DTO significa Data Transfer Object. É uma classe que descreve os campos transportados pela fronteira da aplicação e pode carregar metadados de validação para o ValidationPipe.
TypeScript já não valida o body?
Não em runtime. Os tipos do TypeScript somem na compilação, enquanto o JSON chega pela rede durante a execução. O pipe precisa verificar esse valor real.
Qual a diferença entre whitelist e forbidNonWhitelisted?
whitelist remove propriedades sem decorator de validação. Quando forbidNonWhitelisted também está ativo, a API recusa a requisição em vez de remover silenciosamente essas propriedades.
DTO deve validar regra de negócio?
O DTO é adequado para formato e limites locais. Regras que dependem de banco, usuário, horário ou outro agregado devem ficar no caso de uso ou service, onde as dependências e a decisão ficam explícitas.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. NestJS — validação — docs.nestjs.com
  2. NestJS — pipes — docs.nestjs.com
  3. class-validator — repositório oficial — github.com
  4. class-transformer — repositório oficial — github.com

Continue por aqui