📚 Módulo 03: Backend - DTOs e Validation Pipes
Em uma API madura, você nunca confia nos dados enviados pelo cliente. Se o Frontend envia um formulário para cadastrar um Produto, o NestJS deve interceptar esse JSON, verificar se o preço é positivo e se o nome não está vazio, antes de tentar salvar no banco.
Faremos isso usando a dupla implacável do ecossistema NestJS: class-validator e os Data Transfer Objects (DTOs).
🛡️ 1. O Fluxo de Proteção de DTOs
O diagrama abaixo ilustra como o “ValidationPipe” atua como um escudo entre o mundo externo e a sua regra de negócio.
flowchart TD
A[Cliente / Next.js] -->|POST /produtos JSON| B[Global Validation Pipe]
B --> C{class-validator}
C -- Erro de Validação --> D[Retorna HTTP 400 Bad Request]
C -- Dados Válidos --> E[Controller e Service]
E -->|prisma.produto.create| F[(PostgreSQL)]
Instale as bibliotecas necessárias para as validações automáticas:
npm install class-validator class-transformer
E no src/main.ts, ative o ValidationPipe Globalmente para que toda a API fique protegida:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Ativa a validação automática baseada nos decorators dos DTOs
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Remove campos que não estão no DTO
forbidNonWhitelisted: true // Dá erro se o usuário enviar campos extras
}));
// Habilita o CORS (Permitirá que o Next.js BFF acesse a API)
app.enableCors({
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
});
await app.listen(3000);
}
bootstrap();
📦 2. Criando o Módulo de Produtos
Gere os arquivos usando a CLI:
npx nest g resource produto --no-spec
O Data Transfer Object (DTO)
Abra src/produto/dto/create-produto.dto.ts e anote os campos com as regras de negócios:
import { IsString, IsNotEmpty, IsNumber, Min, IsOptional } from 'class-validator';
export class CreateProdutoDto {
@IsString()
@IsNotEmpty({ message: 'O nome do produto é obrigatório' })
nome: string;
@IsOptional()
@IsString()
descricao?: string;
@IsNumber()
@Min(0.01, { message: 'O preço deve ser maior que zero' })
preco: number;
@IsNumber()
@Min(0, { message: 'O estoque não pode ser negativo' })
estoque: number;
@IsNumber()
categoriaId: number;
}
🏗️ 3. Serviço e Controlador de Produtos
src/produto/produto.service.ts
Implementaremos as regras de salvamento e listagem:
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateProdutoDto } from './dto/create-produto.dto';
@Injectable()
export class ProdutoService {
constructor(private prisma: PrismaService) {}
async create(dto: CreateProdutoDto) {
// Validar se a categoria existe antes de criar o produto
const categoriaExiste = await this.prisma.categoria.findUnique({
where: { id: dto.categoriaId }
});
if (!categoriaExiste) {
throw new NotFoundException('Categoria informada não existe');
}
return this.prisma.produto.create({
data: dto
});
}
async findAll() {
return this.prisma.produto.findMany({
include: {
categoria: true // Faz o equivalente a um JOIN na Categoria
}
});
}
async findOne(id: number) {
const produto = await this.prisma.produto.findUnique({
where: { id },
include: { categoria: true }
});
if (!produto) throw new NotFoundException('Produto não encontrado');
return produto;
}
}
src/produto/produto.controller.ts
O Controlador continua fino, apenas roteando o tráfego HTTP.
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { ProdutoService } from './produto.service';
import { CreateProdutoDto } from './dto/create-produto.dto';
@Controller('produtos')
export class ProdutoController {
constructor(private readonly produtoService: ProdutoService) {}
@Post()
create(@Body() createProdutoDto: CreateProdutoDto) {
return this.produtoService.create(createProdutoDto);
}
@Get()
findAll() {
return this.produtoService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.produtoService.findOne(+id);
}
}
✅ Pré-Requisitos deste Módulo
Antes de passar para o desenvolvimento de faturamento de pedidos transacionais ACID, certifique-se de que:
- O módulo global do Prisma e as tabelas físicas foram criados no Módulo 02.
- As dependências
class-validatoreclass-transformerforam adicionadas aopackage.jsondo backend.
🤔 Por que fizemos assim?
- Por que usar DTOs anotados com decorators de validação declarativa (
class-validator) em vez de checagens manuais com condicionaisif? A validação manual de dados de entrada em tempo de desenvolvimento polui a lógica dos controladores e serviços. O DTO (Data Transfer Object) centraliza e documenta o contrato de dados esperado de forma estrita. Ao delegar a checagem aoValidationPipe, o NestJS valida de forma centralizada a estrutura, tipos e intervalos de valores (como preço maior que zero) antes da requisição atingir a lógica de negócio da API. - Por que configurar
whitelist: trueeforbidNonWhitelisted: truenoValidationPipeglobal? Essa dupla atua como um escudo de segurança de dados (mitigando a vulnerabilidade de Mass Assignment). A propriedadewhitelistfiltra o payload JSON recebido, eliminando de forma automática quaisquer propriedades extras que não pertençam ao DTO. Complementarmente,forbidNonWhitelistedinterrompe o processamento e retorna erro400 Bad Requestse identificar atributos excedentes e não mapeados, blindando a integridade das persistências físicas no banco. - Por que efetuar a validação relacional da categoria no
ProdutoServiceantes de chamar oprisma.create? Embora a chave estrangeira (categoriaId) possua restrições no nível do banco de dados relacional (que lançaria erro caso o ID não existisse), esperar o banco rejeitar a transação expõe a API a erros internos crus de infraestrutura. Checar explicitamente a existência da categoria pai (findUnique) permite interceptar a ausência e retornar uma mensagem clara em formato JSON com status HTTP404 Not Found, otimizando a tratativa de erros no frontend.
🔍 Checkpoint
- Configuração de Pipe: Valide se o
ValidationPipeglobal está devidamente instanciado com as chaveswhitelisteforbidNonWhitelistedno arquivosrc/main.ts. - Teste de Erro de Validação: Dispare uma requisição do tipo POST para criar um produto com preço igual a zero. Confirme se a API retorna o status 400 Bad Request contendo o vetor descritivo detalhando o erro encontrado.
- Filtro de Atributos extras: Tente enviar um JSON contendo o parâmetro espúrio
papel: 'ADMIN'no cadastro de produtos. O pipeline de validação deve barrar a requisição e sinalizar a propriedade não permitida.
⚠️ Erros Comuns
| Erro | Causa | Solução |
|---|---|---|
Os decorators de validação declarados no DTO (como @IsNumber) são sumariamente ignorados pela API |
O componente global de validação do NestJS (ValidationPipe) não foi registrado no arquivo de bootstrap principal. |
Abra o arquivo src/main.ts e certifique-se de chamar o método global app.useGlobalPipes(new ValidationPipe({...})). |
Exceção 422 Unprocessable Entity ou falha de tipo ao enviar IDs válidos no JSON |
O Next.js enviou os IDs como string e a API falhou ao tentar forçar a validação estrita de campos que esperavam inteiros. | Ative a propriedade transform: true dentro da chamada do construtor de ValidationPipe no arquivo src/main.ts para realizar a coerção automática de tipos primitivos. |
| O banco lança erro interno 500 no console ao tentar deletar uma categoria com produtos associados | Falta de tratamento e mapeamento de integridade referencial nas rotas de deleção na camada de serviço. | Certifique-se de que o banco de dados NestJS implementa Filtros de Exceção globais para capturar falhas de chaves estrangeiras do Prisma e retornar status HTTP amigáveis de conflito de dados (como 409 Conflict). |
🏁 Conclusão
Qualquer tentativa de criar um produto com estoque negativo resultará em um Erro HTTP 400 formatado perfeitamente pelo NestJS. No Módulo 04, enfrentaremos o maior desafio do backend: faturar um pedido e garantir transações atômicas com o Prisma Client.