256 lines
10 KiB
Markdown
256 lines
10 KiB
Markdown
# Sport Store
|
||
|
||
A modern sports-fashion e-commerce platform.
|
||
|
||
**Status: milestone 0 — architecture skeleton.** The structure, boundaries, data model and
|
||
tooling are in place and verified. Business features are not implemented yet; see
|
||
[Roadmap](#roadmap).
|
||
|
||
```
|
||
Storefront (Next.js) ─┐
|
||
├─▶ REST API (NestJS) ─▶ PostgreSQL · Redis · R2/S3
|
||
Admin (Next.js) ──────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Quick start
|
||
|
||
Requires **Node ≥ 22**, **pnpm ≥ 10** (`corepack enable`) and **Docker**.
|
||
|
||
```bash
|
||
pnpm setup # env files, real JWT secrets, deps, containers, migrations, seed
|
||
pnpm dev # storefront + admin + API, all watching
|
||
```
|
||
|
||
| Service | URL |
|
||
| ------------------ | --------------------------------------------------- |
|
||
| Storefront | http://localhost:3000 |
|
||
| Admin | http://localhost:3001 |
|
||
| API | http://localhost:4000/api/v1/health |
|
||
| API docs (Swagger) | http://localhost:4000/docs |
|
||
| MinIO console | http://localhost:9001 (`sportminio` / `sportminio`) |
|
||
| Mailpit | http://localhost:8025 |
|
||
|
||
Verify the stack is healthy:
|
||
|
||
```bash
|
||
curl -s http://localhost:4000/api/v1/health | jq
|
||
```
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"status": "ok",
|
||
"dependencies": {
|
||
"database": { "status": "up", "latencyMs": 3 },
|
||
"redis": { "status": "up", "latencyMs": 1 }
|
||
}
|
||
},
|
||
"meta": { "requestId": "019fef70-a80f-77d5-a975-bbe9f5f0c32c", "timestamp": "…" }
|
||
}
|
||
```
|
||
|
||
> **Ports.** PostgreSQL is published on **5433** and Redis on **6380**, not their defaults. Many
|
||
> machines already run one or both, and a shadowed port surfaces as a baffling authentication
|
||
> error rather than a clear conflict. Inside the Docker network they still use 5432/6379.
|
||
|
||
---
|
||
|
||
## Commands
|
||
|
||
### Everyday
|
||
|
||
```bash
|
||
pnpm dev # everything
|
||
pnpm dev:storefront # one app (dependencies are built first)
|
||
pnpm dev:admin
|
||
pnpm dev:api
|
||
|
||
pnpm build # build all, in dependency order
|
||
pnpm lint # ESLint across the workspace
|
||
pnpm lint:fix
|
||
pnpm typecheck # tsc --noEmit everywhere
|
||
pnpm test
|
||
pnpm format # Prettier write
|
||
pnpm format:check
|
||
```
|
||
|
||
### Infrastructure
|
||
|
||
```bash
|
||
pnpm infra:up # PostgreSQL, Redis, MinIO, Mailpit
|
||
pnpm infra:down
|
||
pnpm infra:logs
|
||
pnpm infra:reset # destroys volumes, then restarts
|
||
|
||
docker compose --profile full up --build # full stack behind Nginx on :80
|
||
```
|
||
|
||
### Database
|
||
|
||
```bash
|
||
pnpm db:migrate # create + apply a migration (dev)
|
||
pnpm db:deploy # apply pending migrations (production)
|
||
pnpm db:generate # regenerate Prisma Client
|
||
pnpm db:seed # reconcile permissions + roles (idempotent)
|
||
pnpm db:studio # Prisma Studio
|
||
pnpm db:reset # drop, re-migrate, re-seed
|
||
```
|
||
|
||
### Running one app manually
|
||
|
||
```bash
|
||
pnpm --filter @sport/api run dev # :4000
|
||
pnpm --filter @sport/storefront run dev # :3000
|
||
pnpm --filter @sport/admin run dev # :3001
|
||
```
|
||
|
||
---
|
||
|
||
## Repository layout
|
||
|
||
```
|
||
sport-store/
|
||
├── apps/
|
||
│ ├── storefront/ Next.js customer site (:3000)
|
||
│ │ └── src/
|
||
│ │ ├── app/ App Router — (shop) (checkout) (account) route groups
|
||
│ │ ├── components/ Cross-feature UI (layout, chrome)
|
||
│ │ ├── features/ auth · product · category · collection · search
|
||
│ │ │ cart · checkout · order · wishlist · account
|
||
│ │ ├── hooks/ lib/ services/ stores/ styles/ types/
|
||
│ │
|
||
│ ├── admin/ Next.js back office (:3001)
|
||
│ │ └── src/
|
||
│ │ ├── app/ (auth) login · (dashboard) everything else
|
||
│ │ ├── components/ features/ lib/ styles/
|
||
│ │
|
||
│ └── api/ NestJS modular monolith (:4000)
|
||
│ ├── prisma/ schema.prisma · migrations · seed.ts
|
||
│ └── src/
|
||
│ ├── config/ env validation → typed config object
|
||
│ ├── common/ decorators · filters · guards · interceptors
|
||
│ │ middleware · pipes · errors
|
||
│ ├── infrastructure/ prisma · redis · storage · events · logging
|
||
│ └── modules/ 20 bounded contexts
|
||
│
|
||
├── packages/
|
||
│ ├── types/ Framework-free domain + API contracts (zero deps)
|
||
│ ├── validation/ Zod schemas shared by API and both frontends
|
||
│ ├── api-client/ The only sanctioned way for a frontend to reach the API
|
||
│ ├── ui/ Design-system primitives (Button, Input, Badge, Skeleton)
|
||
│ ├── config/ Shared tsconfig bases + Tailwind theme tokens
|
||
│ └── eslint-config/ Flat configs incl. the architectural boundary rules
|
||
│
|
||
├── infrastructure/
|
||
│ ├── docker/ One Dockerfile per app (turbo prune → standalone)
|
||
│ ├── nginx/ Edge routing, rate ceiling, asset caching
|
||
│ └── scripts/ bootstrap.sh · reset-db.sh
|
||
│
|
||
├── docs/
|
||
│ ├── architecture.md Boundaries, conventions, risks — read this first
|
||
│ └── adr/ 12 decision records
|
||
│
|
||
├── docker-compose.yml Backing services; `--profile full` runs everything
|
||
├── turbo.json pnpm-workspace.yaml package.json
|
||
```
|
||
|
||
---
|
||
|
||
## Architecture in brief
|
||
|
||
Full detail in [`docs/architecture.md`](./docs/architecture.md). The rules that matter most:
|
||
|
||
1. **Only the API touches data.** Neither frontend has a database, Redis or storage client.
|
||
Enforced by ESLint and by the absence of `DATABASE_URL` from their environments.
|
||
([ADR-0004](./docs/adr/0004-the-admin-dashboard-has-no-database-access.md))
|
||
|
||
2. **All business logic lives in the backend.** The storefront may format `₫250.000`; it may
|
||
never compute a discount.
|
||
|
||
3. **Product ≠ ProductVariant.** A product has a page; a _variant_ has a SKU, a price and stock.
|
||
Every colour × size combination is its own variant.
|
||
([ADR-0003](./docs/adr/0003-product-and-productvariant-as-separate-entities.md))
|
||
|
||
4. **Authorization is permissions, never role checks.** There is no `if (user.role === 'ADMIN')`
|
||
anywhere. ([ADR-0007](./docs/adr/0007-rbac-permissions-instead-of-role-checks.md))
|
||
|
||
5. **Authentication is on by default.** The access-token guard is global; a route is public only
|
||
by declaring `@Public()`. Forgetting a decorator fails closed.
|
||
|
||
6. **Money is an integer in minor units**, everywhere.
|
||
([ADR-0011](./docs/adr/0011-money-as-integer-minor-units.md))
|
||
|
||
7. **Modules own their tables exclusively.** Cross-module access goes through a `public/` barrel
|
||
or a domain event — enforced by ESLint, which is what keeps a future service extraction
|
||
possible. ([ADR-0002](./docs/adr/0002-modular-monolith-not-microservices.md))
|
||
|
||
### Storefront routes
|
||
|
||
`/` · `/men` · `/women` · `/sports/[running|football|training|gym|badminton|lifestyle]` ·
|
||
`/products/[slug]` · `/collections/[slug]` · `/search` · `/cart` · `/checkout` ·
|
||
`/account/[profile|orders|addresses|wishlist]` · `/blog`
|
||
|
||
### Backend modules
|
||
|
||
`auth` `users` `customers` `products` `product-variants` `categories` `collections` `brands`
|
||
`inventory` `carts` `checkout` `orders` `payments` `promotions` `coupons` `wishlist` `reviews`
|
||
`cms` `media` `search` (+ `health`)
|
||
|
||
---
|
||
|
||
## Roadmap
|
||
|
||
| Milestone | Scope |
|
||
| --------- | ------------------------------------------------------------------------------------- |
|
||
| **M0** ✅ | Architecture, tooling, schema, health check, Docker, CI |
|
||
| **M1** | Catalog read API: products, variants, categories, collections, brands + Redis caching |
|
||
| **M2** | Auth: login, refresh rotation, RBAC admin, user/role management |
|
||
| **M3** | Admin catalog: product editor, variant matrix, media uploads, inventory |
|
||
| **M4** | Storefront catalog: listings, PDP with variant selector, filters |
|
||
| **M5** | Cart, checkout, orders |
|
||
| **M6** | Search + faceting |
|
||
| **M7** | Promotions, coupons, reviews, CMS |
|
||
| **M8** | Customer account |
|
||
| **M9** | Payments (VNPay, MoMo, ZaloPay, COD), shipping, notifications |
|
||
|
||
**Recommended next step: M1.** It exercises every layer end to end — Prisma repository → service
|
||
→ controller → envelope → `@sport/api-client` → a rendered page — on read-only endpoints where
|
||
mistakes are cheap. It also proves the variant model against real data before anything writes to
|
||
it. Auth (M2) comes second because the enforcement half already exists; only issuance is
|
||
missing.
|
||
|
||
---
|
||
|
||
## Verified in this environment
|
||
|
||
Everything below was run, not assumed:
|
||
|
||
- `pnpm install` — 10 workspace projects resolved
|
||
- `pnpm lint` · `pnpm typecheck` · `pnpm build` — 24/24 Turborepo tasks pass
|
||
- `pnpm format:check` — clean
|
||
- `prisma migrate dev` — 25 tables created
|
||
- `pnpm db:seed` — 36 permissions, 6 roles
|
||
- API boots; `GET /api/v1/health` returns `status: ok` with PostgreSQL and Redis both `up`
|
||
- Error envelope confirmed on a 404; `x-request-id` echoed; Helmet, CORS and rate-limit headers
|
||
present; Swagger served at `/docs`
|
||
- `pnpm test` — 5 passing RBAC guard tests
|
||
- Storefront renders 20 routes (`/sports/curling` correctly 404s); admin renders 17
|
||
|
||
Known benign noise: NestJS logs two `Unsupported route path: "/api/*"` warnings at boot. They
|
||
come from Nest's own global-prefix handling under Express 5 / path-to-regexp v8, are
|
||
auto-converted correctly, and routing is verified working. Nothing in this repository registers
|
||
that path.
|
||
|
||
---
|
||
|
||
## Contributing
|
||
|
||
- Branches: `feat/…`, `fix/…`, `chore/…`, `docs/…`
|
||
- Commits: [Conventional Commits](https://www.conventionalcommits.org/)
|
||
- CI runs lint, typecheck, format, tests against real PostgreSQL and Redis, builds every app,
|
||
and builds all three Docker images on push.
|
||
- Architectural changes need an ADR in [`docs/adr/`](./docs/adr/README.md).
|