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.
2.9 KiB
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
ApiResultto 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
CONFLICTand 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, andGET /api/admin/ordersrun - THEN they share
order::service::listand 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::checkoutreturnsApiError::Conflictand no order row is committed