Capstone: Assemble the Mini Service
Goal: combine M5 + M6 into one small but complete service: an Axum notes API backed by sqlx/SQLite, structured as a workspace — a miniature of the real fleet.
The target architecture
notes-service/
├── Cargo.toml # [workspace] members = ["server"]
└── server/
├── Cargo.toml # axum, sqlx, serde, tokio
├── migrations/ # 0001_create_notes.sql
└── src/
├── main.rs # pool setup + router + serve
├── errors.rs # AppError + IntoResponse (M2/M5 pattern)
├── routes.rs # handlers (M5 pattern)
└── db.rs # typed sqlx queries (M6 pattern)
Why a workspace for one crate: it is the seam where the capstone grows — add
crates/search (M3 engine behind a trait) or crates/ingest (M4 batch loader)
without restructuring. Real repos (uteke, sinau) all grow this way.
The assembly steps
- Schema as migration: move the lab’s
CREATE TABLEintomigrations/0001_create_notes.sql; at boot, run migrations before serving (sqlxmigrate!macro, or the manual loop from M6 for the offline version). - Share the pool through state:
AppState { pool: SqlitePool }— M5’s state slot, M6’s pool. Handlers become async and takeState<AppState>. - Swap the in-memory Vec for queries:
create_note→INSERT ... RETURNING id;get_note→fetch_optional→ok_or(ApiError::NotFound);list_notes→fetch_all. Keep the validation (trim, length) — now the DB CHECK backs it up. - Keep every M5 test: they call handlers directly; only the state constructor changes (temp-file pool instead of the in-memory Vec).
- Add the integration test: boot the router with a temp DB,
Router::oneshot()a POST + GET through the whole app — that is the “Tested means live” bar. - CI: the M7 file, unchanged. Green = done.
Evidence of done (the definition, not a feeling)
cargo test --workspacegreen: unit (handlers) + integration (oneshot roundtrip)cargo clippy --all-targets -- -D warningsclean- Live transcript: server running, four
curlcalls with correct status codes (201 → GET 200 → GET 999 → 404) - Repo with the CI file; PR merged via the checklist
Where to go next
- Add
crates/search: portm3-search’sMatchStrategybehind the notes table - Add
crates/ingest: usem4-batch’s semaphore+JoinSet to bulk-load notes - Point sqlx at Postgres: only the connect options change — that is the point
You have now built, in order: the mental model (ownership), failure handling (errors), abstraction (traits), concurrency (async), transport (axum), storage (sqlx), and the delivery pipeline (CI). That is the full loop every CodeCora service runs in.