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,51 @@
|
||||
# 0001. Modular monolith with handler / service / repository
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-09-18
|
||||
- Deciders: VMall maintainers
|
||||
- Related: [0002](0002-keep-http-rest-not-graphql.md), [tech spec](../tech-specs/rust-api.md)
|
||||
|
||||
## Context
|
||||
|
||||
`vmall-api` started as Axum route modules with SQL, validation, and HTTP mapping in the same handler. That was fine for an MVP of a few files. Checkout, stock, shipment status, and invoices then lived in 200–400 line handlers, duplicated across customer / shop / admin surfaces, with `SELECT *` leaking `password_hash` behind `skip_serializing`.
|
||||
|
||||
Rails-style MVC does not map cleanly onto Axum: there is no View layer, and “Controller” is just the handler. Full hexagonal / DDD (ports, adapters, domain events) would add compile time and indirection without a second persistence backend.
|
||||
|
||||
## Decision
|
||||
|
||||
Keep a **single crate** (`vmall-api`) as a **modular monolith**. Split code by **bounded context** under `apps/api/src/modules/<context>/`, with three roles:
|
||||
|
||||
| Layer | Owns | Must not own |
|
||||
|-------|------|----------------|
|
||||
| Handler | Axum extracts, RBAC, HTTP status, JSON envelope | SQL, Redis, state machines |
|
||||
| Service | Use cases (checkout, default address, publish product) | `Json`, `StatusCode`, path params |
|
||||
| Repository / store | sqlx and Redis | HTTP types |
|
||||
|
||||
Simple CRUD may skip the service file and call the repository from the handler. Do **not** introduce a generic `Repository` trait unless a second backend exists.
|
||||
|
||||
Shared crate roots stay small: `error`, `auth`, `models`, `money`, `state`, `http` (pagination / query DTOs), `config`, `seed`.
|
||||
|
||||
HTTP paths, JSON field names, and `{"error":{"code","message"}}` stay unchanged so `@vmall/shared` and the three Nuxt apps do not move.
|
||||
|
||||
## Consequences
|
||||
|
||||
Positive:
|
||||
|
||||
- Customer, shop, and admin order lists share `order::service` with an `OrderScope`.
|
||||
- Checkout and fulfillment can be unit-tested against `AppState` without HTTP.
|
||||
- New features land in an existing module instead of growing `routes/*.rs`.
|
||||
|
||||
Negative:
|
||||
|
||||
- More files per use case; trivial list endpoints look heavier than a single handler.
|
||||
- Cross-module calls (fulfillment → order repo) must stay explicit; no hidden event bus.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep fat handlers.** Rejected: checkout and shipment transitions were already hard to reuse.
|
||||
|
||||
**Classic MVC packages (`controllers/`, `services/`, `models/`).** Rejected: splits a use case across three top-level trees; Axum has no views.
|
||||
|
||||
**Hexagonal architecture + domain events.** Rejected for current size: one Postgres, one Redis, one process.
|
||||
|
||||
**Framework switch (Loco, Actix).** Rejected: Axum 0.8 already matches the stack; a rewrite would not fix layering.
|
||||
@@ -0,0 +1,38 @@
|
||||
# 0002. Keep HTTP REST; do not replace the API with GraphQL
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-09-18
|
||||
- Deciders: VMall maintainers
|
||||
- Related: [0001](0001-rust-api-modular-monolith.md)
|
||||
|
||||
## Context
|
||||
|
||||
The mall, shop-admin, and platform-admin apps share one typed REST client in `@vmall/shared`. Handlers already return composed DTOs (`ProductWithSkus`, `OrderView`, `CartView`). Command flows (checkout, pay, cancel, partial ship, issue invoice) are state machines with 409 conflicts and idempotent `UPDATE … WHERE status = …`.
|
||||
|
||||
A GraphQL rewrite was proposed to “modernize” the API.
|
||||
|
||||
## Decision
|
||||
|
||||
**Keep REST** on `/api/*`. Do not replace the public contract with GraphQL.
|
||||
|
||||
A **read-only GraphQL** endpoint for catalog browsing may be considered later if a third-party or mobile client needs arbitrary field sets. Write paths (checkout, stock, fulfillment, invoices) stay REST commands.
|
||||
|
||||
## Consequences
|
||||
|
||||
Positive:
|
||||
|
||||
- Existing OpenSpec HTTP scenarios, integration tests, and the mock adapter remain valid.
|
||||
- Role checks stay on routes (`AuthUser::require_*`), not per GraphQL field.
|
||||
- GET caching and payment/webhook-style POSTs stay straightforward.
|
||||
|
||||
Negative:
|
||||
|
||||
- Clients that want a custom nested graph still make several REST calls (already the case; DTOs cover storefront needs).
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Full GraphQL (`async-graphql`) as the only API.** Rejected: would rewrite three apps, `@vmall/shared`, seed scripts, and all HTTP tests; field-level auth for three roles on one schema is harder to audit; N+1 needs DataLoaders; uploads and webhooks still want REST.
|
||||
|
||||
**JSON:API / sparse fieldsets.** Not needed while composed DTOs match the UIs.
|
||||
|
||||
**BFF per frontend.** Unnecessary while all three apps share one contract package.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
ADRs in this directory record **why** the Rust API (`apps/api`, crate `vmall-api`) is structured the way it is. They are written in English and do not replace OpenSpec capability specs (`openspec/specs/`), which describe **what** HTTP behavior clients may rely on.
|
||||
|
||||
| ID | Title | Status |
|
||||
|----|--------|--------|
|
||||
| [0001](0001-rust-api-modular-monolith.md) | Modular monolith with handler / service / repository | Accepted |
|
||||
| [0002](0002-keep-http-rest-not-graphql.md) | Keep HTTP REST; do not replace the API with GraphQL | Accepted |
|
||||
|
||||
Template (MADR-inspired): Context → Decision → Consequences → Alternatives.
|
||||
|
||||
New ADRs: next unused number, `NNNN-kebab-title.md`, Status `Proposed` until accepted.
|
||||
|
||||
Companion: [tech spec — Rust API](../tech-specs/rust-api.md), [OpenSpec — api-architecture](../../openspec/specs/api-architecture/spec.md).
|
||||
Reference in New Issue
Block a user