Skip to content

Frompaje/monorep

Repository files navigation

Módulo de Produto — HexaStore (estudo)

Documentação do fluxo de produto que estou implementando neste monorepo para praticar arquitetura full stack, React e NestJS.

Este README é um diário de estudo: o que existe hoje, o que estou aprendendo e para onde o fluxo pode evoluir.


Objetivo do estudo

Montar um e-commerce simplificado (inspirado em lojas como Kabum) com foco em:

  • Frontend: listagem, busca, detalhe do produto e tela de checkout de serviços (garantia estendida).
  • Backend: API REST de produtos com persistência (PostgreSQL + Drizzle) e upload de imagens (Cloudflare R2).
  • Arquitetura: Clean Architecture e princípios SOLID (camadas Presentation → Application → Domain → Infrastructure).
  • Entrega: CI no GitHub Actions (lint → test → build); CD planejado para deploy automático.
  • Escala (futuro): webhooks de pagamento na compra + filas RabbitMQ para processamento assíncrono.
  • Atendimento (futuro): webhook de dúvidas para o usuário falar direto com o time de suporte (produto, checkout, pedido).
  • Qualidade: pirâmide de testes — meta de cobrir use cases, controllers, hooks, UI e E2E dos fluxos críticos.
  • Segurança: não vazar variáveis de ambiente no client (VITE_* só para URL pública).

Clean Architecture & SOLID

Este módulo segue Clean Architecture (camadas com dependência apontando para dentro) e os cinco princípios SOLID no backend NestJS e, de forma adaptada, no frontend React.

Camadas (backend — apps/api)

┌─────────────────────────────────────────────────────────────┐
│  Presentation    ProductController  (HTTP, Swagger, DTO in) │
├─────────────────────────────────────────────────────────────┤
│  Application     *UseCase.execute()  (regras de negócio)    │
├─────────────────────────────────────────────────────────────┤
│  Domain          CreateProductDto, entidades, validações    │
├─────────────────────────────────────────────────────────────┤
│  Infrastructure  ProductRepository, StorageService, db/   │
└─────────────────────────────────────────────────────────────┘
         ▲ dependências sempre apontam para dentro ▲
Camada Pasta / artefato Responsabilidade
Presentation module/product/controller/ Recebe HTTP, valida entrada, delega ao use case, não contém regra de negócio
Application module/product/useCase/ Orquestra fluxo: conflito de nome, upload, persistência, rollback
Domain module/product/dto/, db/products.ts Contratos e modelo de dados do produto
Infrastructure repository/, infra/storage/ Drizzle, S3/R2, detalhes de framework e banco

O Controller nunca acessa o banco diretamente — só chama useCase.execute().

Camadas (frontend — apps/web)

┌──────────────────────────────────────────────────┐
│  Presentation   pages/ (ProductDetail, Checkout)│
├──────────────────────────────────────────────────┤
│  Application    hooks/ (useProductById, Search) │
├──────────────────────────────────────────────────┤
│  Domain         types/product.ts                │
├──────────────────────────────────────────────────┤
│  Infrastructure api/product.ts, api/axios.ts    │
└──────────────────────────────────────────────────┘

A página não chama axios diretamente — o hook encapsula fetch e estado; a API fica em ProductApi.

SOLID no código (exemplos reais)

Princípio O que significa Como aplico no módulo produto
S — Single Responsibility Uma classe, uma razão para mudar CreateProductUseCase só cria produto; SearchProductsUseCase só busca; ProductRepository só persiste
O — Open/Closed Aberto para extensão, fechado para alteração Novo comportamento = novo use case + registro no ProductModule, sem reescrever os existentes
L — Liskov Substitution Subtipos substituíveis Testes injetam mocks de ProductRepository / StorageService com a mesma interface de uso
I — Interface Segregation Contratos pequenos e focados StorageService (arquivos) separado de ProductRepository (SQL); DTO de entrada ≠ resposta de criação
D — Dependency Inversion Depender de abstrações, não de implementações Hoje: injeção via NestJS, mas use case ainda referencia classes concretas (ProductRepository). Próximo passo: interfaces (ports) — ver seção abaixo

