# Architecture ``` browser → Vite :5173 --proxy /api,/health--> Axum :8080 → PostgreSQL ↑ │ └── generated client ← openapi.json ← utoipa-axum router ``` ## Why this shape One workspace. The server owns HTTP and persistence. The web app owns interaction. The contract package is generated, never edited. Config is shared on purpose. `config/default.toml` is read by both `AppConfig::load` and `apps/web/vite.config.ts`, so the reverse proxy target cannot drift from the bind address. ## Request path 1. Vite binds `127.0.0.1` (not `localhost`) so the browser never hits IPv6 `::1`. 2. Browser calls `/api/v1/items`. 3. Vite proxies to `http://127.0.0.1:8080`. 4. `SetRequestIdLayer` assigns `X-Request-Id`. 5. `request_logging` records method, path, status, latency, request id. 6. Handler returns `Result`. Errors become a single JSON envelope. ## Adding an endpoint 1. DTO in `apps/server/src/models`. 2. Handler in `apps/server/src/handlers` with `#[utoipa::path]`. 3. Register it with `.routes(routes!(your_handler))` in `routes/mod.rs`. 4. `just generate-api`. 5. Call the new function from `apps/web/src/api/generated.ts`.