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

  1. Devemos ler o estoque atual de cada produto selecionado no carrinho.
  2. Se houver estoque insuficiente para algum item, a compra inteira deve ser cancelada (Atomicidade - Tudo ou Nada).
  3. Se houver estoque, devemos decrementar a quantidade correspondente de cada produto.
  4. Devemos registrar a compra na tabela Pedido.
  5. Devemos registrar cada item comprado na tabela associativa ItemPedido contendo 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:


🤔 Por que fizemos assim?


🔍 Checkpoint

  1. D.I. Operante: Valide se o PedidoService foi registrado no array de providers em src/pedido/pedido.module.ts e se o construtor da classe recebe corretamente a injeção do PrismaService.
  2. Isolamento Transacional: Verifique detalhadamente se todas as operações internas dentro de $transaction utilizam o objeto contextual da transação (tx.produto.update, tx.pedido.create) em vez do this.prisma padrão da classe.
  3. 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).


Voltar para o Sumário