📚 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:
- Os validadores globais de DTOs e whitelists foram ativados no bootstrap do arquivo
src/main.tsno Módulo 03. - A classe do banco global
PrismaServiceestá importada e pronta para ser injetada no controlador de pedidos.
🤔 Por que fizemos assim?
- Por que processar as consultas de estoque e os updates de tabelas dentro de um bloco
$transactioninterativo? O faturamento exige atomicidade absoluta. Uma compra envolve a verificação e alteração física de tabelas de múltiplos produtos e a gravação de cabeçalhos e detalhes. Se um único item do carrinho do usuário falhar por estoque indisponível, a transação interativa do Prisma desfaz de forma atômica (Rollback) todas as consultas, deduções e inserts efetuados anteriormente, impedindo a anomalia grave de debitar estoque de produtos sem a emissão física correspondente do Pedido. - Por que usar a variável local do callback
txno lugar da chamada clássica dethis.prismadentro da transação? O$transactionprovisiona uma conexão exclusiva e segura temporária no PostgreSQL. Executar leituras e escritas usando a referênciatxgarante que as operações participem do escopo transacional unificado. Chamarthis.prismarodará a query fora do bloco de controle, inviabilizando a reversão em cascata (rollback) caso ocorra alguma falha na transação. - Por que decorar o array de itens do DTO com a dupla
@ValidateNestede@Type? Por padrão, os validadores não possuem inteligência recursiva para inspecionar propriedades de elementos contidos em vetores complexos do JavaScript. Mapear@ValidateNested({ each: true })instrui o NestJS a descer a árvore de validação, enquanto@Type(() => ItemPedidoDto)doclass-transformerindica para qual classe o conversor deve traduzir e validar os objetos aninhados no JSON.
🔍 Checkpoint
- Validador Aninhado: Verifique se as dependências de validação em
checkout.dto.tsestão configuradas corretamente com os decorators de tipo. - Transação Lógica: Certifique-se de que todas as manipulações no banco dentro de
$transactionusam estritamente o client local de escopotx. - 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 Requeste 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.