axum を学び直す

に公開

前回は Debug トレイトの手動実装について書きました

今回から何回かに渡って axum crate について学び直そうと思います。

axum とは

Rust の Web アプリケーションフレームワークです。

https://github.com/tokio-rs/axum

今回はバージョン 0.8.7 を対象に調べていきます。既に 0.9 に向けて開発は進んでいるので、リポジトリを参照する場合はブランチを指定すると良いと思います。

https://docs.rs/axum/0.8.7/axum/index.html

README や crate ドキュメントには次のようにあります。

High-level features

  • Route requests to handlers with a macro-free API.
  • Declaratively parse requests using extractors.
  • Simple and predictable error handling model.
  • Generate responses with minimal boilerplate.
  • Take full advantage of the tower and tower-http ecosystem of middleware, services, and utilities.
  • リクエストをハンドラーにマクロなしの API でルーティングする
  • Extractor で宣言的にリクエストを解釈する
  • シンプルで予測可能なエラーハンドリング
  • 最小限のボイラープレートでレスポンスを生成する
  • tower crate と tower-http crate の middleware を採用する

実際に触ったことがあれば「そうだね」で終わる説明ですね。実際に触ってみて判断したほうが良さそうです。

crate ドキュメントの見出しからいくつか抜粋すると次のようになります。

  • Routing (Router struct)
  • Handlers (handler mod)
  • Extractors (extract mod)
  • Responses (response mod)
  • Error handling (error_handling mod)
  • Middleware (middleware mod)

この流れで深堀していこうと思いますが、今回はかんたんな例でひとまわりしようかと思います。

かんたんな例

https://github.com/bouzuya/rust-examples/tree/bbd2ff57733a7f48a95f8dce2c177fa466a960fd/axum4

cargo new axum4 など、適当にプロジェクトを作ります。

Cargo.toml

[package]
name = "axum4"
edition = "2024"
publish = false

[dependencies]
axum = "0.8.7"
serde_json = "1.0.145"
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] }

src/main.rs

async fn root() -> &'static str {
    "Hello, world!"
}

async fn get_user(
    axum::extract::Path(user_id): axum::extract::Path<u32>,
) -> Result<String, axum::http::StatusCode> {
    if user_id == 0 {
        Err(axum::http::StatusCode::NOT_FOUND)
    } else {
        Ok(format!("User {}", user_id))
    }
}

async fn create_user(
    axum::extract::Json(json): axum::extract::Json<serde_json::Value>,
) -> axum::response::Json<serde_json::Value> {
    // ...
    axum::response::Json(json)
}

#[tokio::main]
async fn main() {
    let app = axum::Router::new()
        .route("/", axum::routing::get(root))
        .route("/users", axum::routing::post(create_user))
        .route("/users/{user_id}", axum::routing::get(get_user));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

これを cargo run で動かして、 curl で呼び出すとこういう感じ。

$ curl -D - localhost:3000/
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 13
date: Thu, 28 Aug 2025 12:53:00 GMT

Hello, world!

$ curl -D - localhost:3000/users/0
HTTP/1.1 404 Not Found
content-length: 0
date: Thu, 28 Aug 2025 12:53:09 GMT


$ curl -D - localhost:3000/users/123
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 8
date: Thu, 28 Aug 2025 12:53:12 GMT

User 123

$ curl -D - --json '{"name":"bouzuya"}' localhost:3000/users
HTTP/1.1 200 OK
content-type: application/json
content-length: 18
date: Thu, 28 Aug 2025 12:53:31 GMT

{"name":"bouzuya"}

例の説明

この記事では import を避けています。これはエディタなどの補助がない場合、型の区別がつきづらいためです。 import すると、たとえば axum::extract::Path(id)Path(id) のように短く書けます。

Routing

Router struct はルーティングを設定する構造体です。

let app = axum::Router::new()
    .route("/", axum::routing::get(root))
    .route("/users", axum::routing::post(create_user))
    .route("/users/{user_id}", axum::routing::get(get_user));

基本は、パスパターンとメソッドとハンドラーを指定します。

Handler 以外に tower::Service を実装したものへのルーティングもできます。

Handlers

Handler はリクエストを処理してレスポンスを返します。

async fn root() -> &'static str {
    "Hello, world!"
}

Extractors

Extractor は Handler の引数に指定し、リクエストからデータを抽出します。パス・クエリ文字列・ヘッダー・ボディなどから指定した形式で読み取ります。

async fn get_user(
    axum::extract::Path(user_id): axum::extract::Path<u32>,
) -> Result<String, axum::http::StatusCode> {
    if user_id == 0 {
        Err(axum::http::StatusCode::NOT_FOUND)
    } else {
        Ok(format!("User {}", user_id))
    }
}

Responses

Response は Handler の返り値に指定し、指定した形式でレスポンスを返します。

IntoResponse トレイトを実装していれば良いので、前述の &'static str のようなものも返せます。

async fn create_user(
    axum::extract::Json(json): axum::extract::Json<serde_json::Value>,
) -> axum::response::Json<serde_json::Value> {
    // ...
    axum::response::Json(json)
}

Error handling

エラーハンドリングはレスポンスの話の一部とも言えます。

IntoResponse を実装し Err 側の値に IntoResponse を実装しておけば良いだけです。

独自のエラーの定義もできます。

Middleware

axum::servelistenermake_service を引数に取ります。 make_servicetower::Service を取ります。

Router も実は tower::Service を実装しています。

#[tokio::main]
async fn main() {
    let app = axum::Router::new()
        .route("/", axum::routing::get(root))
        .route("/users", axum::routing::post(create_user))
        .route("/users/{user_id}", axum::routing::get(get_user));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

おわりに

今回はとりあえず axum の crate ドキュメントの見出し (構成要素) と、かんたんな例を示しました。

次回以降は各要素を深堀してみようと思います。

参考

GitHubで編集を提案
ドクターメイト

Discussion