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:
@@ -0,0 +1,104 @@
|
||||
Rust API tech spec
|
||||
==================
|
||||
|
||||
Companion to [ADR 0001](../adr/0001-rust-api-modular-monolith.md) and [ADR 0002](../adr/0002-keep-http-rest-not-graphql.md). 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/shared` and HTTP integration tests.
|
||||
|
||||
Non-goals:
|
||||
|
||||
- GraphQL as the primary API ([ADR 0002](../adr/0002-keep-http-rest-not-graphql.md)).
|
||||
- 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`, package `vmall-api`, Axum 0.8, sqlx 0.8, Redis connection manager.
|
||||
- **Migrations:** `apps/api/migrations/`, append-only; run on boot against a **single** `PgPool` shared with the server (see `main.rs` + `state::assemble`).
|
||||
- **Money:** `i64` minor units + ISO code; `money::convert_minor` is a pure function, not a repository.
|
||||
- **SQL:** `sqlx::query*` / `query_as` with explicit binds. Prefer column lists over `SELECT *` on `users` (use `USER_COLUMNS` + `User` row vs `UserPublic` JSON).
|
||||
- **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
|
||||
```
|
||||
|
||||
1. Handler extracts `State<AppState>`, `AuthUser`, path/query/JSON.
|
||||
2. Handler maps HTTP-only concerns (`StatusCode::CREATED`) after the service returns `ApiResult<Dto>`.
|
||||
3. Service opens transactions when more than one write must commit together (checkout, default address, shipment create).
|
||||
4. Repository functions take `&PgPool`, `&mut PgConnection`, or `&mut Transaction` so 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/*.rs` via `tests/common/mod.rs` (`spawn_app`, unique slugs). These tests are the REST regression gate.
|
||||
- **Service:** `tests/order_service.rs` (and similar) call services with `spawn_state()` — empty cart, stock 409, split-by-shop, illegal status transitions.
|
||||
- **Pure functions:** `money`, `http::pagination` unit 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-api` stays green and repeatable against `vmall_test` + Redis.
|
||||
- REST paths and JSON shapes used by `@vmall/shared` do not change without an OpenSpec change.
|
||||
Reference in New Issue
Block a user