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


🤔 Por que fizemos assim?


🔍 Checkpoint

  1. Configuração de Pipe: Valide se o ValidationPipe global está devidamente instanciado com as chaves whitelist e forbidNonWhitelisted no arquivo src/main.ts.
  2. 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.
  3. 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.


Voltar para o Sumário