CLOVER🍀

That was when it all began.

RustのWebフレームワーク、axumを試してみる

これは、なにをしたくて書いたもの?

RustのWebフレームワークをそろそろ試してみようかなということで。

axumを始めてみたいと想います。

RustのWebフレームワーク

RustのWebフレームワークといえば、axumとActix Webが有名なようです。

Actix Web

GitHub - tokio-rs/axum: Ergonomic and modular web framework built with Tokio, Tower, and Hyper

参考。

Web Frameworks » AWWY?

歴史が長いのが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

ドキュメントはクレートのものを読むこと、となっているようです。

axum - Rust

現時点のバージョンは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 - Rust

tower-htttpはtower上に構築されたHTTP固有のミドルウェアとユーティリティーを提供するライブラリーであるとされています。

tower_http - Rust

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のフィーチャーフラグはこちらですね。

Crates axum / Feature flags

また、追加でクレートが必要です。少なくとも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を使ってみる

まずは最小構成から。

Crates axum / Example

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です。

次はルーティングを試してみます。

Crates axum / Routing

Router in axum - Rust

ソースコードをこんな感じに変更。

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メソッドの説明を見ていると、パスの変数化やワイルドカードも取れるようです。

Struct Router / route

ハンドラーについてはドキュメントだけですね。

Crates axum / Handlers

そもそもハンドラーというのは、ルーティングに設定している非同期関数のことのようです。

axum::handler - Rust

ハンドラーは、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トレイトというのはこちらですね。

Send in core::marker - Rust

extractorやレスポンスについてはこちら。

axum::extract - Rust

axum::response - Rust

extractorに進んでみます。extractorは、受信したリクエストを分解してハンドラーが必要とする部分を取得する機能を
もつものです。

Crates axum / Extractors

パスパラメーターやQueryString、JSONといったものですね。

レスポンスも合わせて。

Crates axum / Responses

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ですね。

JSONの操作はserde_jsonを使うようです。

serde_json - Rust

まずはこんなところでしょうか。

おわりに

RustのWebフレームワーク、axumを試してみました。

まだまだRustに慣れていないのにこういうのにチャレンジしていっていますが、やらないと覚えないのでこういうのを
扱っていきつつRustに慣れていきたいですね。