---
title: "Axum（アクサム）とは？Rust製Webフレームワークの使い方・テスト・Actix Web比較【axum 0.8.9】"
url: "https://www.issoh.co.jp/tech/details/7602/"
published: 2025-07-08
updated: 2026-10-24
categories: ["Rust"]
publisher: "株式会社一創"
---

# Axum（アクサム）とは？Rust製Webフレームワークの使い方・テスト・Actix Web比較【axum 0.8.9】

Axum（アクサム）は、非同期ランタイムTokioの開発チーム（GitHubの tokio-rs）が作っているRust製のWebフレームワークです。2026年10月24日時点の最新版は [crates.io](https://crates.io/crates/axum) で **axum 0.8.9**（2026年4月14日公開）。検索上位に残る入門記事の多くは0.6〜0.7が前提で、サーバーの起動方法とパスの書き方が今とは違います。本記事では0.8.9で実際にビルドして動かしたコードをもとに、Hello Worldからルーティング、エラー処理、ミドルウェア、本番前に入れる設定、ハンドラのテスト、旧版からの移行、Actix Web・Rocketとの選び分けまでを説明します。

## まとめ：axum 0.8の要点と選びどころ

- 読みは「アクサム」。Tokio・Hyper 1系・Towerの上に薄く載るフレームワークで、ミドルウェアはTowerのものをそのまま使えます。
- サーバーは `axum::serve(listener, app)` で起動します。`axum::Server` は0.7で削除済みです。
- パスパラメータは0.8から `/users/{id}` 形式です。旧来の `/users/:id` は起動時にpanicします。
- 本文を読む `Json` は最後の引数に置きます。引数の並びや `Send` でない値が原因のコンパイルエラーは、`#[axum::debug_handler]` を付けると原因の行が示されます。
- `Json` など、既定の本文制限を適用するエクストラクタの上限は2MiB（2,097,152バイト）です。認証は `route_layer`、停止は `with_graceful_shutdown` で組み、テストはサーバーを起動せず `oneshot` で書けます。
- 新規のAPIサーバーならaxumを第一候補にしてよいと判断します。0.x系のマイナー版更新では破壊的変更が入り得るため、利用中のAPIへの影響と移行作業の要否を確認してください。

## Axumの読み方と位置づけ

axumのREADMEに読み方の指定はありません。英語話者の発音は「ak-sm」に近く、Rust周辺の読みにくい単語を集めたコミュニティのメモでも「アクサム」が挙げられています。日本語の記事では「アクサム」と書けば通じます。

axumは自前でHTTPを実装していません。HTTPの処理はHyper、非同期I/OはTokio、ミドルウェアの仕組みはTowerの `Service` トレイトに任せ、axumはその上でルーティングと「リクエストを型に変換する層」を提供します。そのため、タイムアウトや圧縮、CORSのような機能はaxum独自ではなく、Tower向けに書かれたものを共有できます。クレートは `#![forbid(unsafe_code)]` でunsafeコードを禁止しており、ライセンスはMITです。公式のAPIリファレンスは [docs.rsのaxumページ](https://docs.rs/axum/latest/axum/)、動くサンプルはGitHubの [examplesディレクトリ](https://github.com/tokio-rs/axum/tree/main/examples) にまとまっています。

Tokioの非同期処理（`async`/`.await` と `Future`）に慣れていない場合は、先に[Rustのasync/await入門｜Futureとpollの仕組み、Tokioでの実行とよくあるエラー](/tech/details/8681/)を読んでおくと、以降のコードで引っかかりにくくなります。

axum本体と周辺クレートの現行版は次のとおりです（crates.ioの各クレートのページで2026年10月24日に確認）。

| クレート       | 最新版    | 公開日        | MSRV | 役割              |
| ---------- | ------ | ---------- | ---- | --------------- |
| axum       | 0.8.9  | 2026-04-14 | 1.80 | ルーティング・エクストラクタ  |
| axum-extra | 0.12.6 | 2026-04-14 | 1.80 | Cookie・型付きヘッダなど |
| tokio      | 1.54.1 | 2026-10-21 | 1.85 | 非同期ランタイム        |
| hyper      | 1.12.0 | 2026-10-06 | 1.63 | HTTPの実装         |
| tower-http | 0.7.1  | 2026-08-31 | 1.65 | ログ・CORS・タイムアウト  |

MSRVは最低限必要なRustのバージョンです。axum単体は1.80で足りますが、Tokioの1.54系はMSRVが1.85なので、`cargo add` で最新を入れると実質1.85以上が必要になります。古いツールチェインを固定しているCI環境では `rustup update` が先です。Rust自体のインストールがまだなら[Rust入門｜環境構築から所有権・基本文法、受託開発での採用判断まで](/tech/details/15373/)で手順を確認してください。

## axum 0.8の環境構築とHello World

プロジェクトを作り、依存クレートを `cargo add` で追加します。Tokioは `full` フィーチャーで入れておくと、`#[tokio::main]` と `TcpListener` がそのまま使えます。axumの `macros` フィーチャーは後述の `#[axum::debug_handler]` に使います。

```
cargo new axum-demo
cd axum-demo
cargo add axum@0.8.9 --features macros
cargo add tokio --features full
cargo add serde --features derive
cargo add serde_json
```

`src/main.rs` を次の内容にします。

```
use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(|| async { "Hello, World!" }));
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}
```

`cargo run` の後に `curl localhost:3000/` を叩くと `Hello, World!` が返ります。0.8では、Tokioの `TcpListener` を自分でbindし、それを `axum::serve` に渡す形が標準です。ポートの競合やアドレスの誤りはbindの段階でエラーになるため、起動失敗の原因を切り分けやすくなっています。

## Routerとハンドラによるルーティング

`Router::new().route(パス, メソッドルーター)` でルートを登録します。同じパスに複数のメソッドを割り当てるときは `get(list_users).post(create_user)` のようにつなげます。ハンドラは普通の `async fn` で、引数に「リクエストから取り出したいもの」を、戻り値に「レスポンスに変換できるもの」を書きます。登録していないメソッドで叩くと、axumが `405 Method Not Allowed` と `allow: GET,HEAD,POST` のようなヘッダを返します。

### パスパラメータの{id}構文と0.8での変更

axum 0.8.0（2025年1月1日公開）でルーティングに使うmatchitが0.8に上がり、パスパラメータの書き方が `/:id` から `/{id}` へ、ワイルドカードが `/*path` から `/{*path}` へ変わりました。旧構文のまま0.8で動かすと、ルート登録の時点で次のpanicが出て起動しません。

```
Path segments must not start with `:`. For capture groups, use `{capture}`.
```

コンパイルは通るのに起動で落ちるため、0.7の記事をコピーしたときに最も踏みやすい変更です。挙動が黙って変わるのを避けるため、あえてpanicさせる設計だと[axumのCHANGELOG](https://github.com/tokio-rs/axum/blob/main/axum/CHANGELOG.md)に書かれています。

### Path・Query・Jsonエクストラクタと引数の順序

次のコードは、ユーザー一覧をクエリ文字列 `?limit=10` で絞り込むGETと、JSON本文からユーザーを作るPOSTです。use宣言と型定義を含む `src/main.rs` の前半で、この後の「エラーハンドリング」「ミドルウェア」「本番前に入れる設定」「テスト」の各コードを同じファイルに続けて書くと、1本のアプリとしてビルドできます（Hello Worldとは別のファイル内容です）。構造体の読み書きには[Serdeの読み方は？Rust serde入門と#\[serde\]属性の使い方](/tech/details/5551/)で説明している `Deserialize`・`Serialize` を使います。

```
use std::{
    collections::HashMap,
    sync::{Arc, RwLock},
    time::Duration,
};

use axum::{
    extract::{DefaultBodyLimit, Path, Query, Request, State},
    http::{header, HeaderValue, Method, StatusCode},
    middleware::{self, Next},
    response::{IntoResponse, Response},
    routing::get,
    Json, Router,
};
use serde::{Deserialize, Serialize};
use tower_http::{cors::CorsLayer, timeout::TimeoutLayer, trace::TraceLayer};

#[derive(Clone, Default)]
struct AppState {
    users: Arc<RwLock<HashMap<u64, User>>>,
}

#[derive(Clone, Serialize)]
struct User {
    id: u64,
    name: String,
}

#[derive(Deserialize)]
struct CreateUser {
    name: String,
}

#[derive(Deserialize)]
struct ListParams {
    limit: Option<usize>,
}

// GET /api/users?limit=10
async fn list_users(
    State(state): State<AppState>,
    Query(params): Query<ListParams>,
) -> Json<Vec<User>> {
    let users = state.users.read().unwrap();
    let mut v: Vec<User> = users.values().cloned().collect();
    v.sort_by_key(|u| u.id);
    v.truncate(params.limit.unwrap_or(100));
    Json(v)
}

// POST /api/users  本文を消費する Json は最後の引数に置く
async fn create_user(
    State(state): State<AppState>,
    Json(input): Json<CreateUser>,
) -> Result<(StatusCode, Json<User>), AppError> {
    if input.name.trim().is_empty() {
        return Err(AppError::Invalid("name is empty"));
    }
    let mut users = state.users.write().unwrap();
    let id = users.len() as u64 + 1;
    let user = User { id, name: input.name };
    users.insert(id, user.clone());
    Ok((StatusCode::CREATED, Json(user)))
}
```

エクストラクタには順序の決まりがあります。`Json` や `String` のようにリクエスト本文を読み切るもの（`FromRequest` を実装する型）は1つだけ、しかも最後の引数にしか置けません。`Path`・`Query`・`State` のようにヘッダやURIだけを見るもの（`FromRequestParts`）は前に複数並べられます。通常の関数ハンドラが受け取れる引数は合計16個までです。順序を間違えたときのエラーの読み方は、後の「Handler未実装エラーの原因と読み方」で扱います。

### Stateとwith\_stateによる状態共有

DB接続プールや設定のように全ハンドラで使う値は、`Router::with_state(値)` で登録し、ハンドラ側で `State<AppState>` として受け取ります。状態の型は `Clone` が必要で、リクエストごとに複製されるため、共有する可変データは `Arc` で包みます。上のコードでは `Arc<RwLock<HashMap>>` でメモリ上に持っていますが、実務では `sqlx::PgPool` のような接続プール（それ自体が安価に複製できる）を入れます。SQLxのマイグレーション運用は[SQLx CLI（sqlx-cli）の使い方｜インストール・マイグレーション・オフラインモードをRustで実践](/tech/details/2901/)にまとめています。

状態の型が合っていないと、実行時ではなくコンパイル時に `Router` の型エラーになります。ハンドラが `AppState` の一部（たとえば設定だけ）を受け取りたいときは、`FromRef` を実装して `State<Config>` のように部分だけを取り出せます。

## エラーハンドリングとIntoResponse

axumのハンドラは `Result<T, E>` を返せます。`T` と `E` の両方が `IntoResponse` を実装していればよいので、アプリ固有のエラー型に `IntoResponse` を実装し、ステータスコードとJSONの形をそこで一度だけ決めるのが定石です。

```
enum AppError {
    NotFound(u64),
    Invalid(&'static str),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, msg) = match self {
            AppError::NotFound(id) => (StatusCode::NOT_FOUND, format!("user {id} not found")),
            AppError::Invalid(m) => (StatusCode::UNPROCESSABLE_ENTITY, m.to_string()),
        };
        (status, Json(serde_json::json!({ "error": msg }))).into_response()
    }
}

// GET /api/users/{id}
async fn get_user(
    State(state): State<AppState>,
    Path(id): Path<u64>,
) -> Result<Json<User>, AppError> {
    state.users.read().unwrap()
        .get(&id)
        .cloned()
        .map(Json)
        .ok_or(AppError::NotFound(id))
}
```

自分で書いたエラー以外に、エクストラクタが入力を受け付けなかったとき（リジェクション）はaxumが自動でエラーを返します。完成したアプリ（認証ヘッダ付き）にcurlで不正な入力を送ったときの実際の応答は次のとおりです。

| リクエスト                             | ステータス | 返したもの        |
| --------------------------------- | ----- | ------------ |
| GET /api/users/9（存在しない）           | 404   | 自作のAppError  |
| POST /api/users 本文 `{"name":" "}` | 422   | 自作のAppError  |
| GET /api/users/abc                | 400   | Pathのリジェクション |
| POST /api/users 本文にnameが無い        | 422   | Jsonのリジェクション |
| POST /api/users Content-Type無し    | 415   | Jsonのリジェクション |

リジェクションの本文は `` Failed to deserialize the JSON body into the target type: missing field `name` ... `` のようなプレーンテキストで、自作エラーのJSONと形がそろいません。APIの利用者にJSONで統一して返したい場合は、ハンドラの引数を `Result<Json<T>, JsonRejection>` で受けて自作エラーに変換するか、axum-extraの `WithRejection` で包みます。

## ミドルウェアとTower・tower-http

axumのミドルウェアはTowerの `Layer` です。アプリ固有の処理は `axum::middleware::from_fn` で普通の `async fn` として書き、ログやCORSのような汎用処理は [tower-http](https://docs.rs/tower-http/latest/tower%5Fhttp/) の既製品を重ねます。次は認証範囲を説明するため、固定のBearerトークンを照合する学習用ミドルウェアです。一致しなければ401を返します。本番では公開済みの固定値を使わず、トークンの発行・失効・検証方式に合わせた実装に置き換えてください。

```
// Authorization: Bearer secret が無ければ 401 を返す
async fn require_token(req: Request, next: Next) -> Result<Response, StatusCode> {
    let ok = req
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|v| v.to_str().ok())
        == Some("Bearer secret");
    if !ok {
        return Err(StatusCode::UNAUTHORIZED);
    }
    Ok(next.run(req).await)
}
```

重ねる順序には2つの決まりがあります。1つ目は、`.layer()` はそれより前に追加したルートだけを包み、後から `.route()` したルートにはかからないこと。2つ目は、後に書いたlayerほど外側になり、リクエスト時には先に実行されることです。次章の `app()` ではTraceLayerを最後に書いているので、全リクエストを最初に受け取ってログに残します。

TraceLayerは既定でDEBUGレベルのログを出すため、`tracing_subscriber::fmt::init()` の既定（INFO）のままでは何も表示されません。最大レベルをDEBUGに上げると、`request{method=POST uri=/api/users version=HTTP/1.1}` のようにリクエストごとのログが出ることを確認しています。

## 本番前に入れる設定（認証範囲・本文上限・CORS・停止処理）

ここまでの部品を組み立てた `app()` と `main` です。`cargo add tower-http --features trace,cors,timeout` と `cargo add tracing-subscriber` を追加してからビルドします。

```
fn app() -> Router {
    let users = Router::new()
        .route("/users", get(list_users).post(create_user))
        .route("/users/{id}", get(get_user))
        .route_layer(middleware::from_fn(require_token));

    let cors = CorsLayer::new()
        .allow_origin(HeaderValue::from_static("https://app.example.com"))
        .allow_methods([Method::GET, Method::POST])
        .allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE]);

    Router::new()
        .route("/health", get(|| async { "ok" }))
        .nest("/api", users)
        .fallback(|| async { (StatusCode::NOT_FOUND, "no route") })
        .layer(DefaultBodyLimit::max(64 * 1024))
        .layer(TimeoutLayer::with_status_code(
            StatusCode::REQUEST_TIMEOUT,
            Duration::from_secs(10),
        ))
        .layer(cors)
        .layer(TraceLayer::new_for_http())
        .with_state(AppState::default())
}

async fn shutdown_signal() {
    tokio::signal::ctrl_c().await.expect("failed to listen for ctrl-c");
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt()
        .with_max_level(tracing_subscriber::filter::LevelFilter::DEBUG)
        .init();
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    axum::serve(listener, app())
        .with_graceful_shutdown(shutdown_signal())
        .await
        .unwrap();
}
```

### route\_layerによる認証範囲の指定

認証のように途中で応答を返すミドルウェアは、`layer` ではなく `route_layer` で付けます。[公式ドキュメント](https://docs.rs/axum/0.8.9/axum/struct.Router.html#method.route%5Flayer)の説明どおり、`route_layer` はリクエストがルートに一致したときだけ動くため、存在しないパスへのアクセスが401に化けません。上のアプリでの実測は、トークン無しの `GET /api/users` が401、`GET /api/nope` はフォールバックの404、`/health` は認証なしで200でした。`route_layer` もそれより前に書いたルートにしか効かないので、ルートを全部書いてから最後に呼びます。

### 本文サイズの上限とタイムアウト

`Json`・`String`・`Bytes` などのエクストラクタは、既定で本文を2,097,152バイト（2MiB）までしか読みません。本文上限を既定値のままにした検証用アプリでは、2,097,140バイトのJSONは200、2,097,160バイトは `413 Payload Too Large` と `Failed to buffer the request body: length limit exceeded` を返しました。上限は [DefaultBodyLimit](https://docs.rs/axum/0.8.9/axum/extract/struct.DefaultBodyLimit.html) で変えられ、上のアプリでは64KiBに絞っています（nameをASCII文字で埋めた場合、60,000文字では201、70,000文字では413）。`DefaultBodyLimit` は上限を守るエクストラクタにだけ効き、本文をストリームとして直接読むハンドラには効きません。どの読み方でも上限をかけたいときは、tower-httpの `RequestBodyLimitLayer` を使います。

タイムアウトはtower-httpの `TimeoutLayer` で、時間内に応答が返らなければ指定したステータス（ここでは408）を返します。`TimeoutLayer::new` はtower-http 0.6.7で非推奨になったため、古い記事のコードをそのまま使うと警告が出ます。0.7系では `with_status_code` で返すステータスを明示してください。

### CORSの許可Originの明示設定

`CorsLayer` にOriginを1つだけ指定した場合、プリフライト（OPTIONS）への応答には常にそのOriginが入ります。実測では `Origin: https://evil.example` からのプリフライトにも `access-control-allow-origin: https://app.example.com` が返り、ブラウザ側でOriginの不一致として拒否されます。サーバーが拒否しているわけではないので、curlで確認するときはヘッダの値で判断してください。開発中に `CorsLayer::permissive()` で全部許可したまま本番に出すのは避けるべきです。

### グレースフルシャットダウンとHTTPS

`with_graceful_shutdown` に渡したFutureが完了すると、axumは新しい接続の受け付けを止め、処理中のリクエストが終わるのを待ってから `serve` を返します。上のアプリはCtrl+C（SIGINT）で正常に終了しました。コンテナで動かす場合はSIGTERMも待つように、`tokio::signal::unix::signal` を `tokio::select!` で並べるのが一般的です。

axum本体にはTLSの機能がありません。HTTPSはロードバランサーやリバースプロキシで終端するのが普通で、アプリ自身で受けたい場合はコミュニティが保守する [axum-server](https://docs.rs/axum-server/latest/axum%5Fserver/)（2026年10月時点の最新は0.8.0）を使い、`tls-rustls` または `tls-openssl` フィーチャーを有効にします。

## oneshotによるハンドラのテスト

axumの `Router` はTowerの `Service` なので、ポートを開かずにリクエストを1本流して応答を検査できます。[公式のtestingサンプル](https://github.com/tokio-rs/axum/tree/main/examples/testing)と同じ方式で、`tower` の `ServiceExt::oneshot` を使います。開発用の依存に `cargo add --dev tower --features util` と `cargo add --dev http-body-util` を追加し、`src/main.rs` の末尾に次を書きます。

```
#[cfg(test)]
mod tests {
    use super::*;
    use axum::body::Body;
    use http_body_util::BodyExt;
    use tower::ServiceExt; // oneshot

    #[tokio::test]
    async fn rejects_without_token() {
        let res = app()
            .oneshot(Request::get("/api/users").body(Body::empty()).unwrap())
            .await
            .unwrap();
        assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
    }

    #[tokio::test]
    async fn creates_user() {
        let req = Request::post("/api/users")
            .header(header::AUTHORIZATION, "Bearer secret")
            .header(header::CONTENT_TYPE, "application/json")
            .body(Body::from(r#"{"name":"alice"}"#))
            .unwrap();
        let res = app().oneshot(req).await.unwrap();
        assert_eq!(res.status(), StatusCode::CREATED);
        let body = res.into_body().collect().await.unwrap().to_bytes();
        assert_eq!(&body[..], br#"{"id":1,"name":"alice"}"#);
    }
}
```

`cargo test` の結果は `2 passed; 0 failed` で、2本合わせて0.01秒でした。`app()` を関数に切り出しておくと、本番の `main` とテストが同じルーター・同じミドルウェアの組み合わせを使います。`oneshot` は `Router` を消費するので、テストごとに `app()` を呼び直してください。状態を持ち越したいテストでは、`AppState` を外で作って `with_state` に渡す形にします。

## Handler未実装エラーの原因と読み方

axumで最も読みにくいのが、ハンドラを `get()` に渡した行で出る次のエラーです。

```
error[E0277]: the trait bound `fn(State<AppState>) -> impl Future<Output = String> {handler}: Handler<_, _>` is not satisfied
    = note: Consider using `#[axum::debug_handler]` to improve the error message
```

戻り値が `IntoResponse` を実装していない場合なども原因になりますが、ここでは次の3例を扱います。`Json` を `State` より前に置くなどの引数の順序違反、引数の型が `FromRequestParts`／`FromRequest` を実装していない、そして戻り値のFutureが `Send` でないことです。3つ目が最も気づきにくく、`std::sync::Mutex` のロックを持ったまま `.await` すると起きます。次は、アクセス数を `hits: Arc<Mutex<u64>>` として状態に持たせた別の小さな例です。

```
#[axum::debug_handler]
async fn handler(State(s): State<AppState>) -> String {
    let mut n = s.hits.lock().unwrap();
    *n += 1;
    tokio::time::sleep(std::time::Duration::from_millis(10)).await; // ロックを持ったままawait
    format!("{n}")
}
```

ハンドラに `#[axum::debug_handler]` を付けると、エラーが次のように具体的になります（axum 0.8.9・rustc 1.99.1で確認）。

```
error: future cannot be sent between threads safely
  = help: within `impl Future<Output = String>`, the trait `Send` is not implemented for `std::sync::MutexGuard<'_, u64>`
note: future is not `Send` as this value is used across an await
```

直し方は、ロックをブロックの中に閉じ込め、`.await` の前にガードを破棄することです。

```
    let n = {
        let mut g = s.hits.lock().unwrap();
        *g += 1;
        *g
    }; // ここでガードが破棄される
    tokio::time::sleep(std::time::Duration::from_millis(10)).await;
```

ロックを持ったまま待つ必要が本当にあるなら `tokio::sync::Mutex` を使います。ただしロックの保持時間が延びて待ちが増えるので、先に上のようにスコープを縮められないかを検討してください。`debug_handler` はエラーメッセージを改善するだけのマクロで、付けたままでも実行時の挙動は変わりません。

## 0.6・0.7時代の記事から移行するときの変更点

axumは1.0に達しておらず、0.xのマイナー版が上がるたびに破壊的変更が入ります。「axum 入門」で検索して上位に出る記事には0.6系を前提にしたものが残っているため、コードをそのまま写すとコンパイルエラーやpanicになります。

### 0.8での主な書き換え

主な差分をaxum公式CHANGELOGから拾うと次のとおりです。

| 旧版の書き方                         | 現行（0.8）の書き方                | 変わった版 |
| ------------------------------ | -------------------------- | ----- |
| axum::Server::bind(…)          | axum::serve(listener, app) | 0.7   |
| /users/:id                     | /users/{id}                | 0.8   |
| /assets/\*path                 | /assets/{\*path}           | 0.8   |
| #\[async\_trait\] 付きの独自エクストラクタ | async\_trait不要             | 0.8   |
| Hostエクストラクタ（axum本体）            | axum-extraへ移動              | 0.8   |
| Option<Path<T>> で全エラーを握りつぶす    | 多くの場合リジェクト                 | 0.8   |

```
// axum 0.6 以前（0.7 以降はコンパイルエラー）
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
    .serve(app.into_make_service())
    .await
    .unwrap();

// axum 0.7 以降
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();

// 0.7 までのパス構文（0.8 では起動時に panic）
.route("/users/:id", get(get_user))
.route("/assets/*path", get(assets))

// 0.8 以降
.route("/users/{id}", get(get_user))
.route("/assets/{*path}", get(assets))
```

`axum::Server` が消えたのは、土台のhyperが1.0で高水準APIの `hyper::Server` を削除したためです。0.7以降で `axum::Server` を書くと `` cannot find `Server` in `axum` `` というコンパイルエラーになります。独自エクストラクタで `#[async_trait]` を付けていた場合は、axum-core 0.5がトレイトの戻り値に `impl Future` を直接使う形へ切り替えたため、属性を外して `async fn from_request_parts` をそのまま書けば動きます。

### 0.9に向けた未リリースの破壊的変更

READMEには、mainブランチは0.9に向けた開発中で破壊的変更を含むと明記されています。GitHubには [0.9のマイルストーン](https://github.com/tokio-rs/axum/milestone/8)がありますが期限は設定されておらず、時期を尋ねた [Discussion #3488](https://github.com/tokio-rs/axum/discussions/3488) にもメンテナーが「There isn’t a timeline yet.」と答えています。2026年10月24日時点のCHANGELOGの未リリース欄には、次のような変更が並んでいます。

- `axum::serve` がhyper既定の `header_read_timeout` を適用する
- ネストしたRouterのフォールバックの合成方法が変わる
- `tower-log` が既定フィーチャーから外れる
- `with_graceful_shutdown` を使わない `serve` の戻り値の型が変わる

1.0前のaxumでも本番利用は広く行われています。Cargo.tomlでは `axum = "0.8"` のようにマイナー版までで止め、`Cargo.lock` をコミットし、0.9へ上げるときはCHANGELOGのbreaking行を読んでから上げる運用にしてください。前章のoneshotテストがあれば、上げた後の回帰確認がそのまま使えます。

## Actix Web・Rocketとの比較と選び方

RustのWebフレームワークで比較対象になるのはActix WebとRocketです。数値はcrates.ioとGitHubで2026年10月24日に確認したものです。

| 項目        | axum         | Actix Web         | Rocket     |
| --------- | ------------ | ----------------- | ---------- |
| 最新版       | 0.8.9        | 4.18.1            | 0.5.2      |
| 最新版の公開日   | 2026-04-14   | 2026-10-23        | 2026-10-23 |
| MSRV      | 1.80         | 1.89              | 1.89       |
| 直近90日のDL数 | 約1億4,178万    | 約1,247万           | 約228万      |
| GitHubスター | 約2.8万        | 約2.5万             | 約2.6万      |
| ルート定義     | Routerに関数を登録 | #\[get\]等の属性マクロも可 | 属性マクロ中心    |
| ランタイム     | Tokio        | actix-rt（Tokio上）  | Tokio      |
| ミドルウェア    | Towerを共有     | 独自のTransform      | 独自のFairing |

axumのダウンロード数が桁違いに多いのは、アプリから直接使われるだけでなく、他のクレートの依存として引き込まれているためです。たとえばgRPCのtonic 0.14.6は既定の `router` フィーチャーでaxumに依存します（[gRPC Rustの実装｜tonic 0.14のクレート分割とTLS・相互接続を解説](/tech/details/17215/)）。利用者数の比較としては割り引いて見る必要があります。それでも、Towerのミドルウェアをそのまま使えるため、タイムアウトや圧縮といった部品をaxum専用品を待たずに調達できる点はaxumの実利です。

Actix Webは2022年2月の4.0以降、メジャー版を上げずに4.xを出し続けています。axumのようにマイナー版ごとの書き換えが起きないことを重視するならActix Webが向いています。ランタイムのactix-rtは「Tokioベースのシングルスレッドランタイム」で、ワーカーごとに独立して動く設計です。Actix Web側の詳しい使い方は[Actix Web（actix-web）とは？Rustでの使い方とaxumとの違い【4.15.0で実行検証】](/column/details/3257/)を参照してください。

Rocketは2024年5月の0.5.1から約2年5か月リリースが途絶えていましたが、2026年10月23日に0.5.2が出ました。[0.5.2のCHANGELOG](https://github.com/rwf2/Rocket/blob/v0.5.2/CHANGELOG.md)の中身はrustls・tokio・hyperなど依存クレートの脆弱性対応とマルチパートの不具合修正が中心で、新機能の版ではありません。既存のRocketアプリはまず0.5.2へ上げるべきですが、新規案件で選ぶなら、リリース間隔の実績からaxumかActix Webを優先すると判断します。

axumを避けたほうがよい場面もあります。テンプレートエンジン・ORM・管理画面まで一式そろったフルスタックの枠組みを期待する場合、axumは何も同梱していないため、選定と組み合わせの作業がすべてチーム側に残ります。画面側までRustで書きたいなら[Leptosとは？Rust製Webフレームワークの使い方とYew・Dioxusとの違い【0.8.22対応】](/tech/details/9089/)や、同じtokio-rsが2026年7月に公開した[Topcoat（Rust）とは？tokio-rs発フルスタックフレームワークの実像](/tech/details/16377/)を比べてください。既存システムと連携するAPIをRustで作るかどうか、言語選定から相談したい場合は[API開発・システム連携](https://www.issoh.co.jp/service/system/api/)の支援もご利用いただけます。

## よくある質問

### axum::Serverが見つからないというコンパイルエラーの原因は？

axum 0.7で `axum::Server` が削除されたためです。`tokio::net::TcpListener::bind` でリスナーを作り、`axum::serve(listener, app)` に置き換えてください。0.6以前の記事のコードを使ったときに起きます。

### axum-extraには何が入っていますか？

axum本体に入れるほど汎用ではない機能を集めた公式の追加クレートです。0.12.6では、Cookieを扱う `CookieJar`（cookieフィーチャー）、型付きヘッダの `TypedHeader`（typed-headerフィーチャー）、型付きルーティング、マルチパート、Protocol Buffersなどを、使う機能ごとにフィーチャーで有効にします。0.8で本体から移った `Host` エクストラクタもここにあります。

### axumでHTMLやテンプレートを返すには？

文字列を `axum::response::Html` で包んで返すと、`Content-Type: text/html` 付きの応答になります。テンプレートはaxumに同梱されていないため、askamaやminijinjaなどのテンプレートエンジンでHTML文字列を作り、それを `Html` で返します。

### 静的ファイルを配信するには？

tower-httpの `ServeDir`（fsフィーチャー）を使い、`Router::new().nest_service("/assets", ServeDir::new("assets"))` のようにディレクトリをマウントします。tower-http 0.7.0からは `/index.html/` のようにファイル名の末尾にスラッシュを付けた要求は404になります。SPAで存在しないパスを `index.html` に落としたい場合は `ServeDir` のfallbackを組み合わせます。

### axum-serverとは何ですか？axumだけでHTTPSにできますか？

axum本体にTLS機能は無いため、アプリでHTTPSを直接受けるならaxum本体とは別にコミュニティが保守する `axum-server` を使います。最新の0.8.0は `tls-rustls` か `tls-openssl` フィーチャーでHTTPSに対応します。ロードバランサーやリバースプロキシでTLSを終端できる環境なら、axumは平文HTTPのまま `axum::serve` で動かすほうが構成は単純です。

## 関連記事

- [Rust入門｜環境構築から所有権・基本文法、受託開発での採用判断まで](/tech/details/15373/)
- [Rustのasync/await入門｜Futureとpollの仕組み、Tokioでの実行とよくあるエラー](/tech/details/8681/)
- [Serdeの読み方は？Rust serde入門と#\[serde\]属性の使い方](/tech/details/5551/)
- [SQLx CLI（sqlx-cli）の使い方｜インストール・マイグレーション・オフラインモードをRustで実践](/tech/details/2901/)
- [Actix Web（actix-web）とは？Rustでの使い方とaxumとの違い【4.15.0で実行検証】](/column/details/3257/)

---

出典: [Axum（アクサム）とは？Rust製Webフレームワークの使い方・テスト・Actix Web比較【axum 0.8.9】](<https://www.issoh.co.jp/tech/details/7602/>)（株式会社一創）
