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.
5.3 KiB
Rust API tech spec
Companion to ADR 0001 and ADR 0002. This document describes the current vmall-api layout and the rules new code must follow. HTTP behavior for each domain remains in openspec/specs/ (auth, catalog, cart, order, …).
Context and motivation
Handlers previously mixed Axum extracts, business rules, and sqlx. The crate is now a modular monolith so checkout, fulfillment, and identity can be shared across the three role surfaces without changing REST URLs or JSON.
Goals:
- One crate, one process, Postgres + Redis.
- Handler → service → repository (or store) per bounded context.
- Stable REST contract for
@vmall/sharedand HTTP integration tests.
Non-goals:
- GraphQL as the primary API (ADR 0002).
- Hexagonal ports/adapters, a generic Repository trait, or a second web framework.
- Changing error envelope, money representation, or RBAC roles.
Implementation considerations
- Crate:
apps/api, packagevmall-api, Axum 0.8, sqlx 0.8, Redis connection manager. - Migrations:
apps/api/migrations/, append-only; run on boot against a singlePgPoolshared with the server (seemain.rs+state::assemble). - Money:
i64minor units + ISO code;money::convert_minoris a pure function, not a repository. - SQL:
sqlx::query*/query_aswith explicit binds. Prefer column lists overSELECT *onusers(useUSER_COLUMNS+Userrow vsUserPublicJSON). - State machines:
UPDATE … WHERE status = …; zero rows →ApiError::Conflict, never a silent no-op success. - RBAC:
AuthUser::require,require_customer,require_admin,require_shop/own_shop. Cross-shop resource access is 404, not 403.
High-level request flow
Nuxt + @vmall/shared → Handler (Axum)
→ Service (use case)
→ Repo / cart store
→ Postgres | Redis
- Handler extracts
State<AppState>,AuthUser, path/query/JSON. - Handler maps HTTP-only concerns (
StatusCode::CREATED) after the service returnsApiResult<Dto>. - Service opens transactions when more than one write must commit together (checkout, default address, shipment create).
- Repository functions take
&PgPool,&mut PgConnection, or&mut Transactionso a service can compose them.
Module map
| Module | Bounded context | Typical routes |
|---|---|---|
identity |
Auth, users, role assignment | /auth/*, /admin/users |
catalog |
Products, SKUs, categories, brands | /products, /shop/products, /brands |
cart |
Redis cart + purchasable snapshots | /cart |
order |
Checkout, pay, cancel, lists by scope | /orders, /shop/orders, /admin/orders |
fulfillment |
Shipments, delivery confirmation | /shipments, /shop/shipments |
billing |
Invoice request / issue | /invoices, /shop/invoices |
address |
Customer address book | /addresses |
shop |
Shop CRUD, profiles, /shop/profile |
/shops, /admin/shops |
content |
Home banners / promos / links | /content/home, /admin/content |
currency |
Rates and convert | /currencies |
health |
Liveness / readiness | /health, /ready |
Each module typically contains mod.rs, handlers.rs, service.rs, and optionally repo.rs / dto.rs / store.rs. Merge routers in modules::api_router().
Shared types live in models.rs (sqlx FromRow + enums). API-facing user JSON is UserPublic (no password_hash).
Error handling
ApiError serializes as {"error":{"code","message"}}:
| Variant | HTTP | code |
|---|---|---|
NotFound |
404 | NOT_FOUND |
BadRequest |
400 | BAD_REQUEST |
Unauthorized |
401 | UNAUTHORIZED |
Forbidden |
403 | FORBIDDEN |
Conflict |
409 | CONFLICT |
Internal |
500 | INTERNAL (generic message; details in logs) |
From<sqlx::Error>: RowNotFound → 404; unique / check violations → 409. Domain-specific unique messages use unique_conflict(err, "email already registered").
Future-proofing
- New capabilities get a new or existing
modules/<ctx>plus OpenSpec; they do not add SQL to handlers. - A later read-only GraphQL surface would sit beside REST and call the same services.
- Compile-time
query_as!may replace string SQL incrementally; it is not required for new queries.
Testing approach
- HTTP contract:
apps/api/tests/*.rsviatests/common/mod.rs(spawn_app, unique slugs). These tests are the REST regression gate. - Service:
tests/order_service.rs(and similar) call services withspawn_state()— empty cart, stock 409, split-by-shop, illegal status transitions. - Pure functions:
money,http::paginationunit tests in-module.
Acceptance criteria
- New write use cases live in a service; handlers do not embed sqlx except trivial reads if a service would be empty ceremony.
cargo test -p vmall-apistays green and repeatable againstvmall_test+ Redis.- REST paths and JSON shapes used by
@vmall/shareddo not change without an OpenSpec change.