Exemplo atual — regra de negócio no use case (ainda acoplado à classe ProductRepository):

// createProduct.usecase.ts — Application Layer
constructor(
  private readonly productRepository: ProductRepository,
  private readonly storageService: StorageService,
) {}

async execute(input: CreateProductInput) {
  if (await this.productRepository.findByName(input.name)) {
    throw new ConflictException('Product already exists');
  }
  const imageUrl = await this.storageService.upload(input.file, 'products');
  try {
    const product = await this.productRepository.create({ ... });
    return { id: product.id, name: product.name, message: '...' };
  } catch (error) {
    await this.storageService.delete(imageUrl); // compensação / rollback
    throw error;
  }
}

Estrutura de módulo (padrão DDD / Clean Arch)

Cada feature no backend segue a mesma organização:

module/product/
├── dto/              → contratos de entrada (validação class-validator)
├── controller/       → adaptador HTTP (Presentation)
├── useCase/          → casos de uso (Application)
├── repository/       → adaptador de persistência (Infrastructure)
├── test/factories/   → Object Mother para testes isolados
└── index.module.ts   → composição e DI (NestJS Module)

O mesmo padrão se repete em module/authentication/ e module/user/, mantendo o monorepo consistente.

Próximo passo: interfaces entre Use Case e Repository (Ports)

Hoje o use case importa a classe ProductRepository (Infrastructure). O objetivo é o Application depender só de contratos, e o NestJS ligar a implementação no módulo.

Situação atual

// use case — acoplado à classe concreta
constructor(private readonly productRepository: ProductRepository) {}

Situação alvo (Ports & Adapters)

UseCase  ──depende──►  IProductRepository (port / interface)
                              ▲
                              │ implements
                       ProductRepository (adapter / Drizzle)

Passos para aplicar no módulo produto

  1. Definir o port (interface) — só os métodos que os use cases realmente usam:
// module/product/repository/product.repository.interface.ts
export interface IProductRepository {
  findById(id: string): Promise<ProductRow | undefined>;
  findByName(name: string): Promise<ProductRow | undefined>;
  create(input: CreateProductInput): Promise<ProductRow>;
  findAll(): Promise<ProductRow[]>;
  search(search: string, page: number, pageSize: number): Promise<SearchResult>;
}
  1. Implementar na Infrastructure — a classe concreta implementa o port:
@Injectable()
export class ProductRepository implements IProductRepository {
  // mesmos métodos, detalhes de Drizzle ficam aqui
}
  1. Injetar abstração no use case — o construtor recebe a interface, não a classe:
constructor(
  @Inject(PRODUCT_REPOSITORY)
  private readonly productRepository: IProductRepository,
) {}
  1. Registrar no ProductModule — token + implementação (Dependency Inversion no DI):
export const PRODUCT_REPOSITORY = Symbol('PRODUCT_REPOSITORY');

providers: [
  { provide: PRODUCT_REPOSITORY, useClass: ProductRepository },
  {
    provide: CreateProductUseCase,
    useFactory: (repo: IProductRepository, storage: StorageService) =>
      new CreateProductUseCase(repo, storage),
    inject: [PRODUCT_REPOSITORY, StorageService],
  },
  // ou: use case com @Inject(PRODUCT_REPOSITORY) no constructor + CreateProductUseCase nos providers
],
  1. Repetir para IStorageService — upload/delete isolados do S3/R2.

  2. Atualizar testes — mock implementa IProductRepository; não precisa mockar classe com métodos extras do Drizzle.

Benefício O que ganha no estudo
Use case sem import de Drizzle/Nest infra Camada Application “pura”
Trocar Postgres por outro adapter Só nova classe implements IProductRepository
Testes mais limpos Mock só do contrato, não da implementação
Interface Segregation Um port por agregado (IOrderRepository, IProductRepository)

