Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. Schema as migration: move the lab’s CREATE TABLE into migrations/0001_create_notes.sql; at boot, run migrations before serving (sqlx migrate! macro, or the manual loop from M6 for the offline version).
  2. Share the pool through state: AppState { pool: SqlitePool } — M5’s state slot, M6’s pool. Handlers become async and take State<AppState>.
  3. Swap the in-memory Vec for queries: create_noteINSERT ... RETURNING id; get_notefetch_optionalok_or(ApiError::NotFound); list_notesfetch_all. Keep the validation (trim, length) — now the DB CHECK backs it up.
  4. Keep every M5 test: they call handlers directly; only the state constructor changes (temp-file pool instead of the in-memory Vec).
  5. 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.
  6. CI: the M7 file, unchanged. Green = done.

Evidence of done (the definition, not a feeling)

  • cargo test --workspace green: unit (handlers) + integration (oneshot roundtrip)
  • cargo clippy --all-targets -- -D warnings clean
  • Live transcript: server running, four curl calls 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: port m3-search’s MatchStrategy behind the notes table
  • Add crates/ingest: use m4-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.