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


🤔 Por que fizemos assim?


🔍 Checkpoint

  1. Instalação dos Validadores: Valide se os pacotes class-validator e class-transformer foram listados nas dependências de produção do seu arquivo package.json.
  2. 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 P2002 e responder com o JSON customizado contendo statusCode: 400 e a mensagem amigável de valor duplicado.
  3. Filtragem de Atributos: Tente enviar um JSON contendo uma propriedade falsa hacker: true na rota de criação de produtos. A API deve rejeitar a requisição com código 400 Bad Request indicando 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.


Voltar para o Sumário