Mesmo padrão vale para pedidos, pagamentos e consumers RabbitMQ (IOrderEventPublisher em vez de publicar AMQP dentro do use case).

Testes alinhados à arquitetura

  • Specs nos use cases (createProduct.usecase.spec.ts) — testam regra de negócio com mocks, sem banco real.
  • Factories (makeCreateProductDto, makeProductRow) — Object Mother, dados realistas, sem duplicar literais.
  • Controller testado à parte (index.controller.spec.ts) — camada de apresentação isolada.

Isso reforça testabilidade e inversão de dependência: o SUT é o use case, dependências são dublês.


Visão geral do fluxo

Home / Busca
    │
    ▼
/product/:id          → Detalhe do produto (useProductById)
    │
    ▼
/product/checkout/:id → Checkout de serviços (garantia estendida)
Rota Componente O que faz
/search/:query SearchPage Busca produtos via API
/categories/search/:query SearchPage Busca por categoria
/product/:id ProductDetail Exibe um produto
/product/checkout/:id ProductCheckout Upsell de garantia + resumo de valores

Estrutura no monorepo

Frontend (apps/web)

Arquivo Papel
src/api/axios.ts Cliente HTTP; baseURL vem de VITE_API_URL (única env no browser — só URL pública)
src/api/product.ts Chamadas: findAll, findById, search, searchCategories
src/hooks/useProductById.tsx Estado + fetch por id
src/hooks/useProductSearch.tsx Estado + busca paginada
src/pages/product-details/ Página de detalhe + link para checkout
src/pages/product-checkout/ UI de garantia estendida
src/pages/product-checkout/GUARANTEE-OPTIONS.ts Opções de garantia extraídas do componente (dados estáticos)
src/types/product.ts Contrato TypeScript do produto

Backend (apps/api)

Camada Onde Responsabilidade
Controller module/product/controller/ Rotas HTTP + Swagger
Use cases module/product/useCase/ Regra de negócio (criar, listar, buscar, findById)
Repository module/product/repository/ Queries Drizzle
Storage infra/storage/ Upload/delete de imagens no R2
DB db/products.ts Schema da tabela products

API de produto (referência)

Base: {VITE_API_URL}/product

Método Endpoint Uso no frontend
GET /product Listar todos
GET /product/:id Detalhe + checkout
GET /product/search/:query?page=&pageSize= Página de busca
GET /product/categories/search/:query?... Busca por categoria
POST /product Criar produto (multipart + imagem) — admin

O que já implementei (checkpoint)

Detalhe do produto (product-details)

  • Estados de UI: loading, erro e produto não encontrado.
  • Exibição de preço, desconto simulado (-28%), parcelamento e frete grátis.
  • Botão “adicionar ao carrinho” com Link para /product/checkout/:id.

Checkout de serviços (product-checkout)

  • Reutiliza useProductById para o resumo do produto (imagem, nome, descrição, preço).
  • Opções de garantia em GUARANTEE-OPTIONS.ts (separação de dados vs. apresentação).
  • RadioGroup controlado por useState — padrão selecionado: 12 meses (R$ 126,70).
  • Painel lateral com: preço do produto + garantia + total geral.
  • Bloco de benefícios inclusos (ícones Lucide).
  • Termos de aceite condicionais quando garantia > 0.

Ainda não persiste pedido/carrinho no backend — é UI + estado local.

Refatoração recente

Extrair GUARANTEE_OPTIONS para arquivo próprio reduz ruído no componente e facilita:

  • Reuso em outros fluxos (carrinho, modal).
  • Testes unitários só nos dados.
  • Evolução futura: buscar planos da API em vez de constante.

Segurança e variáveis de ambiente

Onde Variável Seguro no client?
apps/web VITE_API_URL Sim — URL pública da API
apps/api DATABASE_URL, R2_*, etc. Não — só no servidor

