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 で動かすほうが構成は単純です。