browser → Vite :5173 --proxy /api,/health--> Axum :8080 → PostgreSQL
↑ │
└── generated client ← openapi.json ← utoipa-axum router
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.
127.0.0.1 (not localhost) so the browser never hits IPv6 ::1./api/v1/items.http://127.0.0.1:8080.SetRequestIdLayer assigns X-Request-Id.request_logging records method, path, status, latency, request id.Result<T, ApiError>. Errors become a single JSON envelope.apps/server/src/models.apps/server/src/handlers with #[utoipa::path]..routes(routes!(your_handler)) in routes/mod.rs.just generate-api.apps/web/src/api/generated.ts.