docs: API architecture ADRs, tech spec, and marketing capability tracker

Persist the modular-monolith decision (ADR 0001 handler/service/repo,
ADR 0002 keep REST) with the companion tech spec and the api-architecture
OpenSpec capability. Replace the finished mock-migration tracker with
docs/TBD-marketing.md listing the backend-less marketing domains still on
fixtures (coupons, favorites, account stats, seckill, collective, integral,
reviews). README and AGENTS.md point at the new docs.
This commit is contained in:
Chengdong Zhang
2026-09-18 16:00:31 +08:00
parent c51e96ae41
commit e10cae5789
9 changed files with 341 additions and 10 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ context: |
VMall: B2B2C e-commerce MVP, spec-driven in phases.
Tech stack:
- Backend: Rust (axum 0.8, sqlx 0.8 + Postgres 18, redis 8, JWT, argon2) at apps/api, crate vmall-api. Money is stored as integer minor units + ISO currency code; never floats.
- Backend: Rust (axum 0.8, sqlx 0.8 + Postgres 18, redis 8, JWT, argon2) at apps/api, crate vmall-api. Modular monolith: handler → service → repository under src/modules/<context> (docs/adr/0001, docs/tech-specs/rust-api.md). HTTP REST is the public contract; GraphQL is not the primary API (docs/adr/0002). Money is stored as integer minor units + ISO currency code; never floats.
- Frontends: three Nuxt 3 apps in pnpm workspace: apps/mall (customer storefront, port 3000), apps/shop-admin (merchant console, 3001), apps/admin (platform console, 3002).
- Shared contract: packages/shared (@vmall/shared) — TS types, API client, en/zh locales, ui.css. Frontends must use it; no per-app API reimplementation.
- Dev infra: Postgres + Redis run in local docker (containers pg18, rdb8); databases vmall / vmall_test; API runs migrations on boot (sqlx migrate).
+44
View File
@@ -0,0 +1,44 @@
# api-architecture Specification
## Purpose
Internal layout of `vmall-api`: a modular monolith (handler → service → repository) over a stable HTTP REST contract. Client-visible behavior of each domain remains in the other OpenSpec capabilities.
## Requirements
### Requirement: Bounded-context modules
Domain code SHALL live under `apps/api/src/modules/<context>/` (identity, catalog, cart, order, fulfillment, billing, address, shop, content, currency, health). Shared HTTP helpers SHALL live under `apps/api/src/http/`. New features MUST NOT add sqlx to a deleted-style `routes/` tree.
#### Scenario: new use case
- **WHEN** a developer adds a write use case (for example checkout or set-default-address)
- **THEN** the SQL and transaction live in a service and/or repository, and the Axum handler only authenticates, deserializes, and maps `ApiResult` to status + JSON
#### Scenario: simple list
- **WHEN** the endpoint is a single SELECT with no extra rules
- **THEN** the handler MAY call the repository directly without a dedicated service function
### Requirement: Layer contracts
Services SHALL return `ApiResult<Dto>` and MUST NOT depend on `axum::Json` or `StatusCode`. Repositories SHALL accept `&PgPool`, `&mut PgConnection`, or `&mut Transaction` so one service can compose several writes. The crate MUST NOT introduce a generic persistence trait unless a second backend exists.
#### Scenario: checkout transaction
- **WHEN** checkout locks SKUs, inserts orders, decrements stock, and clears the cart
- **THEN** Postgres writes share one transaction in `order::service`, and Redis cart clear happens after commit
### Requirement: Stable REST envelope
The public API SHALL remain resource-oriented REST under `/api`. Errors SHALL use `{"error":{"code","message"}}`. Unique and check constraint violations SHALL map to HTTP 409 unless the handler substitutes a domain message via `unique_conflict`.
#### Scenario: duplicate email
- **WHEN** registration hits a unique email constraint
- **THEN** the client receives 409 with code `CONFLICT` and a domain message, not a 500
### Requirement: Shared use cases across surfaces
Customer, shop, and platform-admin reads of the same aggregate SHALL call one service with an explicit scope (for example `OrderScope::{User, Shop, Admin}`) instead of copying SQL per router.
#### Scenario: order list
- **WHEN** `GET /api/orders`, `GET /api/shop/orders`, and `GET /api/admin/orders` run
- **THEN** they share `order::service::list` and differ only by auth and scope
### Requirement: Tests
HTTP integration tests under `apps/api/tests/` SHALL remain the contract suite. State-machine use cases (checkout stock, illegal pay/cancel) SHALL also be covered at the service layer without requiring GraphQL.
#### Scenario: service checkout
- **WHEN** a cart qty exceeds SKU stock
- **THEN** `order::checkout` returns `ApiError::Conflict` and no order row is committed