Rust×Axumで作るREST API ② ルーティング・ミドルウェアの実装

rust×Axumで作るREST API

はじめに

本記事では、Rust Axum ルーティング ミドルウェアをテーマに、複数ルートの定義からロギングミドルウェアの実装までを解説します。シリーズの第2回として、第1回で作った最小構成の API を、より実践的な形に育てていく内容です。

本シリーズで予定している内容は以下の通りです。

第1回をまだ読んでいない方は、先にこちらでサーバーを起動できる状態にしておいてください。

Rust×Axumで作るREST API ① セットアップ〜Hello Worldまで
はじめに本記事では、Rust と Axum を組み合わせて REST API を構築するシリーズの第一歩として、「Rust Axum セットアップと Hello World の表示」について解説します...

まずは、今回実装するエンドポイントを整理します。第1回の /hello に加え、以下のルートを追加していきます。

  • GET /health:サーバーの稼働確認用エンドポイント
  • GET /users/:id:パスパラメータでユーザーIDを受け取るエンドポイント
  • GET /api/items:プレフィックス /api を持つ複数ルートのグルーピング例

加えて、すべてのリクエストにロギングミドルウェアを適用し、アクセスログをコンソールに出力できるようにします。これにより、開発中の動作確認やデバッグが格段にしやすくなります。

依存クレートの追加

まずは、ロギング用のクレートを Cargo.toml に追加します。Axum 自体には変更はありません。


[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.5", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

追加した3つのクレートの役割は以下の通りです。

  • tower-http:Axum と組み合わせて使えるミドルウェア集。今回はその中の TraceLayer を利用します
  • tracing:構造化ログを扱うための基盤クレート
  • tracing-subscribertracing が出力するログを実際にコンソールへ書き出すサブスクライバ

なお、tower-http の詳細は公式ドキュメントを参考にしてください。

tower_http - Rust
`async fn(HttpRequest) -> Result`

複数ルートを定義する

次は、ルーターに複数のエンドポイントを定義していきます。第1回の main.rs を、以下のように書き換えます。


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

#[tokio::main]
async fn main() {
    // 複数のルートをチェーンで定義
    let app = Router::new()
        .route("/hello", get(hello))
        .route("/health", get(health));

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    println!("サーバーを起動しました: http://127.0.0.1:3000");
    axum::serve(listener, app).await.unwrap();
}

// /hello ハンドラ
async fn hello() -> &'static str {
    "Hello, World!"
}

// /health ハンドラ:監視ツールからの稼働確認に使う
async fn health() -> &'static str {
    "OK"
}

このように、Router::new().route(...) をチェーンするだけでルートを増やせます。ルートが増えてきたら、後述するグルーピングを使って整理します。

パスパラメータを受け取る

続いて、/users/:id のようにパスの一部を変数として受け取るルートを追加します。Axum では Path 抽出器を使うことで、パスパラメータを型安全に取り出せます。

以下の処理を src/main.rs に追記します。


use axum::{extract::Path, routing::get, Router};

// /users/:id ハンドラ
async fn get_user(Path(id): Path<u32>) -> String {
    // id は自動で u32 にパースされる
    format!("ユーザーID: {} の情報を返します", id)
}

そして、main 関数内のルーター定義に1行追加します。


let app = Router::new()
    .route("/hello", get(hello))
    .route("/health", get(health))
    .route("/users/:id", get(get_user));

これで、/users/123 にアクセスすると「ユーザーID: 123 の情報を返します」と返るようになります。なお、:id 部分が数値に変換できない場合、Axum が自動で 400 Bad Request を返してくれるため、自前でバリデーションコードを書く必要はありません。

ルートをグループ化する

ルートが増えてくると、関連するエンドポイントをまとめたくなります。Axum では Router::nest を使うことで、共通プレフィックスを持つルートをサブルーターとして切り出せます。

以下のように、/api 配下のルートをまとめます。


// /api 以下のルートを別ルーターで定義
fn api_routes() -> Router {
    Router::new()
        .route("/items", get(list_items))
        .route("/items/:id", get(get_item))
}

