# 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//` (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` 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