Regra: nunca usar prefixo VITE_ para segredos. O bundle do Vite expõe tudo que começa com VITE_ no browser.


Conceitos complementares (React & fluxo)

  • Hooks customizados — camada de aplicação no front (useProductById, useProductSearch).
  • React RouteruseParams, Link, rotas em App.tsx.
  • UI desacoplada — shadcn + GUARANTEE-OPTIONS.ts (dados separados da view).
  • Fluxo ponta a ponta:
Browser (React: Page → Hook → ProductApi)
    → axios (VITE_API_URL)
        → ProductController (Presentation)
            → UseCase (Application)
                → ProductRepository → PostgreSQL
                → StorageService → Cloudflare R2

CI/CD — pipeline e evolução

CI (implementado hoje)

O monorepo usa GitHub Actions (.github/workflows/ci.yml) orquestrado pelo Turborepo (pnpm lint, pnpm test, pnpm build).

push / pull_request → main
        │
        ▼
   ┌─────────┐
   │  lint   │  pnpm lint + pnpm typecheck
   └────┬────┘
        ▼
   ┌─────────┐
   │  test   │  pnpm test (env CI: DATABASE_URL, PORT)
   └────┬────┘
        ▼
   ┌─────────┐
   │  build  │  pnpm build → artifact dist/ (7 dias)
   └─────────┘
Job O que valida Por que importa no estudo
lint ESLint + TypeScript em todos os workspaces Qualidade estática antes de rodar testes
test Jest nos use cases e controllers (API) Garante que regras de negócio não quebram no PR
build Compilação Nest + Vite Prova que o código entrega artefato deployável

Detalhes da pipeline:

  • Concurrency — cancela runs antigas do mesmo PR (cancel-in-progress).
  • Cachepnpm cacheado no setup-node.
  • Lockfilepnpm install --frozen-lockfile (build reproduzível).
  • Secrets no CI — só variáveis de pipeline (DATABASE_URL fake para testes), nunca commitadas.

CD (planejado — próxima fase)

Objetivo: transformar o artifact do job build em deploy automático após merge em main.

merge em main
    │
    ▼
┌──────────────┐     ┌─────────────────┐
│ CD — API     │     │ CD — Web        │
│ deploy Nest  │     │ dist/ → CDN     │
│ (container)  │     │ ou static host  │
└──────────────┘     └─────────────────┘
Etapa CD Ideia Ferramentas candidatas
Staging Deploy automático em ambiente de homologação GitHub Environments, secrets por env
Produção Deploy manual ou após approval Environment protection rules
API Imagem Docker ou deploy direto Docker + registry, Fly.io, Railway, etc.
Web Arquivos estáticos de apps/web/dist Cloudflare Pages, S3+CDN, Vercel
Migrações drizzle migrate antes ou durante deploy Job dedicado no workflow

O CI já gera o artifact dist/ — o CD reutilizará esse output em vez de rebuildar sem cache.


Compra assíncrona — webhooks e RabbitMQ (planejado)

Fluxo de compra de produto não será só síncrono (HTTP). A visão é combinar webhooks (gateway de pagamento) com filas RabbitMQ para processamento resiliente, alinhado ao domínio descrito em docs/kabum-ecommercer.md.

Por que não só REST?

Cenário Problema só com HTTP Solução
Pagamento confirmado Gateway chama webhook fora do seu timing Endpoint idempotente + fila
Estoque / pedido Pico de compras sobrecarrega API Consumer assíncrono
E-mail / NF Não pode bloquear resposta ao usuário Publicar evento na fila

Arquitetura alvo (Clean Architecture + mensageria)

