📚 Módulo 04: Backend - Serviços, Injeção de Dependências e Transações ACID
Neste módulo, aprenderemos a separar a lógica de negócios da nossa API REST criando a Camada de Serviço (Service Layer). Neste módulo, aprofundaremos nossos conhecimentos sobre a Injeção de Dependências no NestJS e lidaremos com a lógica mais sensível de um e-commerce: As Transações Atômicas (ACID) do Checkout.
🔄 Transação Atômica ACID
A operação de venda exige precisão. Se o cliente tentar comprar 3 itens e faltar estoque para o último, toda a operação deve ser cancelada (Rollback). Usaremos o $transaction do Prisma para garantir isso.
sequenceDiagram
participant Ctrl as PedidoController
participant Svc as PedidoService
participant Tx as Prisma $transaction
participant DB as PostgreSQL
Ctrl->>Svc: checkout(dto)
Svc->>Tx: Inicia Bloco Atômico [ ]
loop Validando Carrinho
Svc->>Tx: Consulta Estoque Atual
alt Estoque Zerado
Svc-->>Ctrl: Lança Exceção HTTP 400
Note over Tx,DB: Transação Cancelada Imediatamente (Rollback)
else Possui Estoque
Svc->>Tx: Prepara UPDATE (Deduz Estoque)
end
end
Svc->>Tx: Prepara INSERT (Pedido e Itens)
Tx->>DB: Executa Bloco Inteiro de SQLs
DB-->>Tx: Commit Realizado
Tx-->>Svc: Sucesso Atômico
🏛️ 1. Injeção de Dependências no NestJS
A Inversão de Controle (IoC) e a Injeção de Dependências (D.I.) são padrões de projeto fundamentais na engenharia de software para promover baixo acoplamento e alta testabilidade.
No NestJS, qualquer classe anotada com o decorador @Injectable() torna-se um provedor gerenciado pelo container do framework:
// Exemplo: O PrismaService é um provedor gerenciado de conexão
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient {}
Para injetar este serviço em outra classe, basta declará-lo no construtor. O NestJS localiza a instância automaticamente em tempo de execução:
@Injectable()
export class ProdutoService {
constructor(private readonly prisma: PrismaService) {} // D.I. automática pelo construtor
}
💳 2. A Operação Transacional de Checkout
A compra em um e-commerce é uma transação financeira clássica que exige garantia ACID (Atomicidade, Consistência, Isolamento e Durabilidade):
- Devemos ler o estoque atual de cada produto selecionado no carrinho.
- Se houver estoque insuficiente para algum item, a compra inteira deve ser cancelada (Atomicidade - Tudo ou Nada).
- Se houver estoque, devemos decrementar a quantidade correspondente de cada produto.
- Devemos registrar a compra na tabela
Pedido. - Devemos registrar cada item comprado na tabela associativa
ItemPedidocontendo o preço praticado no instante histórico da venda.
Implementando a Lógica em TypeScript
Criaremos o arquivo de contrato DTO de entrada do checkout em src/pedido/dto/checkout.dto.ts:
// src/pedido/dto/checkout.dto.ts
import { IsNotEmpty, IsNumber, IsArray, ValidateNested, ArrayMinSize } from 'class-validator';
import { Type } from 'class-transformer';
export class ItemCarrinhoDto {
@IsNotEmpty()
@IsNumber()
produtoId: number;
@IsNotEmpty()
@IsNumber()
quantidade: number;
}
export class CheckoutDto {
@IsNotEmpty()
@IsNumber()
clienteId: number;
@IsArray()
@ArrayMinSize(1, { message: 'O carrinho deve possuir pelo menos um produto.' })
@ValidateNested({ each: true })
@Type(() => ItemCarrinhoDto)
itens: ItemCarrinhoDto[];
}
Agora, programaremos o serviço transacional assíncrono em src/pedido/pedido.service.ts:
// src/pedido/pedido.service.ts
import { Injectable, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CheckoutDto } from './dto/checkout.dto';
@Injectable()
export class PedidoService {
constructor(private readonly prisma: PrismaService) {}
async checkout(dto: CheckoutDto) {
// Iniciamos uma transação ACID interativa com o banco
return await this.prisma.$transaction(async (tx) => {
let valorTotalPedido = 0;
const itensDetalhadosParaCriacao = [];
// 1. Validar e processar cada item do carrinho sequencialmente
for (const item of dto.itens) {
// Consultar produto dentro da transação ativa (Garante isolamento)
const produto = await tx.produto.findUnique({
where: { id: item.produtoId },
});
if (!produto) {
throw new BadRequestException(`Produto com ID ${item.produtoId} não encontrado no catálogo.`);
}
// Verificar disponibilidade de estoque
if (produto.estoque < item.quantidade) {
throw new BadRequestException(
`Estoque insuficiente para o produto "${produto.nome}". Disponível: ${produto.estoque}, Solicitado: ${item.quantidade}.`
);
}
// Decrementar estoque fisicamente no banco
await tx.produto.update({
where: { id: produto.id },
data: {
estoque: {
decrement: item.quantidade, // Ação atômica de decremento
},
},
});
// Calcular preço parcial e acumular no valor total
const precoItem = Number(produto.preco);
valorTotalPedido += precoItem * item.quantidade;
// Armazenar os dados estruturados para criação em lote posterior
itensDetalhadosParaCriacao.push({
produtoId: produto.id,
quantidade: item.quantidade,
precoUnitario: precoItem,
});
}
// 2. Criar o cabeçalho do Pedido no banco
const pedido = await tx.pedido.create({
data: {
clienteId: dto.clienteId,
status: 'APROVADO',
valorTotal: valorTotalPedido,
// 3. Salvar os itens associados de forma atômica (Cascata relacional do Prisma)
itens: {
create: itensDetalhadosParaCriacao.map((item) => ({
produtoId: item.produtoId,
quantidade: item.quantidade,
precoUnitario: item.precoUnitario,
})),
},
},
include: {
itens: {
include: {
produto: true,
},
},
cliente: true,
},
});
return pedido;
});
// Se QUALQUER exceção for disparada dentro do bloco acima, o Prisma cancela e desfaz
// todas as alterações feitas no banco automaticamente (Rollback garantido).
}
}
✅ Pré-Requisitos deste Módulo
Antes de passar para a criação dos controladores de rota e segurança JWT, certifique-se de que:
- Os validadores globais de DTOs e filtros de exceção foram configurados no Módulo 03.
- A classe
PrismaServiceestá devidamente exportada e disponível no escopo de injeção da aplicação.
🤔 Por que fizemos assim?
- Por que utilizar o padrão de Injeção de Dependências (D.I.) com
@Injectable()no NestJS? Desacopla as camadas arquiteturais da aplicação e melhora a testabilidade. Em vez de instanciar classes manualmente com o operadornewdentro dos controladores (o que tornaria o código rígido e difícil de testar), o container IoC do NestJS gerencia e injeta de forma transparente as instâncias necessárias no construtor. Isso permite, por exemplo, substituir facilmente oPrismaServicepor uma versão Mock nos testes de unidade sem alterar uma linha sequer de código de produção. - Por que encapsular o checkout em uma Transação Interativa (
this.prisma.$transaction)? O checkout exige atomicidade absoluta (princípio ACID: tudo ou nada). Se o carrinho do usuário contiver três produtos e o processamento de estoque falhar no terceiro item, todas as operações anteriores (como débito de estoque dos dois primeiros itens) precisam ser canceladas. A transação interativa cria um bloco seguro de banco de dados onde qualquer exceção lançada faz com que o banco execute um Rollback automático das operações, blindando o sistema contra inconsistências físicas. - Por que efetuar a dedução de estoque usando o operador atômico
decrementno Prisma em vez de calcular no código? Em sistemas com alta concorrência de acessos, se dois clientes tentarem comprar o mesmo produto ao mesmo tempo, calcular o estoque na memória da aplicação (ler valor, subtrair no TypeScript e salvar comupdate) gera um bug clássico de condição de corrida (Race Condition). O operadordecrement: item.quantidadedelega a conta diretamente para o motor do banco de dados PostgreSQL executar a subtração de forma atômica no nível da linha da tabela, blindando a integridade física da coluna de estoque.
🔍 Checkpoint
- D.I. Operante: Valide se o
PedidoServicefoi registrado no array deprovidersemsrc/pedido/pedido.module.tse se o construtor da classe recebe corretamente a injeção doPrismaService. - Isolamento Transacional: Verifique detalhadamente se todas as operações internas dentro de
$transactionutilizam o objeto contextual da transação (tx.produto.update,tx.pedido.create) em vez dothis.prismapadrão da classe. - Teste de Rollback: Force um erro de estoque na metade da validação de uma compra com múltiplos itens e certifique-se de que nenhum pedido foi criado e que os estoques dos itens que possuíam estoque suficiente não sofreram nenhuma alteração.
⚠️ Erros Comuns
| Erro | Causa | Solução |
|---|---|---|
| A transação executa com sucesso, mas o estoque não altera ou o pedido não é criado | O desenvolvedor utilizou a instância padrão this.prisma em vez do parâmetro contextual tx gerado pelo callback da transação. |
Certifique-se de substituir todas as referências de this.prisma por tx nos métodos executados dentro do bloco $transaction. |
Erro 422 Unprocessable Entity ou dados aninhados do carrinho chegando como objetos genéricos vazios no controller |
Falta do mapeamento e conversão de classes aninhadas no DTO de checkout para listas do class-validator. | Use sempre o decorator @ValidateNested({ each: true }) em conjunto com @Type(() => ItemCarrinhoDto) importado de class-transformer em campos que sejam arrays de objetos. |
| O estoque dos produtos fica com valores negativos no banco de dados | A API disparou o decremento direto de estoque no banco sem verificar se a quantidade existente atende à demanda da requisição. | Realize sempre a checagem manual de estoque (produto.estoque < item.quantidade) antes de disparar o update do Prisma e lance uma exceção apropriada. |
🏁 Conclusão
Com a nossa camada de serviços e transações finalizada, os dados do banco de dados relacional estão protegidos contra corrupções lógicas sob concorrência intensa.
No Módulo 05, criaremos as rotas lógicas da API. Desenvolveremos a Camada de Rotas (Controllers) do NestJS, ativaremos os cabeçalhos de segurança CORS para permitir requisições assíncronas do React e programaremos a segurança por tokens stateless JWT, definindo restrições de rotas baseadas em papéis (ADMIN e USER).