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.
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).
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.
┌─────────────────────────────────────────────────────────────┐
│ 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().
┌──────────────────────────────────────────────────┐
│ 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.
| 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;
}
}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.
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
- 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>;
}- Implementar na Infrastructure — a classe concreta implementa o port:
@Injectable()
export class ProductRepository implements IProductRepository {
// mesmos métodos, detalhes de Drizzle ficam aqui
}- Injetar abstração no use case — o construtor recebe a interface, não a classe:
constructor(
@Inject(PRODUCT_REPOSITORY)
private readonly productRepository: IProductRepository,
) {}- 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
],-
Repetir para
IStorageService— upload/delete isolados do S3/R2. -
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).
- 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.
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 |
| 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 |
| 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 |
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 |
- 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
Linkpara/product/checkout/:id.
- Reutiliza
useProductByIdpara 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). RadioGroupcontrolado poruseState— 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.
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.
| 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.
- Hooks customizados — camada de aplicação no front (
useProductById,useProductSearch). - React Router —
useParams,Link, rotas emApp.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
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). - Cache —
pnpmcacheado nosetup-node. - Lockfile —
pnpm install --frozen-lockfile(build reproduzível). - Secrets no CI — só variáveis de pipeline (
DATABASE_URLfake para testes), nunca commitadas.
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.
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.
| 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 |
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
| 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-rabbitmqna camada Infrastructure — use cases continuam sem conhecer AMQP.
| 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).
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,
userIdse 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
questionIdse o front reenviar o formulário. - Sem lógica de negócio no controller — só delega ao use case.
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)
└─────────┘
| 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 |
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
-
ProductRepositorycom banco de teste (Docker / testcontainers no CI) -
StorageServicemockado ou bucket de teste
Backend — E2E (NestJS)
-
GET /product/:id→ 200 / 404 -
POST /productmultipart - Fluxo
POST /orders+ webhook de pagamento (quando existir) -
POST /support/questions+ mock deISupportNotifier(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:e2equando E2E existir
- Carrinho / pedido — Context ou Zustand;
POST /ordersno NestJS. - Checkout real — persistir garantia escolhida (DTO + validação).
- React Query — migrar hooks para
useQuery/useMutation. - Webhook de atendimento (dúvidas) — formulário no produto/checkout →
SubmitSupportQuestionUseCase→ webhook para o time de suporte tratar direto com o usuário.
- Ports & interfaces —
IProductRepository,IStorageService; use cases sem import de classes de Infrastructure. - Symbols + providers — registrar ports no
ProductModule(provide/useClass). - Response DTO — fechar contrato entre Domain e Presentation.
- Completar testes — checklist acima; mocks contra interfaces, não classes concretas.
- CD pipeline — deploy staging/prod a partir do artifact do CI.
- Autenticação — proteger
POST /producte fluxo de compra.
- RabbitMQ —
IEventPublisherno use case; adapter AMQP na Infrastructure. - Webhooks de pagamento —
ProcessPaymentWebhookUseCase+ port de persistência de eventos. - Webhook inbound de suporte — CRM/chat atualiza status do ticket (
respondido,fechado). - Observabilidade — logs estruturados, métricas de fila, pedidos e tickets de atendimento.