Cliente                    API (sync)                    Async
   │                          │                            │
   │ POST /orders             │                            │
   ├─────────────────────────►│ UseCase: CreateOrder       │
   │                          │  → grava pedido pending    │
   │                          │  → publica OrderCreated ───┼──► RabbitMQ
   │◄─────────────────────────┤                            │         │
   │ 202 + orderId            │                            │         ▼
   │                          │                    ┌───────────────────────┐
   │                          │                    │ Consumers               │
   │                          │                    │ • Reservar estoque      │
   │                          │                    │ • Notificar fulfillment │
   │                          │                    │ • Enviar e-mail         │
   │                          │                    └───────────────────────┘
   │                          │                            ▲
   │                          │ POST /webhooks/payment     │
   │                          │◄───────────────────────────┤ Gateway (Stripe, etc.)
   │                          │  → valida assinatura       │
   │                          │  → idempotency key         │
   │                          │  → PaymentConfirmed ───────┼──► RabbitMQ

Filas RabbitMQ (rascunho)

Fila / exchange Evento Consumer (use case)
order.created Pedido criado no checkout ReserveInventoryUseCase
payment.webhook.received Webhook do gateway ConfirmPaymentUseCase
order.paid Pagamento confirmado StartFulfillmentUseCase
order.failed Pagamento expirado / chargeback CancelOrderUseCase

Práticas que pretendo aplicar:

  • Idempotency keys em POST críticos e nos consumers (evitar pedido duplicado).
  • Dead Letter Queue (DLQ) para mensagens que falham após N retries.
  • NestJS microservices ou @golevelup/nestjs-rabbitmq na camada Infrastructure — use cases continuam sem conhecer AMQP.

Webhooks de compra

Endpoint Responsabilidade
POST /webhooks/payments/:provider Receber payload do gateway, validar HMAC/assinatura
Persistir em paymentEvents (trilha de auditoria)
Publicar na fila; não processar estoque inline no controller

O controller de webhook fica na Presentation; validação e publicação na fila ficam em Application (ProcessPaymentWebhookUseCase).

Webhook de atendimento — dúvidas do usuário (planejado)

Além do webhook de pagamento, pretendo um fluxo para suporte: o cliente tira dúvidas (produto, garantia, pedido) e o atendimento responde pelo canal oficial, sem depender só de e-mail manual.

Objetivo

  • Usuário envia pergunta a partir da loja (detalhe do produto, checkout ou área “Ajuda”).
  • A API registra o ticket e dispara webhook para o sistema de atendimento (Slack, Discord, WhatsApp Business, Zendesk, etc.).
  • O time responde no canal; opcionalmente respostas voltam via webhook inbound para atualizar status na API.

Fluxo alvo

Usuário (front)
    │  formulário: produtoId, mensagem, contato
    ▼
POST /support/questions  ou  POST /webhooks/support/outbound
    │
    ▼
SubmitSupportQuestionUseCase (Application)
    │  valida DTO, grava ticket (PostgreSQL)
    │  publica SupportQuestionCreated ──► RabbitMQ (opcional)
    ▼
Adapter de atendimento (Infrastructure)
    │  POST webhook → URL do Slack / WhatsApp / CRM
    ▼
Atendimento humano responde direto ao cliente
Peça Responsabilidade
POST /support/questions Usuário abre dúvida (produto, checkout, pedido)
SubmitSupportQuestionUseCase Regra: campos obrigatórios, vínculo com productId / orderId se existir
ISupportNotifier (port) Contrato “enviar para atendimento” — use case não conhece Slack/WhatsApp
Webhook outbound Infrastructure chama URL configurada em env (SUPPORT_WEBHOOK_URL)
POST /webhooks/support/inbound (futuro) CRM/chat devolve “respondido” / “fechado” para sincronizar ticket

Onde aparece na loja (ideias)

  • Botão “Tirar dúvida” na página de produto e no checkout de garantia.
  • Contexto automático no payload: nome do produto, plano de garantia selecionado, userId se logado.

