22 人が閲覧(直近 30 日) Rust

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

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

Axum(アクサム)は、非同期ランタイムTokioの開発チーム(GitHubの tokio-rs)が作っているRust製のWebフレームワークです。2026年10月24日時点の最新版は crates.io で 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ページ、動くサンプルはGitHubの examplesディレクトリ にまとまっています。

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

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入門|環境構築から所有権・基本文法、受託開発での採用判断までで手順を確認してください。

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 [email protected] --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に書かれています。

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

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

状態の型が合っていないと、実行時ではなくコンパイル時に 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 の既製品を重ねます。次は認証範囲を説明するため、固定の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 で付けます。公式ドキュメントの説明どおり、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 で変えられ、上のアプリでは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(2026年10月時点の最新は0.8.0)を使い、tls-rustls または tls-openssl フィーチャーを有効にします。

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

axumの Router はTowerの Service なので、ポートを開かずにリクエストを1本流して応答を検査できます。公式の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のマイルストーンがありますが期限は設定されておらず、時期を尋ねた Discussion #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・相互接続を解説)。利用者数の比較としては割り引いて見る必要があります。それでも、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で実行検証】を参照してください。

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

axumを避けたほうがよい場面もあります。テンプレートエンジン・ORM・管理画面まで一式そろったフルスタックの枠組みを期待する場合、axumは何も同梱していないため、選定と組み合わせの作業がすべてチーム側に残ります。画面側までRustで書きたいならLeptosとは?Rust製Webフレームワークの使い方とYew・Dioxusとの違い【0.8.22対応】や、同じtokio-rsが2026年7月に公開したTopcoat(Rust)とは?tokio-rs発フルスタックフレームワークの実像を比べてください。既存システムと連携するAPIをRustで作るかどうか、言語選定から相談したい場合は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 で動かすほうが構成は単純です。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.29 テックブログ ソニー生命の情報漏洩38.5万件|ログインできない理由、再開状況と窓口の電話番号
  2. 2026.11.03 テックブログ ノジマ不正アクセスで約700万件漏えい:対象確認・窓口と乗っ取り注文を止める実装
  3. 2026.11.02 テックブログ サントリーの不正アクセスで約10万人分漏えい|M365のファイル大量ダウンロードを監査ログで調べる手順
  4. 2026.11.03 テックブログ クラブツーリズム不正アクセス:約432万件の対象確認と偽メールの見分け方・流出検知の設計
  5. 2026.11.02 テックブログ アララの不正アクセス:メール約7,700万件の持ち出しと契約企業が行うAPIキー再設定・代替送信・連名報告

RELATED POSTS 関連記事

目次