これは、なにをしたくて書いたもの?
RustのWebフレームワークをそろそろ試してみようかなということで。
axumを始めてみたいと想います。
RustのWebフレームワーク
RustのWebフレームワークといえば、axumとActix Webが有名なようです。
GitHub - tokio-rs/axum: Ergonomic and modular web framework built with Tokio, Tower, and Hyper
参考。
歴史が長いのがActix Webで、勢いがあるのがaxumといった状況みたいです。
今回はaxumを選びます。
axum
axumは、tokioの開発元が開発しているWebフレームワークです。axumのGitHubリポジトリーはこちら。
GitHub - tokio-rs/axum: Ergonomic and modular web framework built with Tokio, Tower, and Hyper
ドキュメントはクレートのものを読むこと、となっているようです。
現時点のバージョンは0.8.1ですね。
なんと、GitHubリポジトリーには0.8.1のタグがありませんでした…。今回はGitHubリポジトリーのタグについては
0.8.0のものを使います…。
コミュニティによるエコシステムについてはこちらに記載があります。
https://github.com/tokio-rs/axum/blob/axum-v0.8.0/ECOSYSTEM.md
サンプルはこちら。
https://github.com/tokio-rs/axum/tree/axum-v0.8.0/examples
ひとまず、特徴を見てみましょう。
- マクロのないAPIでリクエストをハンドラーにルーティングする
- extractorを使用してリクエストのパースを宣言的に記述する
- シンプルで予測可能なエラーハンドリングモデル
- 最小のボイラーテンプレートでレスポンスを生成する
- ミドルウェア、サービス、ユーティリティーといったエコシステムでtowerおよびtower-httpのエコシステムを最大限に活用する
Crate axum / High-level features
こう見ると、towerとtower-httpが何者なのか気になりますね。
towerはロバストなネットワーククライアントとサーバーを構築するための、モジュールと再利用可能なコンポーネントを
備えたライブラリーとされています。
tower-htttpはtower上に構築されたHTTP固有のミドルウェアとユーティリティーを提供するライブラリーであるとされています。
axumには独自のミドルウェアシステムがなく、代わりにtower::Service
トレイトを使うようです。
Service in tower_service - Rust
またhyperやtonicで書かれたアプリケーションやミドルウェアを共有することもできるようです。
ひとまず、試してみましょう。
環境
今回の環境はこちら。
$ rustup --version rustup 1.27.1 (54dd3d00f 2024-04-24) info: This is the version for the rustup toolchain manager, not the rustc compiler. info: The currently active `rustc` version is `rustc 1.84.1 (e71f9a9a9 2025-01-27)`
axumをインストールする
まずはCargoパッケージの作成。
$ cargo new --vcs none hello-axum $ cd hello-axum
axumのインストール。
$ cargo add axum
axumのフィーチャーフラグはこちらですね。
また、追加でクレートが必要です。少なくともtokioは加える必要があります。JSONを扱う場合はserde_jsonが必要です。
$ cargo add tokio --features macros,rt-multi-thread $ cargo add serde_json
Cargo.toml
[package] name = "hello-axum" version = "0.1.0" edition = "2021" [dependencies] axum = "0.8.1" serde_json = "1.0.138" tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] }
axumを使ってみる
まずは最小構成から。
src/main.rs
use axum::{routing::get, serve, Router}; use tokio::net::TcpListener; #[tokio::main] async fn main() { let app = Router::new().route("/", get(|| async { "Hello World" })); let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap(); serve(listener, app).await.unwrap(); }
/
にアクセスすると、「Hello World」と返すプログラムですね。
起動。
$ cargo run
確認。
$ curl -i localhost:3000 HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 content-length: 11 date: Sun, 02 Feb 2025 09:38:16 GMT Hello World
OKです。
次はルーティングを試してみます。
ソースコードをこんな感じに変更。
src/main.rs
use axum::{routing::get, serve, Router}; use tokio::net::TcpListener; #[tokio::main] async fn main() { let app = Router::new() .route("/", get(root)) .route("/foo", get(get_foo).post(post_foo)) .route("/foo/bar", get(foo_bar)); let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap(); serve(listener, app).await.unwrap(); } async fn root() -> &'static str { "Root" } async fn get_foo() -> &'static str { "Get Foo" } async fn post_foo() -> &'static str { "Post Foo" } async fn foo_bar() -> &'static str { "Get Foo Bar" }
各パスに対して、以下のようなマッピングになっていますね。
- GET / … 「Root」を返す
- GET /foo … 「Get Foo」を返す
- POST /foo … 「Post Foo」を返す
- GET /foo/bar … 「Get Foo Bar」を返す
起動。
$ cargo run
確認。
$ curl -i localhost:3000 HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 content-length: 4 date: Sun, 02 Feb 2025 09:59:33 GMT Root $ curl -i localhost:3000/foo HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 content-length: 7 date: Sun, 02 Feb 2025 09:59:35 GMT Get Foo $ curl -i -XPOST localhost:3000/foo HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 content-length: 8 date: Sun, 02 Feb 2025 09:59:38 GMT Post Foo $ curl -i localhost:3000/foo/bar HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 content-length: 11 date: Sun, 02 Feb 2025 09:59:40 GMT Get Foo Bar
OKですね。
Router
構造体のroute
メソッドの説明を見ていると、パスの変数化やワイルドカードも取れるようです。
ハンドラーについてはドキュメントだけですね。
そもそもハンドラーというのは、ルーティングに設定している非同期関数のことのようです。
ハンドラーは、Handler
トレイトを実装している必要があります。
Handler in axum::handler - Rust
ハンドラー(Handler
トレイトの実装)に求めるのは、以下の内容のようです。
async
関数である- 引数は16個以下で、すべて
Send
トレイトを実装していること- 最後の引数以外は
FromRequestParts
トレイトを実装している - 最後の引数は
FromRequest
トレイトを実装している
- 最後の引数以外は
- 戻り値は
IntoResponse
トレイトを実装している - クロージャーを使用する場合は、
Clone + Send
を実装し、かつ'static
であるべき - futureである
Send
を返す
Module handler / Debugging handler type errors
Send
トレイトというのはこちらですね。
extractorやレスポンスについてはこちら。
extractorに進んでみます。extractorは、受信したリクエストを分解してハンドラーが必要とする部分を取得する機能を
もつものです。
パスパラメーターやQueryString、JSONといったものですね。
レスポンスも合わせて。
src/main.rs
use std::collections::HashMap; use axum::{ extract::{Path, Query}, routing::{get, post}, serve, Json, Router, }; use serde_json::{json, Value}; use tokio::net::TcpListener; #[tokio::main] async fn main() { let app = Router::new() .route("/", get(root)) .route("/foo/{id}", get(get_id)) .route("/query", get(get_query)) .route("/json", post(post_json)); let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap(); serve(listener, app).await.unwrap(); } async fn root() -> &'static str { "Hello World" } async fn get_id(Path(id): Path<u32>) -> String { format!("id = {}", id) } async fn get_query(Query(query): Query<HashMap<String, String>>) -> String { format!("query word = {}", query.get("word").unwrap()) } async fn post_json(Json(payload): Json<Value>) -> Json<Value> { Json(json!({ "message": payload["message"], "result": payload["a"].as_number().unwrap().as_i64().unwrap() + payload["b"].as_number().unwrap().as_i64().unwrap() })) }
起動。
$ cargo run
確認。
$ curl -i localhost:3000 HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 content-length: 11 date: Sun, 02 Feb 2025 10:51:55 GMT Hello World $ curl -i localhost:3000/foo/123 HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 content-length: 8 date: Sun, 02 Feb 2025 10:52:02 GMT id = 123 $ curl -i localhost:3000/query?word=Hello HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 content-length: 18 date: Sun, 02 Feb 2025 10:52:09 GMT query word = Hello $ curl -i -XPOST -H 'Content-Type: application/json' localhost:3000/json -d '{"message": "Hello World", "a": 5, "b": 3}' HTTP/1.1 200 OK content-type: application/json content-length: 36 date: Sun, 02 Feb 2025 10:52:14 GMT {"message":"Hello World","result":8}
OKですね。
まずはこんなところでしょうか。
おわりに
RustのWebフレームワーク、axumを試してみました。
まだまだRustに慣れていないのにこういうのにチャレンジしていっていますが、やらないと覚えないのでこういうのを
扱っていきつつRustに慣れていきたいですね。