Build the mental model
Axum is a Rust web framework built on the `tokio` runtime — a direct application of Lesson 19's async fundamentals into a production-shaped HTTP server, where each route handler is an `async fn` capable of concurrently handling hundreds of requests with just a handful of OS threads. Axum's core design idea is the "extractor" pattern, declaring exactly which part of a request a handler needs (a path parameter, a query string, a JSON body) through the handler function's parameter types — `Path(id): Path<u32>` auto-extracts and parses `id` from the URL path, and if parsing fails, Axum automatically responds with 400 Bad Request, so no manual validation code is needed. To serialize a response as JSON, add `#[derive(Serialize)]` to a struct, auto-implementing `serde::Serialize` — a pattern that gets you Lesson 14's trait implementation via compiler-generated code (a derive macro) instead of hand-written boilerplate. The route/handler concept is similar to a framework like Node.js/Express, but Axum's handlers are type-checked — writing the wrong extractor type is a compile error, not a runtime one — showing exactly the shape in which this site's backend logic could be complemented by a more memory-safe, type-safe implementation.
Connect it to a real scenario
Build the Tutorial Platform's `GET /lessons/{id}` endpoint as an `async fn get_lesson(Path(id): Path<u32>) -> Json<LessonMetadata>` handler — `id` auto-parses from the URL path (Axum sends an automatic 400 response for an invalid id), look up the metadata from `LessonStat` data (Lesson 21's project), and return it serialized with a `Json(...)` wrapper. Define the router with `Router::new().route("/lessons/{id}", get(get_lesson))` and run the server with `axum::serve(listener, router).await` inside a `#[tokio::main]` main function — Lesson 19's tokio runtime setup returns here. When a lesson id isn't found (putting Lesson 10's `Option<T>` back into practice), explicitly return an HTTP 404 status code — unwrapping the `None` case blindly instead could panic the whole server thread.
Try the working example
use axum::{extract::Path, http::StatusCode, routing::get, Json, Router};
use serde::Serialize;
#[derive(Serialize)]
struct LessonMetadata {
id: u32,
title: String,
word_count: u32,
}
async fn get_lesson(Path(id): Path<u32>) -> Result<Json<LessonMetadata>, StatusCode> {
let lessons = [
LessonMetadata { id: 1, title: "Ownership".into(), word_count: 620 },
LessonMetadata { id: 2, title: "Borrowing".into(), word_count: 540 },
];
lessons
.into_iter()
.find(|lesson| lesson.id == id)
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/lessons/{id}", get(get_lesson));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}A `GET /lessons/1` request returns the JSON response `{"id":1,"title":"Ownership","word_count":620}`; requesting a missing id like `GET /lessons/99` returns an HTTP 404 status instead.5-minute try-it
Add a route with a query parameter, `GET /lessons?difficulty=basic` — use the `Query<HashMap<String, String>>` extractor to read the `difficulty` value and return a matching lesson list (dummy data) as a JSON array.
One important caution
Calling `Option::unwrap()` when a lesson id isn't found in the data store and trying to return a Json response anyway — panicking on the None case can send the client an HTTP 500 (internal server error); an explicit 404 status should be returned instead.
Writing a handler function as `fn` (non-async) and then trying to call a database or file operation that needs `.await` — this is a compile error; every Axum handler must be `async fn`.
Axum Documentation — Rust