📚 Módulo 03: Backend - DTOs, Validações e Filtros de Exceção
Neste módulo, blindaremos a nossa API contra dados maliciosos ou incorretos utilizando o poderoso conceito de Data Transfer Objects (DTOs) em conjunto com o ValidationPipe nativo do NestJS.
🛡️ O Escudo de Validação Global
O fluxo abaixo demonstra como o NestJS intercepta os pacotes JSON maliciosos antes mesmo de eles entrarem no Controlador, protegendo nossa regra de negócio e o banco de dados.
flowchart TD
classDef client fill:#61dafb,stroke:#20232a,stroke-width:2px,color:#000;
classDef pipe fill:#eab308,stroke:#713f12,stroke-width:2px,color:#fff;
classDef nest fill:#ea2845,stroke:#3b0713,stroke-width:2px,color:#fff;
A[Client React SPA]:::client -->|POST JSON| B[Global ValidationPipe]:::pipe
B --> C{class-validator}
C -- Erro Encontrado --> D[Retorna HTTP 400 Bad Request]
C -- Payload Válido --> E[Controller / Service]:::nest
🏛️ 1. O Padrão DTO (Data Transfer Object)
No design de APIs profissionais, nunca devemos expor diretamente a nossa entidade física de banco de dados (o modelo Prisma) para receber dados de uma requisição HTTP. Em vez disso, utilizamos DTOs para descrever exatamente o contrato de dados que a rota espera receber, aplicando validações estritas em tempo de execução.
Instale as dependências de validação no seu terminal:
npm install class-validator class-transformer
Agora, criaremos a estrutura para validar o cadastro de novos produtos. Crie o arquivo DTO em src/produto/dto/criar-produto.dto.ts:
// src/produto/dto/criar-produto.dto.ts
import { IsNotEmpty, IsString, IsNumber, Min, MaxLength, IsOptional } from 'class-validator';
export class CriarProdutoDto {
@IsNotEmpty({ message: 'O nome do produto é obrigatório.' })
@IsString({ message: 'O nome deve ser uma string de texto válida.' })
@MaxLength(150, { message: 'O nome não deve ultrapassar 150 caracteres.' })
nome: string;
@IsOptional()
@IsString({ message: 'A descrição deve ser um texto válido.' })
descricao?: string;
@IsNotEmpty({ message: 'O preço do produto é obrigatório.' })
@IsNumber({}, { message: 'O preço deve ser um número decimal válido.' })
@Min(0.01, { message: 'O preço do produto deve ser maior que zero.' })
preco: number;
@IsNotEmpty({ message: 'A quantidade de estoque é obrigatória.' })
@IsNumber({}, { message: 'O estoque deve ser um número inteiro.' })
@Min(0, { message: 'O estoque inicial não pode ser negativo.' })
estoque: number;
@IsNotEmpty({ message: 'O identificador da categoria é obrigatório.' })
@IsNumber({}, { message: 'A categoriaId deve ser um número inteiro.' })
categoriaId: number;
}
⚡ 2. Ativando Validações Globais (ValidationPipe)
No NestJS, para que os decorators do class-validator funcionem de forma automática em todas as requisições recebidas da API, devemos habilitar o componente global ValidationPipe no bootstrap do sistema.
Abra o arquivo principal da aplicação src/main.ts e configure conforme abaixo:
// src/main.ts
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 em nível global
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Remove automaticamente propriedades que não estejam no DTO
forbidNonWhitelisted: true, // Retorna erro 400 Bad Request caso enviem parâmetros não autorizados
transform: true, // Converte automaticamente os dados para os tipos tipados do DTO
}),
);
await app.listen(process.env.PORT || 3000);
console.log(`🚀 API da TecLoja 03 rodando na porta ${process.env.PORT || 3000}`);
}
bootstrap();
🛡️ 3. Filtro Global de Exceções Customizado (HttpExceptionFilter)
Quando ocorre um erro no banco (ex: tentar cadastrar uma Categoria repetida ou excluir uma categoria com produtos associados), o Prisma lança uma exceção nativa. Se não tratarmos essa exceção, a API retornará o clássico erro interno genérico 500 Internal Server Error com mensagens confusas e vazamentos de stack trace.
Para resolver isso de forma elegante, criaremos um Filtro de Exceções Global que interceptará todos os erros HTTP padrão do NestJS e traduzirá as exceções físicas de banco de dados do Prisma em respostas limpas em formato JSON.
Crie o arquivo em src/common/filters/http-exception.filter.ts:
// src/common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';
import { Prisma } from '@prisma/client';
@Catch() // Captura TODAS as exceções disparadas na aplicação
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message: string | string[] = 'Ocorreu um erro inesperado no servidor.';
let errorType = 'InternalServerError';
// 1. Caso a exceção venha de um erro HTTP padrão do NestJS (ex: validação Bad Request, Unauthorized, etc.)
if (exception instanceof HttpException) {
status = exception.getStatus();
const resContent = exception.getResponse() as any;
message = resContent.message || exception.message;
errorType = resContent.error || exception.name;
}
// 2. Caso a exceção venha de um erro conhecido disparado pelo Prisma ORM
else if (exception instanceof Prisma.PrismaClientKnownRequestError) {
errorType = `PrismaDatabaseError_${exception.code}`;
switch (exception.code) {
case 'P2002': // Erro de restrição de chave única (Unique Constraint)
status = HttpStatus.BAD_REQUEST;
const campos = (exception.meta?.target as string[]) || [];
message = `Valor duplicado inserido para o campo: ${campos.join(', ')}.`;
break;
case 'P2025': // Registro não encontrado em relacionamentos ou deleções
status = HttpStatus.NOT_FOUND;
message = 'O registro solicitado para esta operação não foi encontrado no banco de dados.';
break;
case 'P2003': // Violação de chave estrangeira (Foreign Key Constraint)
status = HttpStatus.CONFLICT;
message = 'Operação negada devido a uma violação de chave estrangeira (relacionamentos existentes no banco).';
break;
default:
status = HttpStatus.BAD_REQUEST;
message = `Falha na requisição física de banco de dados: ${exception.message}`;
break;
}
}
// 3. Resposta padronizada da API para o cliente (UX consistente para o Frontend)
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
error: errorType,
message: message,
});
}
}
⚙️ Registrando o Filtro Globalmente
Para habilitar o filtro, precisamos declará-lo no arquivo raiz src/app.module.ts como um provedor global do NestJS:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { PrismaService } from './prisma/prisma.service';
@Module({
imports: [],
controllers: [],
providers: [
PrismaService,
{
provide: APP_FILTER,
useClass: HttpExceptionFilter, // Registra o filtro de exceção global
},
],
})
export class AppModule {}
✅ Pré-Requisitos deste Módulo
Antes de passar para a lógica de serviços de checkout e transações no banco, certifique-se de que:
- O projeto NestJS CLI foi criado e as migrações de banco do Prisma foram rodadas no Módulo 02.
- As dependências
class-validatoreclass-transformerforam adicionadas aopackage.jsondo backend.
🤔 Por que fizemos assim?
- Por que usar DTOs combinados com a propriedade
whitelist: truedoValidationPipe? Expor diretamente o modelo físico de banco (entidade Prisma) no corpo da requisição HTTP expõe a API a falhas de segurança (como injeção de parâmetros indesejados). O DTO estabelece um contrato estrito de entrada. Owhitelist: trueatua limpando o objeto de requisição recebido, descartando de forma automática quaisquer propriedades excedentes que não tenham decorators de validação explícitos no DTO, blindando a integridade dos dados antes mesmo que a lógica alcance a camada de controle. - Por que usar o interceptador global de exceções (
HttpExceptionFilter) mapeando códigos de erro do Prisma? Quando o banco de dados falha por uma restrição física (como cadastrar um e-mail repetido), o Prisma lança uma exceção crua. Se não for tratada, o NestJS retorna um erro genérico500 Internal Server Errorque vaza detalhes técnicos da infraestrutura. O filtro captura essas exceções do Prisma (PrismaClientKnownRequestError) e as traduz em respostas JSON limpas e consistentes com status HTTP adequados (ex:400 Bad Requestpara duplicidade P2002,409 Conflictpara violação de FK P2003), simplificando a tratativa de erros no frontend React. - Por que ativar
transform: truena configuração doValidationPipe? Por padrão, dados vindos de parâmetros de rota (como/produtos/editar/:id) chegam à API como Strings. Habilitar otransformforça o NestJS a realizar a conversão automática desses tipos para as definições tipadas em TypeScript no DTO ou parâmetros de assinatura dos controladores, eliminando a necessidade de chamar funções de coerção imperativas comoparseInt()em todos os métodos da API.
🔍 Checkpoint
- Instalação dos Validadores: Valide se os pacotes
class-validatoreclass-transformerforam listados nas dependências de produção do seu arquivopackage.json. - Intercepção de Falhas Operante: Suba o servidor NestJS e tente cadastrar no banco uma categoria contendo o nome exato de uma já cadastrada. A API deve capturar o código
P2002e responder com o JSON customizado contendostatusCode: 400e a mensagem amigável de valor duplicado. - Filtragem de Atributos: Tente enviar um JSON contendo uma propriedade falsa
hacker: truena rota de criação de produtos. A API deve rejeitar a requisição com código400 Bad Requestindicando que a propriedade não é permitida, graças à whitelist do pipe de validação.
⚠️ Erros Comuns
| Erro | Causa | Solução |
|---|---|---|
As validações declarativas do DTO (como @Min ou @IsNotEmpty) são ignoradas e a API aceita payloads vazios |
O ValidationPipe não foi registrado no arquivo de bootstrap global da aplicação. |
Abra o arquivo src/main.ts e certifique-se de chamar o método global app.useGlobalPipes(new ValidationPipe({...})) com as configurações ativas. |
| O TypeScript reclama que a resposta de exceção do express no filtro não é válida | Ausência de importação explícita do pacote de tipos do Express para as variáveis do request/response. | Certifique-se de instalar as dependências de desenvolvimento @types/express via npm ou de importar os tipos Request e Response diretamente do módulo 'express' no topo do arquivo do filtro. |
| Erros de banco de dados ainda retornam JSONs brutas com stack trace em produção | O HttpExceptionFilter não foi fornecido ao NestJS como provedor global no módulo raiz. |
Garanta que o array de providers em src/app.module.ts contém o objeto de provedor mapeando a chave APP_FILTER para a classe HttpExceptionFilter. |
🏁 Conclusão
Com os DTOs blindados e o Filtro de Exceções capturando qualquer comportamento inesperado do banco, nossa camada de proteção externa está completa.
No Módulo 04, nos aprofundaremos nas regras de negócio. Desenvolveremos a Camada de Serviço de checkout de pedidos do e-commerce, implementando injeção de dependências no NestJS e a API transacional robusta do Prisma (prisma.$transaction) para garantir operações ACID em concorrência, realizando rollbacks imediatos caso ocorra falta de estoque de produtos.