axum とは
axum は Rust 用の Web フレームワークです。
axum は Rust の非同期処理ランタイムの代表格である tokio のサブプロジェクトとして公開されました。 そのため、axum を使ったアプリケーション実装では、tokio が提供するフレームワークを組み合わせて使用します。
Rust 用の Web フレームワークには、他にも Actix Web (actix-web
) や Rocket (rocket
) などがありますが、axum は後発の Web フレームワークで、公開直後の 2022 年頃から利用者が急増しています。
この人気っぷりは、やはり tokio ファミリーのプロジェクトであることが大きいのでしょう。
axum は、他のフレームワークと違って、get
や post
マクロなどを使わないのが特徴的で、マクロ疲れしている人にはぴったりです。
その代わりに、リクエストをハンドルする関数に、extractor と呼ばれる引数を配置することで、リクエストの情報を抽出します。
例えば、次のような extractor を、ハンドラー関数の引数として任意の数だけ配置できます。
axum::extract::Path
… パスパラメーター(URL 内のパスの部分的なセグメント)を抽出するaxum::extract::Query
… URL の末尾のクエリパラメーターを抽出するaxum::extract::Json
… POST メソッドのペイロードとして送られてきた JSON データを抽出するaxum::extract::Request
… リクエスト全体を抽出するhttp::header::Method
… リクエストメソッドを抽出するhttp::header::HeaderMap
… リクエストヘッダーを抽出するString
… リクエスト本文をそのまま utf-8 文字列で取得する
下記はリクエストに使われた HTTP メソッドとヘッダー、本文(ペイロード)を参照するハンドラー関数の引数の例です。
use axum::http::{Method, HeaderMap};
async fn handler(
method: Method, // メソッドを取得したいときはこの引数を配置
headers: HeaderMap, // ヘッダーを取得したいときはこの引数を配置
body: String, // 本文を取得したいときはこの引数を配置
) {
// ...
}
ここでは、axum を使ってシンプルな HTTP サーバーを実装してみます。
プロジェクトの作成
まず、Rust のプロジェクトを作成します。
ここでは、http-server
という名前にします。
必要なライブラリの依存関係を追加します。
通常は、非同期ランタイムの tokio
や JSON を扱うための serde
も必要になります。
まずは Hello World サーバーを作ってみる
下記は、Hello, World!
というテキストを返すだけの、シンプルな Web サーバーの実装例です。
処理の流れはほとんど明らかだと思いますが、URL のルートパス /
に GET メソッドでアクセスしたら root
という名前のハンドラー関数が呼ばれる、という実装ですね。
次のようにして Web サーバーを起動できます。
$ cargo run
Web サーバーを起動した状態で、別のターミナルや Web ブラウザから http://localhost:8080
にアクセスして、メッセージが返ってきたら成功です。
$ curl localhost:8080
Hello, World!
シンプル!
リクエスト時のパスを取得する (axum::extract::Path)
リクエストされた URL に含まれるパス情報(例: example.com/users/123
の 123
の部分)を取得したいときは、axum の extractor のひとつである axum::extract::Path
を使用します。
次の例では、/users/:id
というパスに対するハンドラー関数 get_user()
を定義し、:id
の位置で指定されたパス文字列を取得しています。
ちなみに、/users/:user_id/team/:team_id
のように 2 つ以上のパスパラメーターを抽出するときは、次のようにタプルを使います。
async fn users_teams_create(
Path((user_id, team_id)): Path<(String, String)>,
) {
// ...
}
リクエスト時のクエリ文字列を取得する (axum::extract::Query)
同様に、URL の末尾に指定されたクエリ文字列(例: example.com/search?genre=ACT&year=2000
の genre=ACT&year=2000
の部分)を取得したいときは、axum::extract::Query
を使用します。
serde
のデシリアライズ機能を使うことで、クエリパラメーターを構造体の形で参照できます。
下記はいろいろなクエリパラメーターでリクエストしたときの、レスポンスの例です。
パラメーターの型が不正なとき(例: year=ABC
)は、すべてのパラメーターをデフォルト値(今回はすべて None
)として扱っています。
リクエストのメソッドやヘッダー情報を取得する
HTTP リクエストに使われたメソッド(GET や POST)を取得したいときは、ハンドラー関数の引数として Method
を追加します。
同様に、リクエストヘッダー情報を取得したいときは、ハンドラー関数の引数として HeaderMap
を追加します。
下記は、curl
コマンドで HTTP リクエストを送ったときのサーバー側の出力を示しています。
ステートを保持する (with_state)
HTTP はステートレスなプロトコルなので、基本的に各リクエストは独立したものになりますが、Router.with_state()
メソッドを使うと、リクエスト間で任意のステート情報を共有することができます。
例えば、次のような用途で使用できます。
- アプリケーションの共通設定
- データベース接続のプール
- セッション管理、認証情報の保持
- ロードに時間がかかるデータのキャッシュ
次の例では、ステートとして AppState
構造体の値を保持しています。
Router.with_state()
でセットしたステート情報は、各ハンドラーの引数として State
extractor を配置することで受け取ることができます。
ハンドラーの中でステートの counter
値をインクリメントしているので、curl
や Web ブラウザーでアクセスするたびに次のように出力が変わります。
POST リクエストで送られた JSON データを取得する
POST メソッドで送られてきた本文を単純な utf-8 文字列として取得するだけであれば、次のようにハンドラー関数に String
型の引数を追加するだけですみます。
async fn handler(body: String) {
println!("{}", body);
}
ただ、REST API などを実装しているときは、本文として送られてくるデータは JSON 形式になっていることがほとんどなので、このデータを構造体インスタンスとして参照できると便利です。
axum の JSON
extractor と、serde の Deserialize
属性を使うことでこれを実現できます。
次の例では、本文として送られてきた JSON データを CreateTodoPayload
構造体として参照しています。
レスポンスとして JSON データを返す
構造体データを JSON 形式のレスポンスとして返したいときは、ハンドラー関数の戻り値で axum::Json
を返します(axum::reponse::Json
でも OK)。
Json
コンストラクターには、serde の Serialize
属性をつけた構造体インスタンスを渡します。