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.
45 lines
2.9 KiB
Markdown
45 lines
2.9 KiB
Markdown
# 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
|