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

HTTP Services with Axum

Goal: you can build a JSON REST API with axum: nested routes, extractors, shared state, and one error enum rendered to status codes via IntoResponse.

The moving parts

PieceWhat it isIn the lab
Routerpath → handler table, nestable/healthz, /notes, /notes/{id}
Extractortyped argument a handler receivesState, Path, Json
Return valueimpl IntoResponseJson<T>, (StatusCode, Json<T>), or your error type
with_statewires shared state into every handlerAppState { notes: Arc<Mutex<Vec<_>>> }

Handlers are plain functions — the same function works under the router and in a #[tokio::test] with no HTTP at all. That is why the lab tests call create_note(...) directly: transport-free unit tests on the exact production code path.

State: cheap clones, inner mutability

#![allow(unused)]
fn main() {
#[derive(Clone)]
struct AppState { notes: Arc<Mutex<Vec<Note>>> }
// Router::with_state(state) — every request gets a clone (Arc bump, not a copy)
}

Mutex::lock() is std (sync) and fine for short critical sections; under real concurrency you’d reach for dashmap or a sqlx pool (M6). The rule that matters: never hold a lock across an .await.

Errors: the M2 pattern on the big stage

#![allow(unused)]
fn main() {
enum ApiError { NotFound, BadRequest(String) }

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            ApiError::NotFound => (StatusCode::NOT_FOUND, "note not found".into()),
            ApiError::BadRequest(m) => (StatusCode::BAD_REQUEST, m),
        };
        (status, Json(serde_json::json!({ "error": message }))).into_response()
    }
}
type ApiResult<T> = Result<T, ApiError>;   // handlers just `?` or return Err
}

Business logic returns Err(ApiError::BadRequest(...)); the boundary renders it. Validation lives at the edge (trim, length cap) — and the 400s carry a reason.

Route syntax

axum 0.8 path params: "/notes/{id}" (curly braces replaced the old :id syntax — old tutorials lie). One handler can serve several methods: get(list).post(create).

Lab

labs/m5-notes-apicargo run -p m5-notes-api, then:

curl -s localhost:3000/healthz
curl -s localhost:3000/notes
curl -s -X POST localhost:3000/notes -H 'content-type: application/json' -d '{"title":"ship it"}'
curl -s localhost:3000/notes/1

Tests (4): roundtrip with trim, monotonic ids, empty title → 400 value, missing → 404.

Done when: tests green, and you can curl the running server through all four calls — plus explain why the handler tests need no HTTP at all.