Práticas (mesmo padrão dos outros webhooks)

  • Assinatura ou token no header (X-Webhook-Secret) para o canal de atendimento confiar na origem.
  • Idempotency por questionId se o front reenviar o formulário.
  • Sem lógica de negócio no controller — só delega ao use case.

Estratégia de testes — cobrir tudo (meta)

Pirâmide de testes como base do projeto: muitos unitários, menos integração, E2E só nos fluxos críticos.

        ┌─────────┐
        │   E2E   │  checkout → pedido → webhook (futuro)
        ├─────────┤
        │ Integr. │  repository + DB, consumer + RabbitMQ testcontainer
        ├─────────┤
        │  Unit   │  use cases, hooks, utils (maior volume)
        └─────────┘

Cobertura atual (módulo produto — API)

Artefato Spec Status
CreateProductUseCase createProduct.usecase.spec.ts
FindAllProductsUseCase findAllProduct.usecase.spec.ts
SearchProductsUseCase searchProduct.usecase.spec.ts
FindByIdProductsUseCase ❌ planejado
SearchProductsCategoriesUseCase ❌ planejado
ProductController index.controller.spec.ts ✅ parcial
ProductRepository ❌ integração com DB
Frontend ProductCheckout ❌ Vitest + Testing Library
Frontend hooks ❌ Vitest

Meta: implementar todos os testes

Backend — unitários (Jest + mocks)

  • findByIdProduct.usecase.spec.ts
  • searchProductsCategories.usecase.spec.ts
  • Factories para todos os DTOs/responses (makeProductResponse, etc.)
  • Padrão do projeto: sut, mockRepository, afterEach(clearAllMocks), make* factories

Backend — integração

  • ProductRepository com banco de teste (Docker / testcontainers no CI)
  • StorageService mockado ou bucket de teste

Backend — E2E (NestJS)

  • GET /product/:id → 200 / 404
  • POST /product multipart
  • Fluxo POST /orders + webhook de pagamento (quando existir)
  • POST /support/questions + mock de ISupportNotifier (webhook de dúvidas)

Frontend (Vitest)

  • useProductById — loading, sucesso, erro
  • useProductSearch — query vazia, lista preenchida
  • ProductCheckout — troca de garantia altera total
  • GUARANTEE-OPTIONS — valores e texto de parcelas

Mensageria (futuro)

  • Consumer: mensagem duplicada não processa duas vezes (idempotency)
  • Consumer: falha vai para DLQ após retries

CI

  • Coverage report no PR (opcional: threshold mínimo)
  • Job separado test:e2e quando E2E existir

Próximos passos (roadmap de estudo)

Produto & checkout (curto prazo)

  1. Carrinho / pedido — Context ou Zustand; POST /orders no NestJS.
  2. Checkout real — persistir garantia escolhida (DTO + validação).
  3. React Query — migrar hooks para useQuery / useMutation.
  4. Webhook de atendimento (dúvidas) — formulário no produto/checkout → SubmitSupportQuestionUseCase → webhook para o time de suporte tratar direto com o usuário.

Arquitetura (médio prazo)

  1. Ports & interfacesIProductRepository, IStorageService; use cases sem import de classes de Infrastructure.
  2. Symbols + providers — registrar ports no ProductModule (provide / useClass).
  3. Response DTO — fechar contrato entre Domain e Presentation.

Qualidade & entrega (médio prazo)

  1. Completar testes — checklist acima; mocks contra interfaces, não classes concretas.
  2. CD pipeline — deploy staging/prod a partir do artifact do CI.
  3. Autenticação — proteger POST /product e fluxo de compra.

Atendimento & escala (longo prazo)

  1. RabbitMQIEventPublisher no use case; adapter AMQP na Infrastructure.
  2. Webhooks de pagamentoProcessPaymentWebhookUseCase + port de persistência de eventos.
  3. Webhook inbound de suporte — CRM/chat atualiza status do ticket (respondido, fechado).
  4. Observabilidade — logs estruturados, métricas de fila, pedidos e tickets de atendimento.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages