Project Overview
ShopFlow is a full-stack e-commerce platform built with TypeScript, using Next.js 14 (App Router) for the frontend and Express.js for the API gateway. It follows a modular monolith architecture with clear domain boundaries — each bounded context owns its schema and exposes a typed public surface.
The codebase spans 847 files across 23 modules, with the six core domain modules (catalog, cart, checkout, users, orders, payments) handling all business logic. Shared infrastructure lives in src/shared/.
Next.js 14 · React 18 · TailwindCSS · Zustand
Express.js · Prisma ORM · PostgreSQL
Docker · Redis (caching) · BullMQ (job queues)
Vitest · Playwright · MSW
Architecture Overview
The system follows a modular monolith pattern with 6 bounded contexts communicating through an internal typed event bus. Each module owns its database schema (via Prisma) and exposes a public API through barrel exports — no module imports another module's internals directly.
Pattern: Each module follows the same internal structure — routes/, services/, repositories/, events/, and types/. This predictability means once you understand one module, you understand all of them.
Repository Structure
Top-level structure with file counts and responsibilities as scanned:
Module / Component Map
Hub files are ranked by dependent count. Higher dependents = higher blast radius if you break it.
| Module | Responsibility | Key Hub File | Dependents | Risk |
|---|---|---|---|---|
| users | Auth (JWT + OAuth), profile management | users/services/auth-service.ts | 41 | CRITICAL |
| catalog | Product CRUD, search indexing, category tree | catalog/services/product-service.ts | 34 | HIGH |
| cart | Cart state, promo engine, price calculation | cart/services/price-calculator.ts | 28 | HIGH |
| checkout | Multi-step checkout, address validation | checkout/services/checkout-orchestrator.ts | 22 | HIGH |
| orders | Order state machine, fulfillment webhooks | orders/services/order-state-machine.ts | 19 | MEDIUM |
| payments | Stripe API, refund logic, invoice generation | payments/services/stripe-adapter.ts | 15 | MEDIUM |
Key Concepts & Domain Model
These are the non-obvious concepts that a new developer must internalize before making changes:
- Product Variant: Products have variants (size, color). The full type hierarchy is defined in catalog/types/variant.ts. A ProductVariant is always linked to a Product — never exists standalone.
- Cart Rules Engine: Promotions are applied via a rules engine in cart/services/promo-engine.ts. Adding a new promo type means implementing the PromoRule interface, then registering it in the engine. Do not mutate cart state directly.
- Order State Machine: Orders follow a strict lifecycle — created → paid → processing → shipped → delivered, with cancelled and refunded as terminal branches. All transitions are in orders/services/order-state-machine.ts. Never set order status directly on the repository — always go through the state machine.
- Internal Event Bus: Modules communicate via typed events, never via direct imports. shared/events/event-bus.ts is the hub. Events are defined per-module in events/types.ts and consumed in events/handlers.ts.
- Barrel Exports: Each module exposes only what it intends to be public through its index.ts barrel. Importing from a module's internal subfolder directly is a convention violation.
Development Workflow
All commands use pnpm. Node 20+ required. Docker needed for full-stack local dev.
# Initial setup pnpm install # Start development servers pnpm dev # Next.js frontend (port 3000) pnpm dev:api # Express API gateway (port 4000) pnpm db:migrate # Run pending Prisma migrations pnpm db:seed # Seed test data (dev only) # Testing pnpm test # Vitest unit tests (watch mode) pnpm test:e2e # Playwright end-to-end suite pnpm test:integration # API integration tests (requires running API) # Production pnpm build # Full production build pnpm docker:up # Full stack via Docker Compose
Architectural Decisions
These decisions were surfaced from commit history, ADR files, and code structure analysis:
- Modular monolith over microservices: Chosen for team size (8 developers). The module boundaries are clean enough that each module can be extracted to an independent service later without major surgery.
- Prisma over raw SQL: Type-safe queries and auto-generated client reduce runtime type errors. Migration files live in shared/database/migrations/ — always review generated SQL before applying to production.
- Zustand over Redux: Simpler API, smaller bundle. Each page has its own store slice under stores/. Do not use a single global store — colocate state with the feature that owns it.
- BullMQ for async jobs: Email sending, inventory sync, and webhook retries run as background jobs. Queue definitions live in shared/jobs/. Redis must be running for any job-dependent feature to work locally.
Cross-Cutting Concerns
- Authentication: JWT tokens with refresh rotation. Middleware at shared/middleware/auth.ts. Every API route except /auth/* requires a valid Authorization: Bearer token. The middleware attaches req.user for downstream use.
- Error Handling: Centralized handler in shared/middleware/error-handler.ts. All domain errors must extend AppError. Never throw raw Error objects from service layer — use the typed subclasses (NotFoundError, ValidationError, etc.).
- Logging: Structured JSON via Pino. Request IDs propagated through the x-request-id header. Always include requestId in log context for traceability.
- Caching: Redis cache with per-module namespacing (catalog:, cart:, etc.). Cache invalidation events flow through the event bus — do not call redis.del() directly from business logic.
Danger Zones & Common Tasks
High-Risk Files — Modify With Extreme Caution
| File | Dependents | Why It's Dangerous |
|---|---|---|
| users/services/auth-service.ts | 41 | Auth logic — breaking this locks out every user in production |
| shared/events/event-bus.ts | 38 | Inter-module communication hub — type changes cascade everywhere |
| catalog/services/product-service.ts | 34 | Product data — affects search, cart, checkout, and orders simultaneously |
| shared/database/prisma-client.ts | 31 | Database access singleton — every module depends on this |
| cart/services/price-calculator.ts | 28 | Price logic — a silent calculation bug means revenue loss at scale |
Circular Dependencies — 2 Clusters Detected
checkout calls payments to initiate a charge; payments emits a payment.confirmed event that checkout listens to for order completion. This creates a logical cycle. Status: Acceptable — event-based decoupling already planned for Q3. Do not resolve by adding a direct import.
cart reads catalog prices for calculation; catalog reads cart state for "in-cart" badge display. Status: Action needed. Extract shared price types to shared/types/pricing.ts to break this cycle before adding new cross-module features.
Common Tasks — Step-by-Step
Quick Reference
Entry Points
Recommended Reading Order (for new engineers)
Critical Environment Variables
Full list in .env.example. These must be set before the application starts: