📚 Módulo 04: Backend - Pedidos e Transações ACID no Prisma

Uma API de e-commerce requer precisão absoluta. Se um cliente tentar finalizar a compra de 3 itens, e o segundo item não tiver estoque suficiente, nenhuma das operações pode ser gravada no banco.

No Prisma, gerenciamos o princípio ACID (Atomicidade, Consistência, Isolamento e Durabilidade) através do comando $transaction.


🔄 1. O Diagrama da Transação Atômica

Observe como o ciclo de vida exige um “Rollback” imediato caso qualquer operação da lista falhe.

sequenceDiagram
    participant Ctrl as PedidoController
    participant Svc as PedidoService
    participant Prisma as Prisma $transaction
    participant DB as PostgreSQL

    Ctrl->>Svc: checkout(dto)
    Svc->>Prisma: Inicia Transação []
    
    loop Verificando Itens
        Svc->>Prisma: Buscar estoque do Produto
        alt Estoque Insuficiente
            Svc-->>Ctrl: Lança BadRequestException
            Note over Prisma,DB: Transação Cancelada (Rollback Implícito)
        else Estoque Suficiente
            Svc->>Prisma: Prepara UPDATE (Deduz Estoque)
        end
    end
    
    Svc->>Prisma: Prepara INSERT Pedido / Itens
    Prisma->>DB: Executa Array de Queries em Bloco
    DB-->>Prisma: Commit
    Prisma-->>Svc: Sucesso
    Svc-->>Ctrl: Pedido Gerado

📦 2. Criando o Módulo de Checkout

Gere os arquivos usando a CLI:

npx nest g resource pedido --no-spec

Os DTOs de Validação

Crie src/pedido/dto/checkout.dto.ts que aninha validações avançadas (o pedido exige um array de itens dentro dele):

import { ArrayMinSize, IsArray, IsNumber, Min, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';

export class ItemPedidoDto {
  @IsNumber()
  produtoId: number;

  @IsNumber()
  @Min(1)
  quantidade: number;
}

export class CheckoutDto {
  @IsNumber()
  usuarioId: number;

  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => ItemPedidoDto)
  @ArrayMinSize(1, { message: 'O carrinho deve conter pelo menos 1 item' })
  itens: ItemPedidoDto[];
}

⚡ 3. Serviço Transacional (pedido.service.ts)

A implementação abaixo utiliza o modo “Interactive Transaction” do Prisma. Observe como injetamos tx (A transação em si) nas requisições.

import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CheckoutDto } from './dto/checkout.dto';

@Injectable()
export class PedidoService {
  constructor(private prisma: PrismaService) {}

  async checkout(dto: CheckoutDto) {
    // Valida se usuário existe
    const usuario = await this.prisma.usuario.findUnique({ where: { id: dto.usuarioId } });
    if (!usuario) throw new NotFoundException('Usuário não encontrado');

    // Inicia a transação atômica do Prisma
    return this.prisma.$transaction(async (tx) => {
      let valorTotalPedido = 0;
      const itensCriar = [];

      // Loop pelos itens do carrinho
      for (const item of dto.itens) {
        // Usa o objeto transacional 'tx' para ler o banco
        const produto = await tx.produto.findUnique({ where: { id: item.produtoId } });
        
        if (!produto) {
          throw new NotFoundException(`Produto ID ${item.produtoId} inexistente`);
        }
        if (produto.estoque < item.quantidade) {
          throw new BadRequestException(`Estoque insuficiente para: ${produto.nome}. Disponível: ${produto.estoque}`);
        }

        // Calcula total parcial
        const subtotal = Number(produto.preco) * item.quantidade;
        valorTotalPedido += subtotal;

        // Deduz estoque fisicamente
        await tx.produto.update({
          where: { id: produto.id },
          data: { estoque: produto.estoque - item.quantidade }
        });

        // Prepara os itens para serem inseridos junto com o pedido
        itensCriar.push({
          produtoId: produto.id,
          quantidade: item.quantidade,
          precoUnitario: produto.preco
        });
      }

      // Cria o Pedido final
      const pedido = await tx.pedido.create({
        data: {
          usuarioId: usuario.id,
          valorTotal: valorTotalPedido,
          itens: {
            create: itensCriar
          }
        },
        include: { itens: true }
      });

      return pedido;
    });
  }

  async findByUsuario(usuarioId: number) {
    return this.prisma.pedido.findMany({
      where: { usuarioId },
      include: {
        itens: { include: { produto: true } } // Join cascata para pegar nome do produto
      },
      orderBy: { dataCriacao: 'desc' }
    });
  }
}

Controlador de Pedidos (pedido.controller.ts)

import { Controller, Post, Body, Get, Param } from '@nestjs/common';
import { PedidoService } from './pedido.service';
import { CheckoutDto } from './dto/checkout.dto';

@Controller('pedidos')
export class PedidoController {
  constructor(private readonly pedidoService: PedidoService) {}

  @Post('checkout')
  checkout(@Body() checkoutDto: CheckoutDto) {
    return this.pedidoService.checkout(checkoutDto);
  }

  @Get('usuario/:id')
  findByUsuario(@Param('id') usuarioId: string) {
    return this.pedidoService.findByUsuario(+usuarioId);
  }
}

✅ Pré-Requisitos deste Módulo

Antes de passar para a barreira de segurança, geração de tokens e controllers do Módulo 05, certifique-se de que:


🤔 Por que fizemos assim?


🔍 Checkpoint

  1. Validador Aninhado: Verifique se as dependências de validação em checkout.dto.ts estão configuradas corretamente com os decorators de tipo.
  2. Transação Lógica: Certifique-se de que todas as manipulações no banco dentro de $transaction usam estritamente o client local de escopo tx.
  3. Rollback Verificado: Tente disparar uma compra enviando um produto válido em conjunto com outro que possua estoque físico nulo. A transação inteira deve falhar com status 400 Bad Request e nenhum estoque do produto válido pode ser deduzido no banco.

⚠️ Erros Comuns

Erro Causa Solução
Condição de corrida (Race Condition) sobrescrevendo estoques sob alta concorrência de compras O novo estoque foi calculado na memória do NestJS e gravado de forma estática no banco. Substitua o cálculo matemático manual pelo operador de atualização atômica do Prisma: data: { estoque: { decrement: item.quantidade } }, fazendo a subtração no banco de dados.
Ocorre erro de tipo no compilador apontando que item possui tipagem any Ausência de importação correta da biblioteca class-transformer nas dependências do DTO. Garanta que @Type(() => ItemPedidoDto) está declarado e importado do local correto para permitir a validação recursiva das propriedades.
O banco de dados trava por excesso de conexões abertas (Pool Exhausted) A transação interativa não foi fechada ou a conexão física do Prisma não foi liberada. Evite manter transações interativas abertas por longos períodos de tempo ou com loops demorados; lance exceções rápidas para forçar a liberação do pool pelo Prisma.

🏁 Conclusão

Com o método $transaction, o Prisma blinda nosso e-commerce de falhas técnicas ou inconsistências. Nossa lógica de faturamento está perfeitamente testável e segura.

No Módulo 05, finalizaremos o ciclo do backend do NestJS trancando as rotas da área administrativa da nossa API, exigindo um Token JWT assinado por meio do Passport.


Voltar para o Sumário