async fn list_items() -> &'static str {
    "アイテム一覧を返します"
}

async fn get_item(Path(id): Path<u32>) -> String {
    format!("アイテムID: {} の詳細を返します", id)
}

そして、main 関数のルーターに nest でぶら下げます。


let app = Router::new()
    .route("/hello", get(hello))
    .route("/health", get(health))
    .route("/users/:id", get(get_user))
    .nest("/api", api_routes());

これで、/api/items/api/items/:id の2つのエンドポイントが、サブルーター経由で利用できるようになります。ルート定義を機能ごとにファイル分割したくなったときも、この nest が起点になります。

ロギングミドルウェアを組み込む

最後に、すべてのリクエストをログ出力するミドルウェアを追加します。Axum におけるミドルウェアは、tower という抽象に従って実装されており、Router.layer() メソッドで適用できます。

まず、main の冒頭で tracing-subscriber を初期化し、TraceLayer.layer() でルーター全体に適用します。

完成形の src/main.rs は以下の通りです。


use axum::{extract::Path, routing::get, Router};
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

#[tokio::main]
async fn main() {
    // ロガーの初期化(環境変数 RUST_LOG に従う。未設定時は info レベル)
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "axum_rest_api=debug,tower_http=debug".into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    let app = Router::new()
        .route("/hello", get(hello))
        .route("/health", get(health))
        .route("/users/:id", get(get_user))
        .nest("/api", api_routes())
        // すべてのリクエストにアクセスログを付与する
        .layer(TraceLayer::new_for_http());

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    tracing::info!("サーバーを起動しました: http://127.0.0.1:3000");
    axum::serve(listener, app).await.unwrap();
}

// ... ハンドラ定義は省略

これで、Rust Axum ルーティング ミドルウェアの基本構成が整いました。cargo run でサーバーを起動し、curl http://127.0.0.1:3000/users/1 などを実行すると、コンソールに以下のようなログが流れます。


2026-05-09T01:23:45.678Z DEBUG request{method=GET uri=/users/1 version=HTTP/1.1}: tower_http::trace::on_request: started processing request
2026-05-09T01:23:45.679Z DEBUG request{method=GET uri=/users/1 version=HTTP/1.1}: tower_http::trace::on_response: finished processing request latency=0 ms status=200

リクエストごとにメソッド・パス・処理時間・ステータスコードが出力されるため、開発中のデバッグや本番のアクセス解析にそのまま活用できます。

動作確認

実装が一通り終わったので、追加したエンドポイントを順に確認していきます。まずは、サーバーを起動します。


cargo run

次に、別のターミナルから各エンドポイントへアクセスします。


curl http://127.0.0.1:3000/health
curl http://127.0.0.1:3000/users/42
curl http://127.0.0.1:3000/api/items
curl http://127.0.0.1:3000/api/items/7

それぞれ、以下のレスポンスが返れば成功です。


OK
ユーザーID: 42 の情報を返します
アイテム一覧を返します
アイテムID: 7 の詳細を返します

サーバー側のターミナルにもリクエストごとのアクセスログが流れているはずです。両方が確認できれば、ルーティングとミドルウェアが正しく動いていることが分かります。

まとめ

今回は、Rust Axum ルーティング ミドルウェアをテーマに、複数ルートの定義・パスパラメータ・サブルーターによるグルーピング・TraceLayer によるロギングまでを実装しました。これにより、シリーズの API が実用的な構造に近づき、リクエストの流れも目で追える状態になりました。

次回は、SeaORM を組み合わせて DB 接続層を実装し、CRUD エンドポイントを完成させていきます。ORM の基礎を先に押さえておきたい方は、Rust×SeaORMで学ぶORM入門も合わせてご覧ください。

Rust×SeaORMで学ぶORM入門:セットアップと基本操作を解説
はじめに今回は、Rustの人気ORMパッケージであるSeaORMをインストールから、簡単なデータベース操作までを解説します。なお、今回紹介するソースコードはこちらにあります。また、Rustのデスクトッ...
タイトルとURLをコピーしました