はい、承知いたしました。Axum Rustを使ったモダンなWebアプリケーション開発の第一歩に関する詳細な記事を、約5000語で記述します。記事の内容を直接ここに表示します。
Axum Rust:モダンなWebアプリケーション開発の第一歩
はじめに
Webアプリケーション開発は、現代ソフトウェア開発の中核をなす分野です。日々進化する技術スタックの中で、安全性、パフォーマンス、そして開発効率は常に追求される目標となっています。Rust言語は、そのユニークな特性、特にメモリ安全性保証とゼロコスト抽象化により、これらの目標を達成するための強力な選択肢として注目を集めています。
Rustは、ガベージコレクションを持たずにメモリ安全性を保証し、CやC++に匹敵する実行速度を実現します。この特性は、特に高い並行性と低レイテンシが求められるWebサービスやAPIの開発において大きな強みとなります。しかし、Rustはその学習曲線が急峻であるとも言われます。所有権システム、借用チェッカー、ライフタイムといった概念は、Rust独自の考え方であり、習得には時間を要します。
それでもなお、一度Rustの哲学を理解すれば、その強力な型システムとコンパイラのチェックによって、実行時エラーの多くをコンパイル時に防ぐことができ、より堅牢で信頼性の高いコードを書くことが可能になります。Web開発においても、これはセキュリティの向上やランタイムクラッシュの減少に直結します。
RustでWebアプリケーションを開発するためのフレームワークはいくつか存在します。代表的なものとしては、Actix-web、Rocket、そしてAxumが挙げられます。
- Actix-web: 最も成熟しており、長年の開発実績を持つフレームワークの一つです。高いパフォーマンスに定評があります。アクターシステムに基づいている点が特徴です。
- Rocket: マクロを多用しており、非常に宣言的なスタイルで書けることが特徴です。開発体験が良いという評価もありますが、Nightly Rustに依存することが多い時期がありました(最近はStable対応も進んでいます)。
- Axum: Tokioエコシステムの一部として開発されており、タワー(Tower)ライブラリを基盤としています。非同期ランタイムTokioとの親和性が高く、ミドルウェアの柔軟性や、ExtractorとResponderを組み合わせたハンドラの表現力が特徴です。まだ比較的新しいフレームワークですが、TokioやTowerといった広く使われているコンポーネントの上に構築されているため、安定性やエコシステムとの連携に優れています。
この記事では、数あるRustのWebフレームワークの中からAxumに焦点を当て、その特徴、使い方、そしてモダンなWebアプリケーションを構築するための基本的なステップを詳細に解説します。Axumは、その設計思想と柔軟性から、特にマイクロサービスや高性能なAPIバックエンドを構築する上で魅力的な選択肢となっています。TokioやTowerといった他のRustエコシステムの重要なコンポーネントについても理解を深めることで、RustによるWeb開発の全体像を掴むことができるでしょう。
この記事を読むことで、あなたは以下のことを習得できます。
- Axumの基本的な構成要素と哲学。
- Axumを使った簡単なWebサーバーの構築方法。
- ルーティング、パスパラメータ、クエリパラメータの扱い方。
- JSONリクエスト/レスポンスの処理方法。
- アプリケーション状態の管理方法。
- ミドルウェアの利用方法。
- 基本的なエラーハンドリング。
- Axumアプリケーションのテスト方法。
RustでのWeb開発は、確かに学びが多い道のりです。しかし、Axumはその道のりをよりスムーズにし、Rustの持つパワーをWebアプリケーションとして最大限に引き出すための優れたツールとなるでしょう。さあ、モダンなRust Web開発の第一歩を踏み出しましょう。
Axumの基礎概念
Axumは、以下の主要なコンポーネントと設計哲学の上に構築されています。
- Tokioエコシステム: Axumは非同期ランタイムとしてTokioを全面的に採用しています。非同期処理の実行、タイマー、I/O操作など、Webサーバーが必要とする非同期基盤は全てTokioが提供します。これにより、非同期処理を効率的に、かつ安全に扱うことができます。TokioはRustで最も広く使われている非同期ランタイムであり、その豊富なツール群やライブラリとの連携が容易です。
- Towerライブラリ: Axumのもう一つの重要な基盤がTowerライブラリです。Towerは、ネットワークサービスの構成要素である
Service
トレイトを核とした非同期処理のための抽象化レイヤーを提供します。Axumのハンドラやミドルウェアは、このService
トレイトに基づいて実装されています。これにより、異なるサービス(例えばHTTPサーバーとgRPCサーバー)間で共通のロジック(ミドルウェアなど)を共有したり、サービスを組み合わせたりすることが容易になります。ミドルウェアはTowerのLayer
トレイトとして表現され、リクエスト処理パイプラインに機能を追加するために使用されます。 - Hyper HTTPライブラリ: AxumはHTTPプロトコル処理のためにHyperライブラリを使用しています。HyperはRustで書かれた高速かつ正しいHTTP実装であり、多くのRust Webフレームワークで利用されています。AxumはHyperの上に構築されることで、低レベルなHTTPプロトコルの詳細から解放され、アプリケーションロジックに集中できます。
- ExtractorとResponder: Axumのハンドラは、リクエストからデータを取り出すための
Extractor
と、レスポンスを生成するためのResponder
という概念に大きく依存しています。ハンドラ関数の引数は、FromRequestParts
またはFromRequestBody
トレイトを実装するあらゆる型にすることができ、これらの型はリクエストの特定の要素(パスパラメータ、クエリパラメータ、JSONボディ、ヘッダーなど)を抽出します。同様に、ハンドラ関数の戻り値は、IntoResponse
トレイトを実装するあらゆる型にすることができ、これらの型はHTTPレスポンスに変換されます。このExtractor/Responderパターンにより、ハンドラのシグネチャが非常に表現豊かになり、ボイラープレートコードを削減できます。 - タプルによるハンドラ合成: 複数のExtractorをハンドラ関数の引数としてタプル形式で渡すことができます。Axumは自動的にこれらのExtractorを実行し、それぞれの抽出結果をハンドラに渡します。この機能により、複数の情報をリクエストから簡単に取り出すことができます。
- Type Stateパターン: ルーターの設定において、特定のミドルウェア(例えば
CorsLayer
)が適用されたルーターは異なる型となり、次に適用できるミドルウェアや設定が制限されるというType Stateパターンが一部で利用されています。これにより、設定の順序に関する論理的な誤りをコンパイル時に検出できます。
これらの概念を理解することで、Axumがどのように動作し、その柔軟性や表現力がどこから来ているのかが見えてきます。特にTokioとTowerは、Axumだけでなく多くのRust非同期ライブラリの基盤となっているため、これらの理解はRustによる非同期プログラミング全般に役立ちます。
環境構築と最初のAxumアプリケーション
Axumアプリケーションを開発するための環境構築から始めましょう。
Rustのインストール
Rustをインストールしていない場合は、rustup
という公式のツールチェインインストーラーを使ってインストールします。
bash
curl --proto '=https' --tlsv1.2 -sSF https://sh.rustup.rs | sh
このコマンドを実行すると、Rustコンパイラ(rustc
)、Cargo(Rustのビルドシステムとパッケージマネージャー)、rustup自身がインストールされます。インストールが完了したら、シェルの設定ファイルを読み込むか、新しいターミナルを開いてください。
インストールが成功したか確認します。
bash
rustc --version
cargo --version
最新のStableバージョンがインストールされていればOKです。
新しいCargoプロジェクトの作成
次に、Cargoを使って新しいRustプロジェクトを作成します。ここでは、単純なWebアプリケーションを作成するので、実行可能ファイルを含むプロジェクトを作成します。
bash
cargo new my-axum-app
cd my-axum-app
これにより、my-axum-app
というディレクトリが作成され、その中にCargo.toml
とsrc/main.rs
が生成されます。
必要な依存関係の追加
Axumアプリケーションを構築するために、いくつかのクレート(Rustのライブラリ)を依存関係としてCargo.toml
に追加する必要があります。
my-axum-app/Cargo.toml
を開き、[dependencies]
セクションに以下を追加します。
“`toml
[dependencies]
axum = “0.7” # Axumフレームワーク本体
tokio = { version = “1”, features = [“full”] } # 非同期ランタイム(フル機能有効化)
tower = “0.4” # Towerライブラリ(主にServiceトレイトのインポートに必要)
tower-http = { version = “0.5”, features = [“full”] } # Towerベースの便利なミドルウェア集
serde = { version = “1”, features = [“derive”] } # 構造体のシリアライズ/デシリアライズ
serde_json = “1” # JSONのシリアライズ/デシリアライズ
ロギング(任意だが強く推奨)
tracing = “0.1”
tracing-subscriber = { version = “0.3”, features = [“env-filter”] }
“`
axum
: Axumフレームワーク本体です。バージョンを指定します。ここでは例として”0.7″系を指定していますが、最新版を使用してください。tokio
: 非同期ランタイムです。features = ["full"]
は、必要な機能(タイマー、I/O、マルチスレッディングなど)を全て有効にします。プロダクションでは必要な機能だけを有効にすることが推奨されますが、開発中は"full"
が便利です。tower
: Towerクレート自体を直接使うことは少ないかもしれませんが、Serviceトレイトなどをインポートするために必要になることがあります。tower-http
: Towerベースの便利なHTTP関連ミドルウェアを集めたクレートです。ロギング、CORS、静的ファイル配信など、多くの一般的な機能が提供されています。features = ["full"]
で全ての機能を有効にしています。serde
,serde_json
: Rustの構造体をJSON形式に変換(シリアライズ)したり、JSONから構造体に変換(デシリアライズ)したりするために使われます。特にJSON APIを構築する際に必須です。serde
のderive
フィーチャーは、構造体に#[derive(Serialize, Deserialize)]
アトリビュートを付加することで、自動的にシリアライズ/デシリアライズのコードを生成するために必要です。tracing
,tracing-subscriber
: 構造化されたログ/トレースを出力するためのライブラリです。Webアプリケーションの状態を把握し、デバッグするために非常に役立ちます。tracing-subscriber
は、tracing
からの出力をどのように表示するかを設定します。env-filter
フィーチャーは、環境変数(例:RUST_LOG=info
)を使ってログレベルを制御できるようにします。
依存関係を追加したら、Cargoがそれらをダウンロードしてビルドできるようにします。
bash
cargo build
これでプロジェクトの基本的なセットアップは完了です。
基本的なAxumアプリケーション
src/main.rs
を編集して、最初のAxumアプリケーションを作成します。最もシンプルな例として、”Hello, world!”を返すエンドポイントを持つWebサーバーを実装します。
“`rust
// 非同期ランタイムとしてTokioを使用することを宣言
[tokio::main]
async fn main() {
// ロギングの設定 (任意だが推奨)
tracing_subscriber::fmt()
.with_env_filter(“info”) // RUST_LOG環境変数でログレベルを制御
.init();
// ルーティングを設定
// "/" パスへのGETリクエストに対して、handler関数を実行する
let app = axum::Router::new().route("/", axum::routing::get(handler));
// サーバーを起動
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
tracing::info!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
// HTTPリクエストを処理する非同期関数
async fn handler() -> String {
“Hello, world!”.to_string()
}
“`
コードの解説です。
#[tokio::main]
: main関数を非同期関数 (async fn
) として実行できるようにするためのTokioマクロです。これにより、Tokioランタイムが設定され、main
関数内で.await
を使うことができます。tracing_subscriber::fmt().with_env_filter("info").init()
:tracing
クレートを使った基本的なロギング設定です。環境変数RUST_LOG
が設定されていればそれを使用し、設定されていなければデフォルトでinfo
レベル以上のログを出力するようにします。init()
で設定を有効化します。axum::Router::new()
: 新しいルーターを作成します。ルーターはどのパスへのリクエストをどのハンドラで処理するかを定義します。.route("/", axum::routing::get(handler))
: ルーターに新しいルートを追加します。- 第一引数
"/"
は、マッチさせたいHTTPパスです。 - 第二引数
axum::routing::get(handler)
は、そのパスへのリクエストのうち、GETメソッドの場合に実行するハンドラを指定します。axum::routing
にはget
,post
,put
,delete
などのメソッドに対応した関数があります。 handler
は、このルートへのリクエストを実際に処理する非同期関数です。
- 第一引数
async fn handler() -> String
: ハンドラ関数です。HTTPリクエストを処理するロジックを含みます。- Axumのハンドラは非同期関数 (
async fn
) である必要があります。 - この例では引数を取りませんが、リクエストから情報(パスパラメータ、クエリ、ボディなど)を抽出したい場合は引数を追加します(後述のExtractor)。
- 戻り値は、HTTPレスポンスに変換可能な型である必要があります。
String
型はAxumによって自動的に200 OK
ステータスコードとContent-Type: text/plain
ヘッダーを持つHTTPレスポンスに変換されます(IntoResponse
トレイトの実装による)。
- Axumのハンドラは非同期関数 (
tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap()
: 指定されたアドレス(ここではローカルホストのポート3000)でIncoming接続を待ち受けるTCPリスナーを作成します。これは非同期操作なので.await
が必要です。unwrap()
はエラーが発生した場合にパニックさせますが、実際にはより丁寧なエラーハンドリングが必要です。axum::serve(listener, app).await.unwrap()
: 作成したTCPリスナーとAxumルーターを使ってHTTPサーバーを起動します。この関数はサーバーが停止するまで(例: プロセスが終了するまで)ブロックします。これも非同期操作です。
アプリケーションの実行
プロジェクトのルートディレクトリ(Cargo.toml
がある場所)で、以下のコマンドを実行します。
bash
cargo run
Cargoは依存関係をビルドし、src/main.rs
のコードをコンパイルして実行します。成功すると、ターミナルに以下のような出力が表示されるはずです。
INFO my_axum_app::main: listening on 127.0.0.1:3000
Webブラウザやcurlなどのツールを使って、http://127.0.0.1:3000/
にアクセスしてみてください。
bash
curl http://127.0.0.1:3000/
ターミナルにHello, world!
と表示されれば成功です。
Ctrl+Cを押すとサーバーが停止します。
これで、Axumを使った基本的なWebサーバーの起動と、簡単なエンドポイントの実装ができました。これはAxum開発のまさに第一歩です。
ルーティングの詳細
AxumのRouter
は、受信したHTTPリクエストのパスとメソッドに基づいて、実行すべきハンドラを決定します。Router::new()
でルーターを作成し、.route()
や.nest()
などのメソッドを使ってルートを定義していきます。
メソッドによるルーティング
.route()
メソッドは、指定したパスに対する特定のHTTPメソッド(GET, POSTなど)のハンドラを設定します。
“`rust
use axum::{
routing::{get, post},
Router,
};
async fn handler_get() -> String { “GET request”.to_string() }
async fn handler_post() -> String { “POST request”.to_string() }
let app = Router::new()
.route(“/resource”, get(handler_get).post(handler_post));
// 同じパスに対して複数のメソッドのハンドラを設定できる
“`
get()
, post()
, put()
, delete()
, patch()
, head()
, options()
, trace()
といった関数がaxum::routing
モジュールで提供されており、それぞれのHTTPメソッドに対応します。.route()
にこれらの関数をチェーンして繋げることで、同じパスに対する複数のメソッドのハンドラを定義できます。
any()
を使うと、指定したパスへの全てのリクエストメソッドに対して同じハンドラを設定できます。
“`rust
use axum::{
routing::any,
Router,
};
async fn handler_any() -> String { “Any method request”.to_string() }
let app = Router::new()
.route(“/any-method”, any(handler_any));
“`
パスパラメータの抽出 (Path
)
URLパスの一部をパラメータとして抽出し、ハンドラに渡すことができます。例えば、/users/123
というパスから123
というユーザーIDを抽出したい場合に使用します。
パスパラメータは、ルーターのパス定義で :
の後にパラメータ名を記述します。ハンドラ関数では、axum::extract::Path
Extractorを使用してパラメータを抽出します。
“`rust
use axum::{
extract::Path,
routing::get,
Router,
};
[tokio::main]
async fn main() {
// … logging setup …
let app = Router::new()
.route("/users/:id", get(get_user)); // :id がパスパラメータ
// ... server setup ...
}
// async fn get_user(Path(user_id): Path
async fn get_user(Path(params): Path
// println!(“Received user ID: {}”, user_id); // 単一パラメータの場合
// format!(“User ID: {}”, user_id) // 単一パラメータの場合
let user_id = params.get("id").expect("id must be present");
println!("Received user ID: {}", user_id);
format!("User ID: {}", user_id)
}
“`
Path<T>
のT
には、抽出したいパスパラメータの型を指定します。
* 単一のパスパラメータを抽出する場合、Path(parameter_name): Path<Type>
のようにタプル構造分解と組み合わせるのが一般的です。例えば、Path(user_id): Path<u32>
は、:id
パラメータを抽出し、それをu32
型に変換してuser_id
変数にバインドします。型変換に失敗した場合、Axumは自動的に400 Bad Request
エラーレスポンスを返します。
* 複数のパスパラメータがある場合(例: /items/:category/:item_id
)、あるいは型変換のエラーハンドリングを細かく制御したい場合は、Path<std::collections::HashMap<String, String>>
や、パラメータ名をフィールドに持つ構造体(#[derive(Deserialize)]
が必要)を使用することもできます。例ではHashMapを使っています。
クエリパラメータの抽出 (Query
)
URLのクエリ文字列(例: /search?query=rust&page=1
)からパラメータを抽出するには、axum::extract::Query
Extractorを使用します。
Query<T>
のT
には、クエリパラメータを保持するための構造体を指定します。この構造体には、クエリパラメータ名に対応するフィールドが必要です。#[derive(Deserialize)]
を付加することで、serde
がクエリ文字列から構造体へのデシリアライズコードを生成します。
“`rust
use axum::{
extract::Query,
routing::get,
Router,
};
use serde::Deserialize;
[derive(Deserialize)]
struct Pagination {
page: Option
per_page: Option
}
[tokio::main]
async fn main() {
// … logging setup …
let app = Router::new()
.route("/items", get(list_items));
// ... server setup ...
}
async fn list_items(Query(pagination): Query
let page = pagination.page.unwrap_or(1);
let per_page = pagination.per_page.unwrap_or(10);
format!("Listing items - Page: {}, Items per page: {}", page, per_page)
}
“`
Query(pagination): Query<Pagination>
のように記述することで、クエリ文字列がPagination
構造体にデシリアライズされ、pagination
変数にバインドされます。構造体のフィールドはOption<T>
にすることで、パラメータが存在しなくてもエラーにならないようにできます。型変換に失敗した場合(例: page=abc
)、Axumは400 Bad Request
を返します。
パスのワイルドカード (*
)
特定のパスセグメント以降の全てのパスを捕捉するには、ワイルドカード*
を使用します。例えば、/static/*path
は/static/css/style.css
や/static/images/logo.png
などにマッチします。
ワイルドカードで捕捉されたパスの部分は、Path<String>
またはPath<Vec<String>>
として抽出できます(セグメントごとに分割されるか全体か)。ただし、通常は静的ファイル配信など、捕捉されたパスをサービスに渡す目的で使用されます。tower_http::services::ServeDir
と組み合わせるのが一般的です。
“`rust
use axum::{
routing::get,
Router,
};
use tower_http::services::ServeDir;
[tokio::main]
async fn main() {
// … logging setup …
let app = Router::new()
.route("/hello", get(|| async { "Hello!" }))
// /static/ 以下のリクエストを ./static ディレクトリからファイルとして配信
.nest_service("/static", ServeDir::new("static")); // ServeDirはTower Serviceを実装
// ... server setup ...
}
“`
この例では、/static/*path
のようなルーティングを直接定義する代わりに、nest_service
メソッドを使っています。これは、特定のパスプレフィックス以下の全てのリクエストを、別のService
実装(ここではServeDir
)に委譲するための便利な方法です。
ネストされたルーター (Router::nest
)
大規模なアプリケーションでは、関連するルートをグループ化し、それぞれを別のルーターとして定義したい場合があります。Router::nest()
メソッドは、あるパスプレフィックス配下に別のルーターをマウントするために使用されます。
“`rust
use axum::{
routing::{get, post},
Router,
};
async fn list_users() -> String { “List users”.to_string() }
async fn create_user() -> String { “Create user”.to_string() }
async fn get_user(Path(id): Path
// ユーザー関連のルートを持つサブルーターを作成
fn users_routes() -> Router {
Router::new()
.route(“/”, get(list_users).post(create_user)) // /users と /users/ にマッチ
.route(“/:id”, get(get_user)) // /users/:id にマッチ
}
[tokio::main]
async fn main() {
// … logging setup …
let app = Router::new()
.route("/", get(|| async { "Welcome!" }))
.nest("/users", users_routes()); // /users 以下に users_routes をマウント
// ... server setup ...
}
“`
この例では、users_routes()
関数がユーザー関連のルートをまとめたルーターを返します。main
関数では、.nest("/users", users_routes())
によって、/users
というパスプレフィックス以下にこのルーターをマウントしています。これにより、/users
へのGETリクエストはlist_users
に、/users
へのPOSTリクエストはcreate_user
に、/users/123
へのGETリクエストはget_user
にルーティングされるようになります。
ルーターをネストすることで、コードが整理され、モジュール性が向上します。また、ネストされたルーター全体に特定のミドルウェアを適用することも容易になります。
ハンドラの作成と引数
Axumのハンドラは、HTTPリクエストを処理する中心的なロジックを含む非同期関数です。ハンドラ関数の最も強力な特徴の一つは、その引数として様々なExtractor
を指定できることです。Axumはリクエストを受け取ると、ハンドラのシグネチャを見て必要なExtractorを実行し、その結果を関数の引数として渡します。
ハンドラ関数の基本的なシグネチャは以下のようになります。
rust
async fn some_handler(
// ここにExtractorを実装した型をカンマ区切りで並べる
// 例: Path<u32>, Query<MyQuery>, Json<MyRequestBody>
) -> impl axum::response::IntoResponse {
// リクエスト処理ロジック
// IntoResponseを実装した型(String, Json, StatusCodeなど)を返す
}
impl axum::response::IntoResponse
は、戻り値がIntoResponse
トレイトを実装していればどのような型でもよいことを示します。これにより、様々な種類のレスポンス(文字列、JSON、ステータスコード、ファイルなど)を返すことができます。
主要なExtractor
Axumは多くの便利な組み込みExtractorを提供しています。いくつか主要なものを紹介します。
axum::extract::Path
前述の通り、URLパスからパラメータを抽出します。
rust
async fn get_item(Path((category, item_id)): Path<(String, u32)>) -> String {
format!("Getting item '{}' from category '{}'", item_id, category)
}
// ルーティング: .route("/items/:category/:item_id", get(get_item))
複数のパスパラメータを抽出する場合、タプルや構造体(#[derive(Deserialize)]
)を使用できます。
axum::extract::Query
クエリ文字列からパラメータを抽出します。#[derive(Deserialize)]
付きの構造体を使用します。
“`rust
[derive(Deserialize)]
struct SearchParams {
q: String,
limit: Option
}
async fn search(Query(params): Query
let limit = params.limit.unwrap_or(10);
format!(“Searching for ‘{}’ with limit {}”, params.q, limit)
}
// ルーティング: .route(“/search”, get(search))
“`
axum::extract::Json
リクエストボディをJSONとしてパースし、#[derive(Deserialize)]
付きのRust構造体に変換します。HTTPヘッダーContent-Type: application/json
が必要です。
“`rust
use axum::Json;
use serde::Deserialize;
use serde_json::{Value, json}; // レスポンスでjson!マクロを使う例
[derive(Deserialize)]
struct CreateUser {
username: String,
email: String,
}
async fn create_user(Json(payload): Json
// 通常はここでデータベースに保存するなど
println!(“Creating user: {}”, payload.username);
// 成功レスポンスをJSONで返す
Json(json!({
"status": "success",
"message": "User created",
"username": payload.username
}))
}
// ルーティング: .route(“/users”, post(create_user))
“`
Json<T>
は、リクエストボディをT
にデシリアライズするExtractorであると同時に、T
をJSONレスポンスとしてシリアライズするIntoResponse
の実装でもあります。上記の例では、戻り値の型もJson<Value>
として、serde_json::json!
マクロで構造化されたJSONを生成して返しています。
axum::extract::Form
リクエストボディをURLエンコードされたフォームデータ(application/x-www-form-urlencoded
)またはマルチパートフォームデータ(multipart/form-data
)としてパースし、#[derive(Deserialize)]
付きのRust構造体に変換します。
“`rust
use axum::Form;
use serde::Deserialize;
[derive(Deserialize)]
struct LoginForm {
username: String,
password: String,
}
async fn login(Form(form): Form
// 通常はここで認証処理
format!(“Login attempt for user: {}”, form.username)
}
// ルーティング: .route(“/login”, post(login))
“`
マルチパートフォームデータ(ファイルのアップロードなど)を扱う場合は、Form
はmultipart
クレートと連携します。
axum::extract::State
アプリケーション全体で共有される状態(データベース接続プール、設定オブジェクトなど)にアクセスするために使用します。ルーターに.with_state()
メソッドで状態を追加する必要があります。
“`rust
use axum::{extract::State, routing::get, Router};
use std::sync::{Arc, Mutex};
[derive(Clone)] // Stateとして使用するにはCloneを実装する必要がある
struct AppState {
counter: Arc
}
[tokio::main]
async fn main() {
// … logging setup …
let shared_state = AppState {
counter: Arc::new(Mutex::new(0)),
};
let app = Router::new()
.route("/", get(increase_counter))
.with_state(shared_state); // ルーターに状態を追加
// ... server setup ...
}
async fn increase_counter(State(state): State
let mut counter = state.counter.lock().unwrap();
counter += 1;
format!(“Counter value: {}”, counter)
}
“`
State<T>
Extractorを使うことで、ハンドラ関数内でアプリケーションの状態T
にアクセスできます。状態は通常、複数のリクエスト間で共有されるため、Arc
(アトミック参照カウント)と、ミュータブルな共有のためにMutex
やRwLock
のような同期プリミティブと組み合わせて使用されます。State
として共有する型はClone
トレイトを実装している必要があります。
その他の主要なExtractor
axum::extract::Extension
: ミドルウェアの層を介してリクエストに付加されたデータにアクセスします。認証されたユーザー情報などをハンドラに渡す際によく使われます。axum::headers::Headers
: リクエストヘッダー全体、または特定のヘッダーにアクセスします。axum::headers
モジュールには多くの標準HTTPヘッダーに対応する型が定義されています(例:ContentType
,UserAgent
)。axum::body::Bytes
: リクエストボディを生のバイト列として取得します。axum::body::Body
: リクエストボディをストリームとして取得します。大きなボディを効率的に処理する場合に使用します。axum::extract::OriginalUri
: リクエストの元のURIを取得します。axum::extract::ConnectInfo
: クライアントのIPアドレスなど、接続に関する情報を取得します。TcpListener
にIncoming::into_make_service_with_connect_info
を使用する必要があります。axum::extract::Request
: 生のHyperのRequest
オブジェクト全体を取得します。Extractorが提供する以上の情報が必要な場合に使います。
複数のExtractorの使用
ハンドラ関数は複数のExtractorを引数として取ることができます。AxumはこれらのExtractorを順番に実行し、全てが成功した場合にのみハンドラ関数を実行します。Extractorの実行順序は基本的に自由ですが、ボディを消費するExtractor(Json
, Form
, Bytes
, Body
)は一度しか使用できません。また、ボディを消費するExtractorは、ボディを消費しないExtractorの後ろに置くのが一般的です。
“`rust
use axum::{
extract::{Path, Query, Json},
routing::post,
Router,
};
use serde::{Deserialize, Serialize};
[derive(Deserialize)]
struct UpdateItemPayload {
name: String,
price: f64,
}
[derive(Serialize)]
struct ItemUpdatedResponse {
id: u32,
name: String,
price: f64,
message: String,
}
[derive(Deserialize)]
struct OptionalQueryParams {
notify: Option
}
async fn update_item(
Path(item_id): Path
Query(query_params): Query
Json(payload): Json
) -> Json
println!(“Updating item ID: {}”, item_id);
println!(“Payload: name={}, price={}”, payload.name, payload.price);
if query_params.notify.unwrap_or(false) {
println!(“Notification requested.”);
}
// 通常はここでデータベース更新など
Json(ItemUpdatedResponse {
id: item_id,
name: payload.name,
price: payload.price,
message: format!("Item {} updated successfully", item_id),
})
}
// ルーティング: .route(“/items/:id”, post(update_item))
“`
このハンドラは、パスパラメータ(item_id
)、クエリパラメータ(notify
)、そしてJSONリクエストボディ(name
, price
)を一度に抽出しています。非常に宣言的で分かりやすいシグネチャになります。
カスタムExtractorの実装
組み込みのExtractorで対応できない、より複雑なリクエストデータの抽出が必要な場合は、独自のカスタムExtractorを実装できます。カスタムExtractorは、axum::extract::FromRequestParts
またはaxum::extract::FromRequestBody
トレイトを実装します。
例えば、特定のヘッダーが存在し、その値が正しい形式であることを保証しつつ抽出するExtractorを実装できます。これは、APIキーやトークンによる認証の初期段階などに利用できます。
これらのトレイトの実装はやや複雑になりますが、AxumのExtratorシステムの柔軟性を示しています。
レスポンスの生成
ハンドラ関数の戻り値は、axum::response::IntoResponse
トレイトを実装している必要があります。このトレイトは、Rustの値をHTTPレスポンスに変換する方法を定義します。Axumは多くの一般的な型に対してこのトレイトを実装済みです。
基本的なレスポンス型
String
/&'static str
:200 OK
ステータスとtext/plain
のContent-Typeを持つレスポンスになります。ボディは文字列の内容です。()
:200 OK
ステータスと空のボディを持つレスポンスになります。StatusCode
: 指定したステータスコードを持つレスポンスになります。ボディは空です。Result<T, E>
:T
がIntoResponse
を実装し、E
もIntoResponse
を実装している場合、Ok(T)
はT
をレスポンスに変換し、Err(E)
はE
をレスポンスに変換します。これはエラーハンドリングで非常に役立ちます。
“`rust
use axum::http::StatusCode;
async fn create_item_handler() -> StatusCode {
// アイテム作成ロジック…
let success = true; // 例
if success {
StatusCode::CREATED // 201 Created
} else {
StatusCode::BAD_REQUEST // 400 Bad Request
}
}
// ルーティング: .route(“/items”, post(create_item_handler))
“`
カスタムレスポンスの構築 (Response
)
より詳細なレスポンス(特定のヘッダー、カスタムステータスコード、複雑なボディなど)を構築したい場合は、axum::response::Response
型を直接構築するか、axum::response::IntoResponse
トレイトを使ってレスポンスをカスタマイズします。
Response
を直接構築するのはやや手間がかかるため、通常はIntoResponse
トレイトを実装した型を返すか、後述の便利なレスポンダーを使用します。
JSONレスポンス (Json
)
前述の通り、axum::Json<T>
型は、T
がserde::Serialize
を実装している場合、T
をJSON形式にシリアライズしてapplication/json
のContent-Typeを持つレスポンスとして返します。
“`rust
use axum::Json;
use serde::Serialize;
[derive(Serialize)]
struct Item {
id: u32,
name: String,
price: f64,
}
async fn get_item_json(Path(item_id): Path
// データベースからアイテムを取得するロジック…
let item = Item { id: item_id, name: “Sample Item”.to_string(), price: 10.99 };
Json(item) // Item構造体をJSONにシリアライズして返す
}
// ルーティング: .route(“/items/:id”, get(get_item_json))
“`
リダイレクト (Redirect
)
クライアントを別のURLにリダイレクトさせるには、axum::response::Redirect
を使用します。
“`rust
use axum::response::Redirect;
async fn old_path_handler() -> Redirect {
Redirect::to(“/new-path”) // 303 See Other リダイレクトを返す
}
// ルーティング: .route(“/old-path”, get(old_path_handler))
“`
.to()
は303 See Other
を、.permanent()
は301 Moved Permanently
を返します。
HTMLレスポンス (Html
)
静的なHTML文字列を返すには、axum::response::Html
を使用します。Content-Typeは自動的にtext/html
になります。
“`rust
use axum::response::Html;
async fn render_html() -> Html<&’static str> {
Html(“
Hello from Axum!
This is HTML content.
“)
}
// ルーティング: .route(“/html”, get(render_html))
“`
より複雑なHTMLテンプレートエンジン(Askama, Tera, Handlebarsなど)と組み合わせる場合は、テンプレートエンジンが生成したString
をHtml()
でラップして返すか、テンプレートエンジンの出力型自体がIntoResponse
を実装するようにします。
その他のレスポンダー
AxumやTower-HTTPには、他にも様々な便利なレスポンダーが用意されています。
tower_http::services::ServeFile
: 単一のファイルをレスポンスとして配信します。tower_http::services::ServeDir
: ディレクトリ内のファイルを静的ファイルとして配信します(前述の例)。axum::body::StreamBody
: 非同期ストリームからレスポンスボディをストリーム配信します。大きなファイルやリアルタイムデータなどに使用できます。
これらのレスポンダーは、それぞれの型がIntoResponse
を実装しているため、ハンドラから直接返すことができます。
アプリケーションの状態管理
Webアプリケーションでは、複数のリクエストやハンドラ間で共有される状態を持つことがよくあります。例えば、データベース接続プール、キャッシュ、アプリケーション設定、認証情報などがこれにあたります。Axumでは、axum::extract::State
Extractorとaxum::Router::with_state()
メソッドを使って状態を管理します。
状態として渡す型は、以下の条件を満たす必要があります。
'static
ライフタイムを持つこと(アプリケーションの生存期間中存在すること)。Clone
トレイトを実装していること。Axumは各リクエストの処理前に状態をクローンしてハンドラに渡すためです。
ミュータブルな状態や共有リソース(データベース接続など)を扱う場合、スレッドセーフな方法で共有する必要があります。Rustでは、通常std::sync::Arc
とstd::sync::{Mutex, RwLock}
を組み合わせます。
Arc<T>
: スレッド間でT
型のデータを安全に共有するための参照カウンタです。データを複数のスレッドから参照できますが、変更はできません。Mutex<T>
: ミュータブルなデータT
への排他的アクセスを提供します。データにアクセスする際はロックを取得する必要があります。書き込みが多い場合に適しています。RwLock<T>
: ミュータブルなデータT
への共有/排他的アクセスを提供します。複数のリーダーまたは単一のライターによるアクセスを許可します。読み取りが多い場合に適しています。
データベース接続プールのような共有リソースは、アプリケーション起動時に生成し、Arc
でラップして状態としてルーターに渡すのが一般的なパターンです。
“`rust
use axum::{extract::State, routing::get, Router};
use std::sync::{Arc, Mutex};
use tokio::net::TcpListener;
use tracing::{info, Level};
use tracing_subscriber::FmtSubscriber;
// アプリケーション状態を定義
struct AppState {
// データベース接続プールなど、共有したいミュータブルなリソース
// ここでは簡単なカウンターの例
counter: Mutex
// 設定値など、イミュータブルな共有データ
config: String,
}
// Stateとして使用するためにCloneを実装
impl Clone for AppState {
fn clone(&self) -> Self {
// MutexはCloneできないので、新しいMutexを初期値0で作成するのではなく、
// Arcと組み合わせてミュータブルなデータを共有するのが一般的。
// この例のAppStateはMutexを直接持っているため、
// 実際にはAppState自体をArcでラップする方が現実的。
// 例のため、ここではカウンターはクローンごとにリセットされると仮定(非現実的)
Self {
counter: Mutex::new(0), // この例は良くない実装です!
config: self.config.clone(),
}
}
}
// ★より現実的なAppStateの定義例 (Arc<Mutex<…>>を使用)
struct AppStateArc {
// アプリケーション全体で共有されるデータはArcでラップ
counter: Arc
// 設定など、クローン可能なデータはそのまま持てる
config: String,
}
impl Clone for AppStateArc {
fn clone(&self) -> Self {
Self {
counter: Arc::clone(&self.counter), // Arc::clone は参照カウントを増やすだけ
config: self.config.clone(),
}
}
}
[tokio::main]
async fn main() {
FmtSubscriber::builder()
.with_env_filter(“info”)
.init();
// ★現実的なAppStateArcの初期化
let shared_state_arc = AppStateArc {
counter: Arc::new(Mutex::new(0)),
config: "Some app config".to_string(),
};
let app = Router::new()
.route("/", get(handler_with_state))
.route("/config", get(handler_read_config))
// ルーターに状態を追加
.with_state(shared_state_arc); // AppStateArc全体をStateとして渡す
let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
info!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
// State
async fn handler_with_state(State(state): State
// Mutexをロックしてカウンターをインクリメント
let mut counter = state.counter.lock().unwrap(); // ロックの取得はブロッキング
*counter += 1;
format!("Counter: {}, Config: {}", *counter, state.config)
}
async fn handler_read_config(State(state): State
// 読み取り専用のフィールドにアクセス
format!(“Config value: {}”, state.config)
}
“`
この改良された例では、ミュータブルなcounter
をArc<Mutex<u32>>
でラップしています。これにより、AppStateArc
自体のクローンはArc
の参照カウントを増やすだけで済み、複数のハンドラやスレッド間で同じカウンターインスタンスを安全に共有できるようになります。
State<T>
Extractorは、指定された型の状態がルーターに.with_state(state)
で追加されていることを期待します。複数の異なる種類の状態を共有したい場合は、それらをフィールドに持つ単一の構造体を作成し、その構造体を状態として渡すのが一般的です。
“`rust
// 複数の共有状態を持つ構造体の例
struct AppStateCompound {
db_pool: sqlx::PgPool, // 例: データベース接続プール
cache: Arc
config: AppConfig, // アプリケーション設定
}
// Clone を実装(各フィールドが Clone 可能である必要あり)
impl Clone for AppStateCompound {
fn clone(&self) -> Self {
Self {
db_pool: self.db_pool.clone(), // sqlx::PgPool は Clone を実装している
cache: Arc::clone(&self.cache), // Arc::clone
config: self.config.clone(), // AppConfig が Clone を実装している必要あり
}
}
}
// … main 関数内で初期化してルーターに追加 …
// let state = AppStateCompound { db_pool: …, cache: …, config: … };
// let app = Router::new().route(…).with_state(state);
// … ハンドラ内での使用 …
// async fn handler_needs_state(State(state): State
// let user = sqlx::query_as!(User, “SELECT id, username FROM users WHERE id = $1”, 1)
// .fetch_one(&state.db_pool) // db_pool を使用
// .await?;
// if let Some(cached_value) = state.cache.get(“some_key”) { // cache を使用
// println!(“Cached value: {}”, cached_value);
// }
// println!(“App config value: {}”, state.config.some_setting); // config を使用
// Ok(format!(“User: {}”, user.username))
// }
“`
このように、State
ExtractorとArc
/Mutex
/RwLock
を組み合わせることで、Rustの所有権と並行性のルールを守りながら、アプリケーション全体で安全に状態を共有できます。
ミドルウェア
ミドルウェアは、リクエストがハンドラに到達する前や、ハンドラがレスポンスを返した後に、共通の処理を挿入するための強力なメカニズムです。ログ出力、認証、CORS対応、圧縮、レート制限など、様々な横断的な concerns をミドルウェアとして実装できます。
AxumはTowerライブラリのミドルウェアシステムを全面的に採用しています。Towerでは、ミドルウェアはLayer
トレイトとして定義され、サービス(Service
トレイトを実装するもの、Axumのルーターやハンドラも含む)をラップして新しいサービスを作成します。
Axumルーターにミドルウェアを追加するには、.layer()
メソッドを使用します。このメソッドはtower::Layer
トレイトを実装したものを引数に取ります。tower-http
クレートには、多くの一般的なミドルウェアがLayer
の実装として提供されています。
ミドルウェアは、ルーターに適用された順序で内側から外側へと実行されます。つまり、.layer(A).layer(B)
と適用した場合、リクエストはまずBを通り、次にAを通り、その後にルーターに到達します。レスポンスは逆の順序(ルーター -> A -> B)で戻ります。
主要な組み込みミドルウェア (tower-http
)
tower-http
クレートは、Axumと非常に相性の良い便利なミドルウェアを多数提供しています。Cargo.tomlでtower-http
の対応するフィーチャーを有効にする必要があります(features = ["full"]
で全て有効にできます)。
tower_http::trace::TraceLayer
: リクエスト/レスポンスに関する詳細なログを出力します。tracing
クレートと連携します。非常に便利で、ほとんどのアプリケーションで最初に加えるべきミドルウェアです。
“`rust
use axum::{routing::get, Router};
use tower_http::trace::TraceLayer;
[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(“info”)
.init();
let app = Router::new()
.route("/", get(|| async { "Hello!" }))
// TraceLayer を追加
.layer(TraceLayer::new_for_http()); // HTTPリクエスト用に設定
// ... server setup ...
}
“`
TraceLayer
は様々な設定が可能で、ログのフォーマットや、リクエスト/レスポンスのどの情報をログに含めるかなどをカスタマイズできます。
tower_http::timeout::TimeoutLayer
: リクエスト処理にタイムアウトを設定します。指定した時間内にレスポンスが返されない場合、リクエストはキャンセルされ、504 Gateway Timeout
などが返されます。
“`rust
use axum::{routing::get, Router};
use tower_http::timeout::TimeoutLayer;
use std::time::Duration;
use tokio::time::sleep;
async fn slow_handler() -> String {
sleep(Duration::from_secs(5)).await; // 5秒待つ
“Done!”.to_string()
}
[tokio::main]
async fn main() {
// … logging setup …
let app = Router::new()
.route("/slow", get(slow_handler))
// 2秒のタイムアウトを設定
.layer(TimeoutLayer::new(Duration::from_secs(2)));
// ... server setup ...
}
“`
/slow
にアクセスすると、2秒後にタイムアウトエラーが発生します。
tower_http::compression::CompressionLayer
: レスポンスボディを自動的に圧縮します (gzip, brotliなど)。クライアントのAccept-Encoding
ヘッダーに応じて最適な圧縮方法を選択します。
“`rust
use axum::{routing::get, Router};
use tower_http::compression::CompressionLayer;
[tokio::main]
async fn main() {
// … logging setup …
let app = Router::new()
.route("/", get(|| async { "Compress me!" }))
// 応答圧縮を有効化
.layer(CompressionLayer::new());
// ... server setup ...
}
“`
クライアントが圧縮をサポートしている場合、レスポンスは圧縮されて送信されます。
tower_http::cors::CorsLayer
: CORS (Cross-Origin Resource Sharing) を設定します。異なるオリジンからのHTTPリクエストを許可するかどうかなどを制御します。
“`rust
use axum::{routing::get, Router};
use tower_http::cors::CorsLayer;
use tower_http::cors::Any; // Any は全てを許可する場合に便利
[tokio::main]
async fn main() {
// … logging setup …
let app = Router::new()
.route("/", get(|| async { "CORS enabled!" }))
// 全てのオリジン、メソッド、ヘッダーを許可する最も緩い設定
.layer(CorsLayer::new().allow_origin(Any).allow_methods(Any).allow_headers(Any));
// ... server setup ...
}
“`
CorsLayer
は非常に多くの設定オプションを持ちます。本番環境では、許可するオリジン、メソッド、ヘッダーなどを制限することが強く推奨されます。
tower_http::auth::RequireAuthorizationLayer
: 認証ミドルウェア。特定のリクエストヘッダー(例:Authorization: Bearer token
)をチェックし、認証が成功した場合のみリクエストを次のサービス(ハンドラなど)に進めます。失敗した場合は401 Unauthorized
や403 Forbidden
を返します。
“`rust
use axum::{routing::get, Router, extract::Extension};
use tower_http::auth::{RequireAuthorizationLayer, AuthorizeRequest};
use http::{Request, StatusCode};
use axum::body::Body;
// 認証状態をExtensionとしてハンドラに渡すための型
struct CurrentUser {
user_id: u32,
}
// RequireAuthorizationLayerに渡す認証ロジックを実装する型
struct MyAuthorizer;
impl AuthorizeRequest
for MyAuthorizer {type ResponseBody = Body; // 認証失敗時のレスポンスボディの型
type Future = std::future::Ready<Result<(), StatusCode>>; // 認証結果の非同期型
fn authorize(&mut self, request: &mut Request<Body>) -> Self::Future {
// ★本来はここでRequestのヘッダーなどをチェックして認証ロジックを実行★
// 例として、ここでは常に認証成功とみなし、Extensionを追加
println!("Authorizing request...");
let auth_success = true; // ダミーの認証結果
let result = if auth_success {
// 認証成功の場合、Extensionとしてユーザー情報を追加
request.extensions_mut().insert(CurrentUser { user_id: 123 });
Ok(()) // 認証成功
} else {
Err(StatusCode::UNAUTHORIZED) // 認証失敗 (401 Unauthorized)
};
std::future::ready(result)
}
}
[tokio::main]
async fn main() {
// … logging setup …
let app = Router::new()
.route("/protected", get(protected_handler))
// 認証ミドルウェアを追加
// MyAuthorizerが認証ロジックを提供し、成功時にExtensionを追加
.layer(RequireAuthorizationLayer::custom(MyAuthorizer));
// ... server setup ...
}
// protected_handler は Extension
// 認証ミドルウェアが成功した場合にのみ、このハンドラは実行され、CurrentUser が Extension として利用可能になっている
async fn protected_handler(Extension(user): Extension
format!(“Welcome, user {}! This is a protected resource.”, user.user_id)
}
“`
この例では、MyAuthorizer
がAuthorizeRequest
トレイトを実装しており、ここで実際のリクエスト認証ロジックを記述します。認証が成功した場合、request.extensions_mut().insert(...)
を使って認証済みユーザー情報などをExtension
としてリクエストにアタッチできます。その後のハンドラでは、Extension<CurrentUser>
Extractorを使ってこの情報を取り出せます。
カスタムミドルウェアの実装
Layer
トレイトを実装することで、独自のカスタムミドルウェアを作成できます。これは、TowerのService
トレイトの概念を理解している必要があります。
単純なカスタムミドルウェアの例として、リクエスト処理時間を計測してログに出力するミドルウェアを作成します。
“`rust
use axum::body::Body;
use http::{Request, Response};
use std::task::{Context, Poll};
use std::time::Duration;
use tower::{Layer, Service};
use tracing::info;
// ミドルウェア自体の設定を持つ型 (必要に応じて)
[derive(Clone)]
struct TimingMiddleware;
// Layer トレイトの実装
impl Layer for TimingMiddleware {
type Service = TimingService;
fn layer(&self, inner: S) -> Self::Service {
TimingService { inner }
}
}
// ミドルウェアのロジックを持つ Service トレイトの実装
[derive(Clone)]
struct TimingService {
inner: S,
}
impl Service
where
S: Service
S::Future: Send + ‘static,
{
type Response = S::Response;
type Error = S::Error;
type Future = impl std::future::Future
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
// ラップしているサービスが準備できているか確認
self.inner.poll_ready(cx)
}
fn call(&mut self, request: Request<Body>) -> Self::Future {
let start = std::time::Instant::now(); // リクエスト開始時刻を記録
let path = request.uri().path().to_string();
// ラップしているサービスの call メソッドを呼び出す (次のミドルウェアまたはハンドラ)
let future = self.inner.call(request);
// 非同期ブロック内でレスポンスが返るのを待つ
async move {
let response = future.await?; // レスポンスを待つ
let duration = start.elapsed(); // 処理時間を計算
info!("Request to {} took {:?}", path, duration); // ログ出力
Ok(response) // レスポンスをそのまま返す
}
}
}
// アプリケーションでの使用例
use axum::{routing::get, Router};
use tokio::net::TcpListener;
[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(“info”)
.init();
let app = Router::new()
.route("/", get(|| async { "Hello!" }))
// カスタムミドルウェアを適用
.layer(TimingMiddleware);
let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
info!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
“`
カスタムミドルウェアの実装は、TowerのService
トレイトの理解を深める必要がありますが、非常に強力で柔軟な拡張ポイントとなります。多くの場合は、tower-http
や他のコミュニティ提供のミドルウェアで十分でしょう。
ミドルウェアを効果的に活用することで、アプリケーションロジックから共通の懸念事項を分離し、コードの可読性、保守性、再利用性を向上させることができます。
エラーハンドリング
Webアプリケーションでは、リクエスト処理中に様々なエラーが発生する可能性があります(例: データベースエラー、外部API呼び出しの失敗、入力値のバリデーションエラーなど)。これらのエラーを適切にハンドリングし、クライアントに分かりやすいHTTPレスポンスとして返すことが重要です。
Axumでは、ハンドラがResult<T, E>
を返すことでエラーを示すことができます。ここで、T
とE
はどちらもaxum::response::IntoResponse
トレイトを実装している必要があります。Ok(T)
の場合はT
がHTTPレスポンスに変換され、Err(E)
の場合はE
がHTTPレスポンスに変換されます。
これにより、ハンドラ関数内で発生したエラーを伝播させ、最終的にエラー型E
のIntoResponse
実装によってエラーレスポンス(例えば400 Bad Request
や500 Internal Server Error
など)に変換してクライアントに返すことができます。
カスタムエラー型の定義
アプリケーション固有のエラーや、複数の種類のエラーを扱うために、独自のカスタムエラー型を定義することが一般的です。このカスタムエラー型は、thiserror
やanyhow
のようなクレートと組み合わせて使用すると便利です。
カスタムエラー型をHTTPレスポンスに変換するためには、その型にaxum::response::IntoResponse
トレイトを実装する必要があります。
“`rust
use axum::{http::StatusCode, response::IntoResponse, Json};
use serde_json::json;
use thiserror::Error; // エラー型定義を簡素化
[derive(Error, Debug)]
enum AppError {
#[error(“Item not found”)]
NotFound,
#[error(“Invalid input: {0}”)]
InvalidInput(String),
#[error(“Database error”)]
DatabaseError(#[from] sqlx::Error), // 他のエラー型をラップする場合
#[error(“Internal server error”)]
Internal(#[from] anyhow::Error), // 汎用的なエラーをラップする場合
}
// AppError を HTTP レスポンスに変換するための IntoResponse 実装
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let (status, error_message) = match self {
AppError::NotFound => (StatusCode::NOT_FOUND, “Item not found”.to_string()),
AppError::InvalidInput(msg) => (StatusCode::BAD_REQUEST, format!(“Invalid input: {}”, msg)),
AppError::DatabaseError() | AppError::Internal() => {
// 内部エラーやデータベースエラーは、クライアントには詳細を隠蔽することが多い
(StatusCode::INTERNAL_SERVER_ERROR, “Something went wrong”.to_string())
}
};
// エラーレスポンスのボディをJSONで返す例
let body = Json(json!({
"error": error_message,
}));
(status, body).into_response()
}
}
// このカスタムエラー型を使用するハンドラの例
use axum::{extract::Path, routing::get, Router};
async fn get_item(Path(item_id): Path
// ダミーのロジック
if item_id == 0 {
return Err(AppError::NotFound); // Item not found のエラーを返す
}
if item_id == 1 {
return Err(AppError::InvalidInput(“ID cannot be 1”.to_string())); // Invalid input のエラーを返す
}
// 通常はここでデータベース問い合わせなど
// sqlx::query(…).fetch_one(&pool).await.map_err(AppError::DatabaseError)?; // データベースエラーをラップ
Ok(format!("Item details for ID: {}", item_id))
}
[tokio::main]
async fn main() {
// … logging setup …
let app = Router::new()
.route("/items/:id", get(get_item));
// ... server setup ...
}
“`
この例では、AppError
というEnumで様々なエラーケースを定義し、thiserror
を使ってエラーメッセージや他のエラー型からの変換(#[from]
)を自動生成しています。
impl IntoResponse for AppError
ブロックでは、AppError
の各バリアントに対して、対応するHTTPステータスコードとレスポンスボディを決定しています。クライアントへのレスポンスとして、ステータスコードとエラーメッセージを含むJSONオブジェクトを返しています。これにより、ハンドラ内でResult<_, AppError>
を返すだけで、Axumが自動的に適切なエラーレスポンスを生成してくれます。
グローバルなエラーハンドリング
特定のルーターやアプリケーション全体に対して、特定の種類のエラーを統一的に処理したい場合があります。これは、ミドルウェアやAxumの内部的なエラーハンドリング機構を利用することで実現できます。
例えば、すべてのリクエストに対して発生する可能性のあるエラー(例: Extractorの失敗)を捕捉し、カスタムエラーページを表示する、といった処理を実装できます。
Axumでは、ルーターにaxum::error_handling::HandleError
レイヤーを適用することで、特定のサービス(ルーターやハンドラ)で発生したエラーを指定したハンドラ関数で処理させることができます。
“`rust
use axum::{
error_handling::HandleErrorLayer,
http::StatusCode,
routing::get,
Router,
};
use tower::{BoxError, ServiceBuilder};
use std::time::Duration;
async fn main_handler() -> String {
// 何かエラーが発生する可能性のある処理…
// 例えば、PathやQueryの抽出に失敗した場合など、AxumのExtractorがエラーを返すことがある
// この例では直接エラーは返さないが、Extractorエラーなどを捕捉する
“OK”.to_string()
}
// エラーハンドリングを行う関数
async fn handle_error(err: BoxError) -> StatusCode {
// Extractorエラーなどは BoxError としてラップされて渡される
// エラーの内容をログに出力するなど
eprintln!(“Unhandled error: {:?}”, err);
// クライアントには汎用的なエラーコードを返す
StatusCode::INTERNAL_SERVER_ERROR
}
[tokio::main]
async fn main() {
// … logging setup …
let app = Router::new()
.route("/", get(main_handler))
.layer(
// ServiceBuilderを使って複数のレイヤーを組み合わせる
ServiceBuilder::new()
// HandleErrorLayer を追加し、エラー発生時に handle_error 関数を呼び出す
.layer(HandleErrorLayer::new(handle_error))
// 他のミドルウェアもここに追加できる(HandleErrorLayerの外側に置く)
.timeout(Duration::from_secs(10)) // 例: タイムアウトミドルウェア
);
// ... server setup ...
}
“`
HandleErrorLayer
は、その内側のサービス(この例ではmain_handler
を含むルーター全体)で発生したエラーを捕捉し、指定されたハンドラ関数(handle_error
)に渡します。エラーハンドラ関数は、受け取ったエラー情報に基づいて適切なレスポンスを返します。捕捉されるエラーはBoxError
(Box<dyn Error + Send + Sync>
の型エイリアス)にラップされます。
エラーハンドリングは堅牢なWebアプリケーションにおいて非常に重要な要素です。AxumのResult
とIntoResponse
、そしてTowerのHandleErrorLayer
を組み合わせることで、柔軟かつ構造化されたエラーハンドリングを実現できます。
非同期処理と並行性
RustのWeb開発、特にAxumのようなフレームワークを使用する場合、非同期処理の理解は不可欠です。AxumはTokioランタイム上で動作し、ハンドラ関数やミドルウェアは非同期関数 (async fn
) として記述されます。
async fn
と.await
async fn
は、非同期的に実行される可能性のあるコードブロックを定義します。async fn
を呼び出すだけではコードはすぐには実行されず、Future
と呼ばれる、将来的に結果を生成する非同期タスクが返されます。このFuture
を実行するためには、.await
演算子を使用するか、非同期ランタイムにタスクとしてスポーンする必要があります。
ハンドラ関数の定義にasync fn
が必要なのは、Axumがこれらの関数から返されるFuture
をTokioランタイム上で実行するからです。ハンドラ内でデータベースアクセスや外部API呼び出しなど、時間のかかるI/O操作を行う際は、必ずその処理を返すFuture
に対して.await
を使用する必要があります。.await
は、I/O操作が完了するまで非同期的に待機し、その間ランタイムは別の準備ができたタスクを実行できます。これにより、単一のスレッドでも多数のコネクションを効率的に処理する、いわゆるノンブロッキングI/Oが実現されます。
“`rust
use tokio::time::{sleep, Duration};
async fn example_handler() -> String {
println!(“Handler started”);
// 非同期的な待機 (I/O 操作の代わり)
sleep(Duration::from_secs(1)).await;
println!(“Handler finished waiting”);
“Async operation complete”.to_string()
}
// ルーティング: .route(“/async”, get(example_handler))
“`
sleep
関数はTokioが提供する非同期タイマーです。sleep(...).await
は、指定した時間だけ現在のタスクの実行を中断し、ランタイムが別のタスクを実行できるようにします。時間が経過すると、ランタイムは中断したタスクの実行を再開します。
タスクの生成 (tokio::spawn
)
一つのリクエスト処理の中で、複数の非同期操作を並行して実行したい場合があります。例えば、複数の外部サービスに同時に問い合わせを行うなどです。このような場合、tokio::spawn
を使って新しい非同期タスクを生成し、それらを並行して実行させることができます。
“`rust
use tokio::task;
async fn parallel_handler() -> String {
// 複数のタスクを並行して実行
let task1 = task::spawn(async {
sleep(Duration::from_secs(2)).await;
“Result from task 1”
});
let task2 = task::spawn(async {
sleep(Duration::from_secs(1)).await;
"Result from task 2"
});
// 各タスクの完了を待って結果を取得
// .await はタスクが完了するまで待機
let result1 = task1.await.expect("Task 1 failed");
let result2 = task2.await.expect("Task 2 failed");
format!("Task 1: {}, Task 2: {}", result1, result2)
}
// ルーティング: .route(“/parallel”, get(parallel_handler))
“`
tokio::spawn
は、新しい非同期タスクをTokioランタイムに登録し、すぐに制御を返します。スポーンされたタスクはバックグラウンドで実行されます。タスクの結果を取得したり、タスクの完了を待ったりするには、JoinHandle
に対して.await
を使用します。expect("Task failed")
は、スポーンされたタスク自体がパニックした場合に、そのエラーメッセージを表示してプログラムを終了させます。
非同期処理における共通の落とし穴
- ブロッキング処理の実行:
async fn
の中でファイルI/Oや同期的なネットワーク呼び出しなど、ブロッキングする可能性のある処理を直接実行すると、そのタスクだけでなく、同じスレッド上で実行されている他の全てのタスクがブロックされてしまいます。これにより、サーバーの応答性が著しく低下します。ブロッキング処理を実行する必要がある場合は、tokio::task::spawn_blocking
を使用してください。これは、専用のスレッドプール上でブロッキング処理を実行します。 - 長いCPUバウンド処理: 複雑な計算など、CPUを長時間占有する処理も、
.await
ポイントがない限りスレッドをブロックします。これも応答性低下の原因となります。CPUバウンド処理もtokio::task::spawn_blocking
を使用するか、処理を小さな非同期可能なチャンクに分割する必要があります。 - デッドロック:
Mutex
やRwLock
のようなロックを使用する場合、非同期コンテキストでのデッドロックに注意が必要です。特に、.await
ポイントをまたいでロックを保持し続けると、他のタスクがそのロックを必要とした場合にデッドロックが発生する可能性があります。非同期コードでは、可能な限り短い期間だけロックを保持するようにし、.await
を呼び出す前にロックを解放することが推奨されます。Tokioは非同期コンテキストで使用するための独自の同期プリミティブ(tokio::sync::Mutex
,tokio::sync::RwLock
など)も提供しており、これらはブロッキングを伴わない待機を可能にします。
Rustの非同期プログラミングは強力ですが、その特性を理解して適切に扱うことが重要です。Tokioドキュメントやコミュニティのリソースを参照しながら学習を進めることをお勧めします。
テスト
Webアプリケーション開発において、テストは品質と信頼性を確保するために不可欠です。Axumアプリケーションは比較的簡単にテストすることができます。外部にHTTPリクエストを送信する必要はなく、内部的にルーターをサービスとして呼び出して、そのレスポンスを検証できます。
AxumルーターはTowerのService
トレイトを実装しています。TowerクレートのServiceExt
トレイトには、テストに便利なメソッドが追加されています。特にoneshot()
メソッドは、サービスに一度だけリクエストを送信し、そのレスポンスを非同期的に取得するために使用されます。
テストには、Cargoの組み込みテストフレームワークと、tokio::test
マクロ、そしてtower::ServiceExt
(と通常はhttp
クレートのRequest
とResponse
型)を使用します。
まず、Cargo.toml
の[dev-dependencies]
にtokio
とhttp
を追加します。
toml
[dev-dependencies]
tokio = { version = "1", features = ["rt", "macros"] } # テスト用のTokioランタイム
http = "1" # HTTPリクエスト/レスポンス構築のため
rt
フィーチャーはTokioランタイムのコア機能を、macros
フィーチャーは#[tokio::test]
マクロを有効にします。
次に、src/main.rs
(または別のテストファイル)にテストコードを記述します。
“`rust
// src/main.rs 内、または tests/integration_test.rs などに記述
[cfg(test)] // テスト時のみコンパイルされるようにする
mod tests {
use super::*; // main.rs の要素(handler, app_state など)にアクセスするため
use axum::{
body::Body,
http::{Request, StatusCode},
Router,
};
use tower::ServiceExt; // .oneshot() メソッドを使うために必要
// 非同期テスト関数を定義
#[tokio::test]
async fn basic_route_returns_hello_world() {
// テスト対象のルーターを作成(必要なら状態も設定)
let app = Router::new().route("/", get(|| async { "Hello, world!" }));
// リクエストを構築
let request = Request::builder()
.uri("/")
.body(Body::empty()) // ボディは空
.unwrap();
// ルーターをサービスとして呼び出し、レスポンスを取得
// .oneshot() は ServiceExt トレイトが提供
let response = app.oneshot(request).await.unwrap();
// レスポンスを検証
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
assert_eq!(&body[..], b"Hello, world!");
}
#[tokio::test]
async fn json_post_request() {
use axum::Json;
use serde::{Serialize, Deserialize};
use serde_json::json;
#[derive(Deserialize)]
struct Input { name: String }
#[derive(Serialize)]
struct Output { message: String }
async fn json_handler(Json(input): Json<Input>) -> Json<Output> {
Json(Output { message: format!("Received: {}", input.name) })
}
let app = Router::new().route("/json", post(json_handler));
let request_body = json!({ "name": "Axum" });
let request = Request::builder()
.method("POST")
.uri("/json")
.header("Content-Type", "application/json") // JSONリクエストにはContent-Typeが必要
.body(Body::from(request_body.to_string()))
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers()["content-type"], "application/json");
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
let json_body: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json_body, json!({ "message": "Received: Axum" }));
}
}
“`
解説:
#[cfg(test)] mod tests { ... }
: このモジュール内のコードがテスト実行時のみコンパイルされるようにします。use super::*;
:main.rs
内の要素(例えば、テスト対象のハンドラ関数やルーターを生成する関数)にアクセスするために必要です。#[tokio::test]
: 非同期テスト関数を定義するためのTokioマクロです。これにより、テスト関数内で.await
を使用できます。- テスト関数内では、テストしたいルーターをセットアップします。アプリケーション全体をテストしたい場合は、
main
関数で作成しているルーターのセットアップロジックを共有する関数を作成し、テストでもその関数を呼び出すのが良いでしょう。 http::Request::builder()
: テストリクエストを構築します。メソッド、URI、ヘッダー、ボディなどを設定できます。.body(Body::empty())
や.body(Body::from(...))
: リクエストボディを設定します。axum::body::Body
はTowerのBody
トレイトを実装しています。app.oneshot(request).await.unwrap()
: ルーター(app
変数)をサービスとして呼び出し、構築したリクエストを送信します。結果はResult<Response<Body>, Error>
で返されます。テストでは通常unwrap()
でエラーを無視しますが、実際にはエラーの種類をアサートすることも可能です。response.status()
: レスポンスのステータスコードを取得します。response.headers()
: レスポンスヘッダーを取得します。hyper::body::to_bytes(response.into_body()).await.unwrap()
: レスポンスボディを非同期的に読み出し、Bytes
型として取得します。Axumは内部的にHyperを使用しているため、Hyperのボディ操作関数を使用します。- 取得したボディデータやその他のレスポンス情報を使って、
assert_eq!
などのマクロで期待する結果と比較し、検証を行います。
複数のテストケースを持つアプリケーション全体をテストする場合、ルーターのセットアップロジックをテストヘルパー関数にまとめることが推奨されます。例えば、fn app() -> Router
のような関数を作成し、各テスト関数はこのapp()
関数を呼び出してテスト対象のルーターを取得します。状態(データベース接続など)が必要な場合は、テスト用のインメモリデータベースやモックなどを使用するか、テスト用のデータベースをセットアップして状態として渡します。
Axumのテストは、インテグレーションテストとして非常に効果的です。ハンドラ、Extractor、Responder、ルーティング、ミドルウェアなどが組み合わさったエンドツーエンドに近い動作を検証できます。
発展的なトピック
Axumを使ったモダンなWeb開発は、この記事で触れた基本的な内容にとどまりません。より高度な機能やパターンを利用することで、様々な要件に対応できます。
- WebSockets: リアルタイム通信のためにWebSocketsを使用する場合、AxumはWebSocketプロトコルのハンドリングをサポートしています。
axum::extract::ws::{WebSocketUpgrade, WebSocket}
Extractorとaxum::response::IntoResponse
を実装するWebSocket接続を確立するための機能が提供されています。 - gRPC: HTTP/2をベースにした高性能なRPCフレームワークであるgRPCとAxumを組み合わせることも可能です。
tonic
のようなRustのgRPC実装はTowerのService
トレイトを実装しているため、axum::Router::nest_service
などを使ってgRPCサービスをHTTPサーバーと同じポートで提供できます。 - Type Stateパターン: ルーターの設定において、特定のレイヤーが適用されたルーターの型が変化し、それによって適用できる次のレイヤーや操作が制限されるというType StateパターンがAxumの一部で使用されています。これはコンパイル時安全性を提供しますが、ルーターの型シグネチャが複雑になる場合があります。理解することで、より安全なアプリケーション設定が可能になります。
- プロダクションデプロイメント: 実際のプロダクション環境にAxumアプリケーションをデプロイする際には、考慮すべき点がいくつかあります。
- シグナルハンドリング: SIGINT (Ctrl+C) や SIGTERM などの終了シグナルを捕捉し、サーバーをグレースフルシャットダウン(処理中のリクエストを完了させてから終了)させる必要があります。Tokioはシグナルハンドリング機能を提供しています。
- ログと監視:
tracing
とtracing-subscriber
を使って、構造化されたログを適切に出力し、ログ収集システムや監視ツールと連携させることが重要です。 - 設定管理: アプリケーション設定(データベースURL、ポート番号など)を環境変数や設定ファイルから読み込むためのライブラリ(
config
,dotenv
など)を使用します。 - セキュリティ: HTTPSの有効化、適切なヘッダーの設定(HSTS, CSPなど)、依存クレートの脆弱性スキャンなどを実施します。
これらの発展的なトピックは、アプリケーションの規模や要件に応じて学習・適用していくことになります。AxumはTokioやTowerといった確立されたエコシステムの上に構築されているため、これらのエコシステムの他のライブラリと組み合わせることで、様々な高度な機能を実装できます。
まとめと次のステップ
この記事では、RustのWebフレームワークAxumを使ったモダンなWebアプリケーション開発の第一歩として、その基礎概念、環境構築、ルーティング、ハンドラ、Extractor、Responder、状態管理、ミドルウェア、エラーハンドリング、テスト、そして非同期処理について詳しく解説しました。
Axumは、Rustの安全性とパフォーマンスをWeb開発にもたらす強力なツールです。Tokio、Tower、HyperといったRustエコシステムの堅牢な基盤の上に構築されており、Extractor/ResponderパターンやTowerの柔軟なミドルウェアシステムによって、表現力豊かで保守性の高いコードを書くことができます。
Rustの所有権システムや非同期処理には慣れが必要ですが、一度習得すれば、コンパイル時に多くのバグを防ぎ、実行時に優れたパフォーマンスを発揮するWebサービスを開発できます。
モダンなWebアプリケーションのバックエンドや高性能なAPIをRustで構築したいと考えているなら、Axumは非常に魅力的な選択肢です。
次のステップとして、以下のことをお勧めします。
- Axum公式ドキュメントを読む: Axumの
examples
ディレクトリには、様々な機能を使った具体的なコード例が豊富に含まれています。また、APIドキュメントも非常に詳細です。 - TokioとTowerについて学ぶ: Axumの基盤となっているTokioとTowerについて理解を深めることで、Axumの内部動作や非同期処理、ミドルウェアの仕組みに対する理解が深まります。Tokioの公式チュートリアルは非同期Rustを学ぶ上で非常に優れています。
- 実際に何か作ってみる: 小さなAPIやWebサービスを実際にAxumを使って構築してみましょう。データベース連携 (sqlx, dieselなど)、認証、バリデーションなどの機能を組み込んでみることで、理解が深まります。
- 他の関連クレートを探索する: 認証(
argon2
,jsonwebtokens
)、バリデーション(validator
)、設定(config
,dotenv
)、データベース操作(sqlx
,diesel
)、APIドキュメンテーション生成(utoipa
)など、RustのWeb開発で役立つ様々なクレートがあります。
RustとAxumを使ったWeb開発の旅は始まったばかりです。この強力な組み合わせで、安全かつ高性能なWebアプリケーションを構築していきましょう。