【入門】Rust Webフレームワーク 主要3選を徹底解説
Rustはその安全性、パフォーマンス、並行処理能力の高さから、近年バックエンド開発の分野で注目を集めています。特に、高い信頼性や応答速度が求められるシステムにおいて、Rustは強力な選択肢となり得ます。しかし、RustでWebアプリケーションやAPIを構築するには、適切なWebフレームワークを選ぶことが重要です。
この記事では、RustのWeb開発に入門する方を対象に、現在人気があり、広く使われている主要なWebフレームワークを3つピックアップし、それぞれの特徴、使い方、メリット・デメリットを徹底的に解説します。約5000語というボリュームで、各フレームワークの基本的な使い方から、JSON処理、エラーハンドリング、ステート管理、ミドルウェアといった実践的なトピックまで深く掘り下げていきます。
Rustの基本的な文法や非同期プログラミング(async
/await
)にある程度慣れている方を想定していますが、必要な知識は記事の中で補足していきます。
さあ、RustでのWeb開発の世界に飛び込みましょう!
1. はじめに:なぜRustでWeb開発をするのか?
Rustはシステムプログラミング言語として知られていますが、その強力な機能はWeb開発にも大いに活かせます。RustでWebアプリケーションやAPIを構築する主なメリットは以下の通りです。
- 安全性: Rustの所有権システムと借用チェッカーにより、コンパイル時にデータ競合やヌルポインタ参照といった一般的なバグを防ぐことができます。これにより、実行時のクラッシュやセキュリティホールが大幅に減少します。メモリ安全性は、特に大規模で複雑なWebサービスにおいて非常に重要です。
- パフォーマンス: Rustはガベージコレクションを持たず、メモリ管理を開発者がより細かく制御できます。これにより、非常に高速でメモリ効率の良いアプリケーションを構築できます。WebサーバーやAPIの応答速度は、ユーザー体験やシステムのスケーラビリティに直結するため、Rustのパフォーマンスは大きな強みとなります。
- 並行処理: Rustはデータ競合なしに安全な並行処理を書くための強力な抽象化を提供します。Actix-webやAxumのようなフレームワークは、非同期ランタイム(主にTokio)を活用して、多数の同時接続を効率的に処理できます。これは高負荷なWebサービスにとって不可欠です。
- 信頼性: 厳格なコンパイル時のチェックと強力な型システムにより、本番環境で予期せぬエラーが発生する可能性を低減できます。
- 軽量: Rustでビルドされたバイナリは自己完結しており、ランタイムを必要としません(特定のフレームワークや機能に依存する場合を除く)。これにより、デプロイが容易になり、コンテナ環境などでも効率的に動作します。
これらのメリットを享受しつつ、Web開発を効率的に進めるためには、適切なフレームワークの選択が不可欠です。フレームワークは、ルーティング、リクエスト/レスポンス処理、ミドルウェア、ステート管理など、Webアプリケーション開発に必要な定型的なタスクを抽象化し、開発を加速させます。
この記事では、特に人気の高い以下の3つのフレームワークに焦点を当てて解説します。
- Actix-web: 成熟しており、非常に高いパフォーマンスを誇るアクターモデルベースのフレームワーク。
- Warp: 関数型アプローチに基づいた、モジュール性が高く軽量なフレームワーク。
- Axum: TokioとTowerエコシステムの上に構築された、シンプルで柔軟性の高い最新のフレームワーク。
これらのフレームワークはそれぞれ異なる設計思想を持っており、得意な分野や開発スタイルが異なります。この記事を読めば、それぞれの特徴を理解し、ご自身のプロジェクトに最適なフレームワークを選択できるようになるはずです。
2. Rust Web開発の基礎知識
Webフレームワークの解説に入る前に、RustでWeb開発を行う上で理解しておくべき基本的な概念について触れておきます。
2.1. HTTPの基本
Web開発はHTTPプロトコルに基づいています。HTTPはクライアント(ブラウザなど)とサーバー間でデータをやり取りするための規約です。
- リクエスト (Request): クライアントがサーバーに送る要求です。URL(パス)、HTTPメソッド(GET, POST, PUT, DELETEなど)、ヘッダー(追加情報)、ボディ(POSTなどで送信するデータ)を含みます。
- レスポンス (Response): サーバーがクライアントに送る応答です。ステータスコード(200 OK, 404 Not Found, 500 Internal Server Errorなど)、ヘッダー、ボディ(HTML、JSON、画像などのデータ)を含みます。
- ルーティング (Routing): incoming リクエストのパスやメソッドに応じて、どのコード(ハンドラー)を実行するかを決定する仕組みです。
- ハンドラー (Handler): 特定のリクエストパスとメソッドに対応する処理を行う関数やメソッドです。リクエストを処理し、レスポンスを生成します。
Webフレームワークは、これらのHTTPの要素をRustの構造体や関数として扱いやすくするための抽象化を提供します。
2.2. 非同期プログラミング (async
/await
, Tokio)
RustのWebフレームワークは、高い並行性を実現するために非同期プログラミングを広く利用しています。非同期処理は、あるタスク(例えばデータベースへの問い合わせや外部APIへのリクエスト)が完了するのを待っている間に、他のタスクを実行できるようにする手法です。これにより、一つのサーバープロセスで多数の同時リクエストを効率的に処理できます。
async
/await
: Rustにおける非同期プログラミングの中核をなすキーワードです。async fn
で定義された関数は、実行を一時停止して後で再開できる非同期タスク(Future)を返します。await
キーワードは、Futureが完了するのを待つために使われます。await
を使用すると、現在のタスクはブロックされずに他のタスクにCPUを明け渡すことができます。- Future: 非同期操作の結果を表すトレイトです。まだ完了していない処理や、将来利用可能になる値を表現します。
- 非同期ランタイム:
async
/await
で書かれた非同期タスクを実行するための環境です。Futureのポーリングを行い、完了したタスクを検出して実行を再開させます。Rustの非同期Web開発で最も広く使われているランタイムはTokioです。Actix-web, Warp, AxumはいずれもTokioをサポートしており、特にAxumはTokioとTowerエコシステムの上に構築されています。
Webフレームワークのハンドラー関数は、多くの場合async fn
として定義され、await
を使って非同期操作を行います。
2.3. クレートと依存関係 (Cargo.toml
)
Rustのプロジェクトはクレート(crate)と呼ばれるパッケージで構成されます。外部のライブラリ(Webフレームワーク、データベースドライバ、JSONパーサーなど)を利用するには、プロジェクトのルートにあるCargo.toml
ファイルに依存関係として記述します。
例えば、Actix-webとTokioを使いたい場合は、Cargo.toml
の[dependencies]
セクションに以下のように追加します。
toml
[dependencies]
actix-web = "4" # Actix-web フレームワーク本体
tokio = { version = "1", features = ["full"] } # Tokio ランタイム
serde = { version = "1", features = ["derive"] } # JSON 処理などに使う Serde
serde_json = "1" # JSON 処理に使う Serde の実装
2.4. ミドルウェア (Middleware)
ミドルウェアは、リクエストがハンドラーに到達する前や、ハンドラーがレスポンスを返した後に、追加の処理を実行するための仕組みです。複数のリクエスト/レスポンスで共通して実行したい処理(例:ロギング、認証、CORSヘッダーの追加、エラー処理)をミドルウェアとして定義することで、コードの重複を防ぎ、保守性を高めることができます。
各フレームワークは独自のミドルウェアの概念や実装方法を持っていますが、基本的な目的は同じです。
これらの基礎知識を踏まえた上で、いよいよ主要なRust Webフレームワーク3選を詳しく見ていきましょう。
3. 主要Webフレームワーク徹底解説
3.1. Actix-web
- 公式サイト: https://actix.rs/
- 特徴: Actix-webは、Rustで最もパフォーマンスの高いWebフレームワークの一つとして知られています。アクターモデルをベースに設計されており、並行処理を効率的に行います。成熟しており、大規模なWebアプリケーションやAPIの構築に適しています。
3.1.1. 特徴と設計思想
Actix-webは、AkkaやErlangに影響を受けたアクターモデルを採用しています(Actix 3.xまではより強くアクターモデルに依存していましたが、4.xでTokioベースになり、アクターの直接的な利用は必須ではなくなりました。しかし、その設計思想は随所に現れています)。アクターは独立した軽量な並行処理単位であり、メッセージパッシングによって通信します。これにより、スレッド間でのミュータブルな状態共有を避け、安全かつ効率的な並行処理を実現します。
Actix-webは、高いパフォーマンスと豊富な機能を両立させているのが強みです。ルーティング、リクエスト抽出(パスパラメーター、クエリ、JSONなど)、レスポンス生成、エラーハンドリング、ミドルウェア、テストユーティリティなど、Web開発に必要な機能が一通り揃っています。
3.1.2. 基本的な使い方
Actix-webアプリケーションの基本的な構造を見てみましょう。
まず、新しいプロジェクトを作成し、依存関係を追加します。
bash
cargo new my-actix-app
cd my-actix-app
Cargo.toml
に依存関係を追加します。
toml
[dependencies]
actix-web = "4"
tokio = { version = "1", features = ["full"] } # 非同期ランタイムとして使用
serde = { version = "1", features = ["derive"] } # JSON 処理用
serde_json = "1"
src/main.rs
に以下のコードを書きます。
“`rust
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};
// リクエストボディ用の構造体
[derive(Deserialize)]
struct Info {
username: String,
}
// レスポンスボディ用の構造体
[derive(Serialize)]
struct User {
id: u32,
username: String,
}
// シンプルなハンドラー関数
async fn hello() -> impl Responder {
HttpResponse::Ok().body(“Hello world!”)
}
// 非同期ハンドラー関数(ルートマクロを使用)
[get(“/async_hello”)]
async fn async_hello() -> impl Responder {
HttpResponse::Ok().body(“Hello async world!”)
}
// パスパラメータ付きハンドラー
// パスパラメータは web::Path<T>
で抽出
async fn greet(path: web::Path
let name = path.into_inner();
HttpResponse::Ok().body(format!(“Hello {}!”, name))
}
// クエリストリング付きハンドラー
// クエリストリングは web::Query<T>
で抽出
[derive(Deserialize)]
struct QueryInfo {
name: String,
}
async fn query_greet(info: web::Query
HttpResponse::Ok().body(format!(“Hello {} from query!”, info.name))
}
// JSON リクエストボディを受け取るハンドラー
// JSON ボディは web::Json<T>
で抽出
async fn json_input(info: web::Json
HttpResponse::Ok().body(format!(“Received username: {}”, info.username))
}
// JSON レスポンスを返すハンドラー
async fn get_user() -> impl Responder {
let user = User {
id: 1,
username: “test_user”.to_string(),
};
// web::Json は Content-Type: application/json を自動で設定
HttpResponse::Ok().json(user)
}
[actix_web::main] // main 関数を async にするためのマクロ (Tokio を使う場合)
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
// ルーティングを定義
.route(“/”, web::get().to(hello)) // GET / に hello 関数を紐付け
.service(async_hello) // #[get(“/async_hello”)] マクロで定義したハンドラーを登録
.route(“/greet/{name}”, web::get().to(greet)) // GET /greet/{name} に greet 関数を紐付け
.route(“/query_greet”, web::get().to(query_greet)) // GET /query_greet?name=… に query_greet 関数を紐付け
.route(“/json_input”, web::post().to(json_input)) // POST /json_input に json_input 関数を紐付け
.route(“/user”, web::get().to(get_user)) // GET /user に get_user 関数を紐付け
})
.bind((“127.0.0.1”, 8080))? // サーバーを 127.0.0.1:8080 にバインド
.run() // サーバーを実行
.await // 非同期にサーバーの終了を待つ
}
“`
このコードは、以下のエンドポイントを持つシンプルなWebサーバーを起動します。
GET /
: “Hello world!” を返します。GET /async_hello
: “Hello async world!” を返します。GET /greet/{name}
: パスパラメーター{name}
を取得し、”Hello {name}!” を返します。例:/greet/Rust
は “Hello Rust!” を返します。GET /query_greet?name={name}
: クエリストリングname
を取得し、”Hello {name} from query!” を返します。例:/query_greet?name=User
は “Hello User from query!” を返します。POST /json_input
(ボディにJSON{ "username": "..." }
): リクエストボディのJSONを構造体にデシリアライズし、受け取ったユーザー名を返します。GET /user
: 構造体をJSONにシリアライズして返します。
HttpServer::new
のクロージャ内でApp::new()
を呼び出し、ルーティングルールを定義します。web::get()
, web::post()
などのメソッドでHTTPメソッドを指定し、.to()
でハンドラー関数を紐付けます。#[actix_web::main]
マクロは、main
関数を非同期関数として実行可能にし、内部でTokioランタイムを起動します。
3.1.3. リクエストデータの抽出
Actix-webでは、リクエストデータをハンドラー関数の引数として抽出子(Extractor)を使って取得します。主な抽出子には以下があります。
web::Path<T>
: パスパラメーターweb::Query<T>
: クエリストリングweb::Json<T>
: JSONリクエストボディ (POST, PUTなど)web::Form<T>
: URLエンコードされたフォームデータ (POST, PUTなど)web::Data<T>
: アプリケーション全体またはスコープごとのステートデータHttpRequest
: 元のリクエストオブジェクト全体HeaderMap
: リクエストヘッダー全体
これらの抽出子は、T
にリクエストデータの構造を定義したRustの構造体を指定します。構造体には、serde
クレートのDeserialize
トレイトを派生させる必要があります(#[derive(Deserialize)]
)。
3.1.4. JSONの処理
web::Json<T>
はリクエストボディのJSONを、HttpResponse::Ok().json(value)
はレスポンスボディをJSONとして扱うための便利な方法です。これらを使うためには、serde
クレートで構造体にSerialize
やDeserialize
を derive する必要があります。web::Json
は自動的にContent-Type: application/json
ヘッダーを設定してくれます。
3.1.5. エラーハンドリング
Actix-webのハンドラーは、impl Responder
を返すのが一般的です。Responder
トレイトを実装する型であれば、HTTPレスポンスに変換できます。HttpResponse
構造体は最も基本的なResponder
の実装です。
より複雑なケースやエラーを返す場合、ハンドラー関数はResult<impl Responder, Error>
のようなシグネチャを持つことができます。Error
はActix-webのエラー型や、独自のエラー型にResponseError
トレイトを実装したものを指定します。Actix-webは、Err
を返した場合に適切なHTTPエラーレスポンス(例: 400 Bad Request, 500 Internal Server Error)に変換してくれます。
カスタムエラーハンドリングやエラーレスポンスのカスタマイズも可能です。
3.1.6. ミドルウェア
ミドルウェアはApp::new()
に.wrap()
メソッドを使って追加します。Actix-webは様々な組み込みミドルウェアを提供しており、また独自のミドルウェアを作成することも容易です。
一般的なミドルウェアの例:
Logger
: リクエスト情報をログに出力します。NormalizePath
: 末尾のスラッシュを正規化します。Cors
: クロスオリジンリソース共有を制御します。
ロギングミドルウェアを追加する例:
“`rust
use actix_web::{middleware::Logger, App, HttpServer};
[actix_web::main]
async fn main() -> std::io::Result<()> {
// RUST_LOG 環境変数でログレベルを制御できるように初期化
std::env::set_var(“RUST_LOG”, “debug”);
env_logger::init(); // env_logger クレートが必要
HttpServer::new(|| {
App::new()
.wrap(Logger::default()) // ロギングミドルウェアを追加
// ... 他のルーティング ...
})
// ...
.run()
.await
}
“`
3.1.7. ステート管理
アプリケーション全体で共有したいデータ(例:データベースコネクションプール、設定情報)は、web::Data
を使って管理します。web::Data::new()
でデータをラップし、App::new().app_data()
で登録します。ハンドラー関数ではweb::Data<T>
抽出子を使ってデータにアクセスできます。
“`rust
use actix_web::{web, App, HttpServer, Responder, HttpResponse};
use std::sync::Mutex;
// 共有したいステートの構造体
struct AppState {
app_name: String,
counter: Mutex
}
async fn show_app_name(data: web::Data
HttpResponse::Ok().body(format!(“App name: {}”, data.app_name))
}
async fn increment_counter(data: web::Data
let mut counter = data.counter.lock().unwrap(); // Mutex をロック
counter += 1;
HttpResponse::Ok().body(format!(“Counter: {}”, counter))
}
[actix_web::main]
async fn main() -> std::io::Result<()> {
let shared_data = web::Data::new(AppState {
app_name: “My Actix App”.to_string(),
counter: Mutex::new(0),
});
HttpServer::new(move || { // shared_data をクロージャに移動するために move を使用
App::new()
.app_data(shared_data.clone()) // ステートをアプリケーション全体で利用可能にする
.route("/name", web::get().to(show_app_name))
.route("/increment", web::get().to(increment_counter))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
“`
web::Data
は内部的にArc
(Atomic Reference Counting)を使っているため、複数のワーカーや非同期タスク間で安全に共有できます。ミュータブルなデータ(例:カウンター、データベースコネクションプール)を共有する場合は、Mutex
やRwLock
などの適切な同期プリミティブで保護する必要があります。
3.1.8. テストの書き方
Actix-webはアプリケーションのテストを容易にするためのテストユーティリティを提供しています。actix_web::test
モジュールを使用します。
“`rust
[cfg(test)]
mod tests {
use super::*; // 外側のモジュール(main.rs)の要素をインポート
use actix_web::{test, App, http::StatusCode};
#[actix_web::test] // actix-web のテストマクロ
async fn test_hello_world() {
let app = test::init_service(App::new().route("/", web::get().to(hello))).await; // アプリケーションを初期化
let req = test::TestRequest::get().uri("/").to_request(); // テストリクエストを作成
let resp = test::call_service(&app, req).await; // リクエストを実行し、レスポンスを取得
assert_eq!(resp.status(), StatusCode::OK); // ステータスコードを確認
let body = test::read_body(resp).await; // レスポンスボディを読み込み
assert_eq!(body, "Hello world!"); // レスポンスボディを確認
}
#[actix_web::test]
async fn test_greet_name() {
let app = test::init_service(App::new().route("/greet/{name}", web::get().to(greet))).await;
let req = test::TestRequest::get().uri("/greet/testuser").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = test::read_body(resp).await;
assert_eq!(body, "Hello testuser!");
}
// JSON POST リクエストのテスト例
#[actix_web::test]
async fn test_json_input() {
let app = test::init_service(App::new().route("/json_input", web::post().to(json_input))).await;
let payload = r#"{"username": "test_json"}"#;
let req = test::TestRequest::post()
.uri("/json_input")
.set_payload(payload) // リクエストボディを設定
.insert_header(("Content-Type", "application/json")) // ヘッダーを設定
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = test::read_body(resp).await;
assert_eq!(body, "Received username: test_json");
}
}
“`
#[actix_web::test]
マクロは、Tokioランタイム上でテスト関数を実行可能にします。test::init_service
でテスト用のアプリケーションインスタンスを作成し、test::TestRequest
でリクエストを構築、test::call_service
でリクエストを実行します。
3.1.9. メリットとデメリット
-
メリット:
- 非常に高いパフォーマンス: 様々なベンチマークで常に上位にランクインします。
- 成熟度: 長い開発期間と活発なコミュニティにより、機能が豊富で安定しています。
- 機能網羅性: ルーティング、抽出子、エラーハンドリング、ミドルウェア、テストなど、Web開発に必要な機能が一通り揃っています。
- 大規模プロジェクトへの適性: アクターモデルの設計思想が、複雑な並行処理を持つ大規模アプリケーションに適しています。
-
デメリット:
- アクターモデルの学習コスト: Actix 4.xでアクターの直接的な利用は減りましたが、フレームワークの内部設計や一部の高度な機能ではアクターモデルの理解が役立つ場合があります。
- 非同期処理の記述: 他のフレームワークと比較して、一部の非同期処理の記述がやや冗長に感じられることがあるかもしれません。
- コンパイル時間: 機能が豊富なため、他の軽量なフレームワークと比較してコンパイル時間が長くなる傾向があります。
Actix-webは、高いパフォーマンスと安定性を求める大規模なWebサービスやAPIゲートウェイの構築に特に適しています。
3.2. Warp
- 公式サイト: https://github.com/seanmonstar/warp
- 特徴: Warpは、RustのFutureとStreamに基づいた、フィルターという概念を中心にした関数型アプローチのWebフレームワークです。非常にモジュール性が高く、軽量で、パイプラインのようにリクエスト処理を組み立てることができます。
3.2.1. 特徴と設計思想
Warpは、warp::Filter
トレイトを中核として設計されています。フィルターは、 incoming リクエストを検査・変換したり、値を抽出したりする機能を持つコンポーネントです。複数のフィルターを演算子(and
, or
, map
, then
など)を使って組み合わせることで、複雑なルーティングやリクエスト処理パイプラインを構築します。この関数型で宣言的なアプローチがWarpの最大の特徴です。
WarpはTokio上で動作し、非同期処理を効果的に利用します。シンプルで軽量な設計を目指しており、必要最低限の機能だけを提供し、他のクレートとの連携を重視しています。
3.2.2. 基本的な使い方
Warpアプリケーションの基本的な構造を見てみましょう。
新しいプロジェクトを作成し、依存関係を追加します。
bash
cargo new my-warp-app
cd my-warp-app
Cargo.toml
に依存関係を追加します。
toml
[dependencies]
warp = "0.3" # Warp フレームワーク本体
tokio = { version = "1", features = ["full"] } # Tokio ランタイム
serde = { version = "1", features = ["derive"] } # JSON 処理用
serde_json = "1"
src/main.rs
に以下のコードを書きます。
“`rust
use warp::{Filter, Reply};
use serde::{Deserialize, Serialize};
// リクエストボディ用の構造体
[derive(Deserialize)]
struct Info {
username: String,
}
// レスポンスボディ用の構造体
[derive(Serialize)]
struct User {
id: u32,
username: String,
}
[tokio::main] // Tokio ランタイムで main 関数を実行
async fn main() {
// GET /
let hello_route = warp::path::end() // パスが完全に終了しているか (つまり “/”)
.map(|| “Hello world!”); // ハンドラー関数 (非同期でも可)
// GET /async_hello
let async_hello_route = warp::path!("async_hello") // パスが /async_hello か
.and(warp::get()) // GET メソッドか
.and_then(|| async { // 非同期ハンドラー
Ok::<_, warp::Rejection>("Hello async world!") // Result を返す (Rejection はエラー型)
});
// GET /greet/{name}
let greet_route = warp::path!("greet" / String) // パスが /greet/{name} か。String はパスパラメータを抽出
.map(|name| format!("Hello {}!", name)); // パスパラメータを受け取るハンドラー
// GET /query_greet?name={name}
#[derive(Deserialize)]
struct QueryInfo {
name: String,
}
let query_greet_route = warp::path!("query_greet")
.and(warp::get())
.and(warp::query::<QueryInfo>()) // クエリパラメータを抽出
.map(|info: QueryInfo| format!("Hello {} from query!", info.name));
// POST /json_input (ボディにJSON)
let json_input_route = warp::path!("json_input")
.and(warp::post())
.and(warp::body::json()) // JSON リクエストボディを抽出
.map(|info: Info| format!("Received username: {}", info.username));
// GET /user (JSON レスポンス)
let get_user_route = warp::path!("user")
.and(warp::get())
.map(|| { // シンプルなマップハンドラー (非同期でも可)
let user = User {
id: 1,
username: "test_user".to_string(),
};
warp::reply::json(&user) // JSON レスポンスを生成
});
// 全てのルートを結合
let routes = hello_route
.or(async_hello_route)
.or(greet_route)
.or(query_greet_route)
.or(json_input_route)
.or(get_user_route);
// サーバーを起動
println!("Server running on http://127.0.0.1:8080");
warp::serve(routes)
.run(([127, 0, 0, 1], 8080)) // IPアドレスとポートを指定
.await; // 非同期にサーバーの終了を待つ
}
“`
Warpでは、ルーティングとリクエスト処理はフィルターの組み合わせとして表現されます。
warp::path::end()
: パスがルート (/
) であることをフィルタリングします。warp::path!("segment" / Type / ...)
: パスセグメントをフィルタリングし、パスパラメーターを抽出します。warp::method()
: HTTPメソッドをフィルタリングします。warp::query::<T>()
: クエリ文字列を抽出します。warp::body::json()
: JSONリクエストボディを抽出します。filter1.and(filter2)
: filter1 と filter2 の両方が一致した場合に通過する新しいフィルターを作成します。抽出された値はタプルとして結合されます。filter1.or(filter2)
: filter1 または filter2 のいずれかが一致した場合に通過する新しいフィルターを作成します。filter.map(|extracted_value| ...)
: フィルターによって抽出された値を使ってレスポンスを生成する同期ハンドラーを定義します。filter.and_then(|extracted_value| async { ... })
: フィルターによって抽出された値を使ってレスポンスを生成する非同期ハンドラーを定義します。非同期ハンドラーはResult<impl Reply, Rejection>
を返す必要があります。Reply
トレイトを実装する型がレスポンスに変換されます。warp::reply::json(&value)
はReply
を実装しており、JSONレスポンスを生成します。warp::serve(routes)
: サーバービルダーを作成し、指定されたフィルター(ルーティング)に基づいてリクエストを処理します。.run((ip, port))
: サーバーを指定されたアドレスで実行します。
#[tokio::main]
マクロは、main
関数を非同期関数として実行し、内部でTokioランタイムを起動します。
3.2.3. リクエストデータの抽出
Warpでは、リクエストデータの抽出もフィルターを使って行います。
warp::path::param<T>()
: パスパラメーターを抽出します。warp::path!("segment" / T)
の形式でも抽出できます。warp::query::<T>()
: クエリ文字列をT
という構造体にデシリアライズして抽出します(#[derive(Deserialize)]
が必要)。warp::header::<T>(name)
: 指定した名前のヘッダーを抽出します。warp::body::json()
: JSONリクエストボディを抽出します(#[derive(Deserialize)]
が必要)。warp::body::form()
: URLエンコードされたフォームデータを抽出します(#[derive(Deserialize)]
が必要)。warp::addr::remote()
: リモートアドレスを抽出します。
これらの抽出フィルターは、and
を使って他のフィルターと組み合わせ、ハンドラーに値を提供します。
3.2.4. JSONの処理
WarpでJSONを扱うには、warp::body::json()
フィルターを使ってリクエストボディを抽出し、warp::reply::json(&value)
関数を使って構造体をJSONレスポンスに変換します。どちらの場合もserde
のDeserialize
やSerialize
トレイトが必要になります。warp::reply::json
は自動的にContent-Type: application/json
ヘッダーを設定します。
3.2.5. エラーハンドリング
WarpではエラーはRejection
という形で扱われます。フィルターがリクエストを処理できない場合(例:パスが一致しない、必要なヘッダーがない)、そのフィルターはリクエストをreject
します。一連のフィルターのパイプラインで最終的にリクエストがreject
された場合、WarpはRejection
を適切なHTTPエラーレスポンスに変換しようとします。
デフォルトのエラーハンドリングはシンプルですが、.recover()
メソッドを使って独自のエラーリカバリーロジックを定義できます。これにより、特定のエラータイプに対してカスタムなエラーレスポンスを返すことが可能です。
例:カスタムエラーレスポンス
“`rust
use warp::{Filter, Rejection, Reply, http::StatusCode};
[derive(Debug)]
struct MyCustomError;
impl warp::reject::Reject for MyCustomError {} // MyCustomError を Rejection として扱えるようにする
// このハンドラーは MyCustomError を reject する可能性がある
async fn might_fail() -> Result
// … 処理 …
if / 失敗条件 / true {
Err(warp::reject::custom(MyCustomError)) // カスタムエラーを reject
} else {
Ok(“Success!”)
}
}
// Rejection を Reply に変換するリカバリー関数
async fn handle_rejection(err: Rejection) -> Result
if err.is_not_found() { // 404 Not Found の場合
Ok(warp::reply::with_status(“Not Found”, StatusCode::NOT_FOUND))
} else if let Some(_) = err.find::
eprintln!(“Got a custom error: {:?}”, err); // エラーログ出力など
Ok(warp::reply::with_status(“Something went wrong (custom error)”, StatusCode::INTERNAL_SERVER_ERROR))
} else {
// Warp のデフォルトの Rejection をそのまま扱う
Err(err)
}
}
[tokio::main]
async fn main() {
let routes = warp::path(“test”)
.and_then(might_fail)
.recover(handle_rejection); // エラーリカバリーを追加
warp::serve(routes)
.run(([127, 0, 0, 1], 8080))
.await;
}
“`
.recover()
に渡される関数はRejection
を受け取り、Result<impl Reply, Rejection>
を返します。Ok
を返すとそのReply
がクライアントに返され、Err(Rejection)
を返すと、そのRejection
がさらに上位のリカバリー関数またはWarpのデフォルトハンドラーに渡されます。
3.2.6. ミドルウェア
Warpは明示的な「ミドルウェア」の概念を持っていませんが、フィルターを組み合わせることで同様の機能を実現します。例えば、ロギングや認証といった共通処理は、リクエストを処理するメインのフィルターの前にand
で連結することで実現できます。
例:ロギングフィルターの追加
“`rust
use warp::{Filter, Reply, http::StatusCode};
use std::time::Instant;
[tokio::main]
async fn main() {
let log = warp::log(“my_app”); // 組み込みのロギングフィルター
let hello_route = warp::path::end()
.map(|| "Hello world!");
let routes = hello_route.with(log); // with メソッドでフィルターチェーンにロギングフィルターを追加
warp::serve(routes)
.run(([127, 0, 0, 1], 8080))
.await;
}
“`
warp::log("...")
は、リクエストとレスポンスの情報をログに出力するフィルターを返します。これを.with()
メソッドでルートに適用することで、そのルートを通る全てのリクエスト/レスポンスに対してロギングが実行されます。
より複雑なミドルウェア(例:カスタムヘッダーの追加、認証チェック)は、独自のフィルター関数を作成するか、map
, and_then
, untuple_one
などのコンビネーターを使ってリクエスト/レスポンスを変換する形で実装します。
3.2.7. ステート管理
Warpでアプリケーション全体またはルートツリー全体で共有するステートを管理するには、warp::any().map(...)
または warp::any().and(warp::any().map(...))
のようにして、warp::Filter
を実装する構造体やArc
でラップしたデータをフィルターチェーンに挿入し、ハンドラー関数がその値を受け取れるようにします。最も一般的な方法はwarp::any().map(move || shared_data.clone())
またはwarp::any().with(warp::any().map(move || shared_data.clone()))
のようなフィルターをルートにand
またはwith
で結合することです。
例:ステート共有
“`rust
use warp::{Filter, Reply, http::StatusCode};
use std::sync::{Arc, Mutex};
// 共有したいステートの構造体
struct AppState {
app_name: String,
counter: Mutex
}
// ステートをハンドラーに渡すためのフィルター
// warp::any() は常にマッチし、何も抽出しないフィルター
// .map() でクロージャを実行し、その結果を次のフィルター/ハンドラーに渡す
fn with_state(state: Arc
warp::any().map(move || state.clone())
}
// ステートを受け取るハンドラー
async fn show_app_name(state: Arc
Ok(state.app_name.clone())
}
// ステートを受け取る別のハンドラー
async fn increment_counter(state: Arc
let mut counter = state.counter.lock().unwrap();
counter += 1;
Ok(format!(“Counter: {}”, counter))
}
[tokio::main]
async fn main() {
let shared_state = Arc::new(AppState {
app_name: “My Warp App”.to_string(),
counter: Mutex::new(0),
});
// ルートに with_state フィルターを and で結合
let name_route = warp::path("name")
.and(warp::get())
.and(with_state(shared_state.clone())) // ステートを結合
.and_then(show_app_name); // ハンドラーは結合された抽出子を受け取る
let increment_route = warp::path("increment")
.and(warp::get())
.and(with_state(shared_state.clone())) // ステートを結合
.and_then(increment_counter); // ハンドラーは結合された抽出子を受け取る
let routes = name_route.or(increment_route);
println!("Server running on http://127.0.0.1:8080");
warp::serve(routes)
.run(([127, 0, 0, 1], 8080))
.await;
}
“`
共有したいデータはArc
でラップし、with_state
のようなヘルパーフィルターを作成してclone
したArc
をハンドラーに渡すのが一般的なパターンです。
3.2.8. テストの書き方
Warpはフィルターベースの設計のため、テストもフィルターに対して行います。warp::test
モジュールを使用します。
“`rust
[cfg(test)]
mod tests {
use super::*;
use warp::{Filter, Reply, http::StatusCode};
use serde_json::json;
#[tokio::test] // Tokio のテストマクロ
async fn test_hello_world() {
let api = warp::path::end().map(|| "Hello world!"); // テスト対象のフィルター
// テストリクエストを作成し、フィルターに適用
let resp = warp::test::request()
.method("GET")
.path("/")
.reply(&api) // api フィルターに適用
.await;
assert_eq!(resp.status(), StatusCode::OK); // ステータスコードを確認
assert_eq!(resp.body(), "Hello world!"); // レスポンスボディを確認
}
#[tokio::test]
async fn test_greet_name() {
let api = warp::path!("greet" / String)
.map(|name| format!("Hello {}!", name));
let resp = warp::test::request()
.method("GET")
.path("/greet/testuser")
.reply(&api)
.await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.body(), "Hello testuser!");
}
// JSON POST リクエストのテスト例
#[tokio::test]
async fn test_json_input() {
let api = warp::path!("json_input")
.and(warp::post())
.and(warp::body::json())
.map(|info: Info| format!("Received username: {}", info.username));
let payload = json!({"username": "test_json"}).to_string();
let resp = warp::test::request()
.method("POST")
.path("/json_input")
.header("Content-Type", "application/json") // ヘッダーを設定
.body(payload) // リクエストボディを設定
.reply(&api) // api フィルターに適用
.await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.body(), "Received username: test_json");
}
}
“`
#[tokio::test]
マクロを使用し、warp::test::request()
でテストリクエストを構築します。.reply(&filter)
メソッドで、そのリクエストを指定したフィルターに適用し、レスポンスを取得します。
3.2.9. メリットとデメリット
-
メリット:
- 関数型アプローチとモジュール性: フィルターを組み合わせてルーティングや処理を定義するスタイルは、モジュール性が高く、コードの見通しが良いと感じる開発者もいます。
- 軽量: 必要最低限の機能に絞られており、依存関係が比較的少ないです。
- 高いパフォーマンス: シンプルなAPIなどではActix-webに匹敵する高いパフォーマンスを発揮します。
- フィルターによる柔軟な構成: フィルターを自由に組み合わせてカスタムな処理パイプラインを構築できます。
-
デメリット:
- 学習コスト: フィルターベースのプログラミングモデルは独特で、慣れるまで時間がかかる場合があります。
- ドキュメントと例の少なさ: Actix-webやAxumと比較すると、ドキュメントや複雑なケースの例が少ない傾向があります(ただし改善されつつあります)。
- エラーハンドリングの複雑さ: カスタムエラーハンドリングは
.recover()
を使う必要があり、Actix-webやAxumと比較してやや複雑に感じられる場合があります。
Warpは、関数型プログラミングに馴染みがあり、モジュール性が高く軽量なAPIを構築したい場合に良い選択肢となります。フィルターの組み合わせによる柔軟性は、特定の要件に合わせて処理を細かく制御したい場合に役立ちます。
3.3. Axum
- 公式サイト: https://github.com/tokio-rs/axum
- 特徴: Axumは、Rustの非同期ランタイムであるTokioと、ミドルウェアフレームワークであるTowerの上に構築された、比較的新しいWebフレームワークです。シンプルさ、柔軟性、Tokio/Towerエコシステムとの統合を重視しています。
3.3.1. 特徴と設計思想
AxumはTokioプロジェクトの一部として開発されており、Tokioのasync
/await
や、ServiceやLayerといったTowerの概念を深く統合しています。Towerは、リクエスト/レスポンスを扱うサービス(Service)と、そのサービスをラップして追加の処理を施すレイヤー(Layer、他のフレームワークでいうミドルウェアに相当)を定義するフレームワークです。
Axumは、このTowerエコシステムを活用することで、柔軟なミドルウェア構成や、gRPCやHTTP/2など他のTowerベースのサービスとの連携を容易にしています。また、マクロを多用せず、標準的なRustの機能とTowerの抽象化を用いてシンプルかつ見通しの良いコードを書けることを目指しています。
3.3.2. 基本的な使い方
Axumアプリケーションの基本的な構造を見てみましょう。
新しいプロジェクトを作成し、依存関係を追加します。
bash
cargo new my-axum-app
cd my-axum-app
Cargo.toml
に依存関係を追加します。
toml
[dependencies]
axum = "0.7" # Axum フレームワーク本体
tokio = { version = "1", features = ["full"] } # Tokio ランタイム
serde = { version = "1", features = ["derive"] } # JSON 処理用
serde_json = "1"
tower = "0.4" # Tower (Axum は Tower を利用)
tower-http = { version = "0.5", features = ["add-extension", "cors", "trace"] } # 一般的なミドルウェアを提供
src/main.rs
に以下のコードを書きます。
“`rust
use axum::{
routing::{get, post},
http::StatusCode,
Json, Router,
};
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener; // TCP リスナー
// リクエストボディ用の構造体
[derive(Deserialize)]
struct Info {
username: String,
}
// レスポンスボディ用の構造体
[derive(Serialize)]
struct User {
id: u32,
username: String,
}
// シンプルなハンドラー関数
async fn hello() -> &’static str {
“Hello world!”
}
// パスパラメータ付きハンドラー
// パスパラメータは Path
async fn greet(axum::extract::Path
format!(“Hello {}!”, name.into_inner())
}
// クエリストリング付きハンドラー
// クエリストリングは Query
[derive(Deserialize)]
struct QueryInfo {
name: String,
}
async fn query_greet(axum::extract::Query
format!(“Hello {} from query!”, info.name)
}
// JSON リクエストボディを受け取るハンドラー
// JSON ボディは Json
async fn json_input(axum::Json
format!(“Received username: {}”, info.username)
}
// JSON レスポンスを返すハンドラー
async fn get_user() -> axum::Json
let user = User {
id: 1,
username: “test_user”.to_string(),
};
// axum::Json は Content-Type: application/json を自動で設定
axum::Json(user)
}
// レスポンスとしてステータスコードとボディを返すハンドラー
async fn always_bad_request() -> (StatusCode, String) {
(StatusCode::BAD_REQUEST, “Something went wrong”.to_string())
}
[tokio::main] // Tokio ランタイムで main 関数を実行
async fn main() {
// ルーターを定義
let app = Router::new()
.route(“/”, get(hello)) // GET / に hello ハンドラーを紐付け
.route(“/greet/:name”, get(greet)) // GET /greet/:name に greet ハンドラーを紐付け (:name はパスパラメータ)
.route(“/query_greet”, get(query_greet)) // GET /query_greet?name=… に query_greet ハンドラーを紐付け
.route(“/json_input”, post(json_input)) // POST /json_input に json_input ハンドラーを紐付け
.route(“/user”, get(get_user)) // GET /user に get_user ハンドラーを紐付け
.route(“/bad_request”, get(always_bad_request)); // GET /bad_request に always_bad_request ハンドラーを紐付け
// サーバーを起動
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap(); // TCP リスナーを作成
println!("Server running on http://127.0.0.1:8080");
axum::serve(listener, app).await.unwrap(); // axum::serve でリクエストを処理
}
“`
Axumでは、Router
を使ってルーティングを定義します。Router::new()
で新しいルーターを作成し、.route()
メソッドでパス、HTTPメソッド(get()
, post()
など)、ハンドラー関数を紐付けます。
Axumのハンドラー関数は、async fn
である必要があります。ハンドラー関数の戻り値は、axum::response::IntoResponse
トレイトを実装している必要があります。&'static str
, String
, Json<T>
, StatusCode
, (StatusCode, T)
, Result<T, E>
など、多くの一般的な型がIntoResponse
を実装しています。これにより、様々な形式のレスポンスを直感的に返すことができます。
サーバーの起動は、TokioのTcpListener
を作成し、axum::serve
関数に渡して行います。
3.3.3. リクエストデータの抽出
Axumでは、リクエストデータをハンドラー関数の引数として抽出子(Extractor)を使って取得します。抽出子はaxum::extract
モジュールにあります。
Path<T>
: パスパラメーターQuery<T>
: クエリストリング (#[derive(Deserialize)]
が必要)Json<T>
: JSONリクエストボディ (#[derive(Deserialize)]
が必要)Form<T>
: URLエンコードされたフォームデータ (#[derive(Deserialize)]
が必要)State<T>
: アプリケーション全体またはルーターごとのステートデータExtension<T>
: TowerのAddExtensionLayer
で追加されたデータOriginalUri
: 元のリクエストURIRequest
: 元のリクエストオブジェクト全体HeaderMap
: リクエストヘッダー全体
これらの抽出子は、ハンドラー関数の引数リストに並べるだけで使用できます。Axumはリクエストから適切なデータを抽出し、引数として渡してくれます。
“`rust
use axum::{
extract::{Path, Query, Json}, // 抽出子をインポート
routing::get, Router,
};
use serde::Deserialize;
[derive(Deserialize)]
struct ProductQuery {
category: String,
}
[derive(Deserialize)]
struct CreateProductJson {
name: String,
price: u32,
}
async fn get_product_by_id(Path(product_id): Path
format!(“Getting product with ID: {}”, product_id)
}
async fn search_products(Query(query): Query
format!(“Searching products in category: {}”, query.category)
}
async fn create_product(Json(payload): Json
format!(“Creating product: {} with price {}”, payload.name, payload.price)
}
async fn main() {
let app = Router::new()
.route(“/products/:product_id”, get(get_product_by_id)) // Path
.route(“/search”, get(search_products)) // Query
.route(“/products”, post(create_product)); // Json
// ... サーバー起動 ...
}
“`
3.3.4. JSONの処理
AxumでJSONを扱うには、リクエストボディの抽出にaxum::Json<T>
を使用し、JSONレスポンスを返す場合もaxum::Json(value)
のように構造体をラップして返します。抽出と同様に、構造体にはserde
のDeserialize
やSerialize
トレイトを derive する必要があります。axum::Json
はIntoResponse
を実装しており、自動的にContent-Type: application/json
ヘッダーを設定します。
3.3.5. エラーハンドリング
AxumのハンドラーはResult<T, E>
を返すことができます。ここでT
はIntoResponse
を実装する成功時の型、E
はIntoResponse
を実装するエラー時の型です。通常、E
はaxum::http::StatusCode
や、カスタムエラー型にIntoResponse
を実装したものを使用します。
カスタムエラーレスポンスを返すには、独自のエラー型を作成し、axum::response::IntoResponse
トレイトを実装します。
“`rust
use axum::{
http::StatusCode,
response::IntoResponse,
Json
};
use serde::Serialize;
[derive(Debug)]
enum AppError {
UserNotFound,
InternalServerError,
InvalidInput(String),
}
// AppError に IntoResponse を実装
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let (status, error_message) = match self {
AppError::UserNotFound => (StatusCode::NOT_FOUND, “User not found”),
AppError::InternalServerError => (StatusCode::INTERNAL_SERVER_ERROR, “Internal server error”),
AppError::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg.as_str()),
};
// エラーレスポンスのボディとして JSON を返す例
let body = Json(serde_json::json!({
"error": error_message,
}));
(status, body).into_response() // タプルも IntoResponse を実装
}
}
async fn get_user_safely(user_id: axum::extract::Path
let id = user_id.into_inner();
if id == 1 {
// 成功
let user = User { id: 1, username: “test_user”.to_string() };
Ok(Json(user))
} else if id == 404 {
// ユーザーが見つからないエラー
Err(AppError::UserNotFound)
} else if id == 500 {
// 内部サーバーエラー
Err(AppError::InternalServerError)
} else {
// 無効な入力エラー
Err(AppError::InvalidInput(format!(“User ID {} is invalid”, id)))
}
}
async fn main() {
let app = Router::new()
.route(“/users/:user_id”, get(get_user_safely)); // Result を返すハンドラー
// ... サーバー起動 ...
}
“`
Axumはハンドラーが返したResult
のErr
バリアントを検知し、そのエラー型に実装されたIntoResponse
トレイトを使って適切なHTTPレスポンスを生成します。これにより、エラーハンドリングのロジックをハンドラー関数自体に含めることができ、非常にシンプルになります。
3.3.6. ミドルウェア
AxumはTowerのLayer
をミドルウェアとして使用します。Router::new()
に.layer()
メソッドを使ってミドルウェアを追加します。tower-http
クレートは、一般的なWeb開発に役立つ多くのTowerレイヤー(ミドルウェア)を提供しています。
一般的なミドルウェアの例:
tower_http::trace::TraceLayer
: リクエスト/レスポンスのトレーシング(ロギング)tower_http::cors::CorsLayer
: クロスオリジンリソース共有tower_http::add_extension::AddExtensionLayer
: リクエストごとにデータを追加tower::limit::LimitLayer
: レートリミット
ロギングミドルウェアを追加する例:
“`rust
use axum::{routing::get, Router};
use tower_http::trace::TraceLayer; // TraceLayer をインポート
use tower_http::cors::CorsLayer; // CorsLayer をインポート
use tokio::net::TcpListener;
[tokio::main]
async fn main() {
// ロギングを有効にする (env_logger などと連携)
tracing_subscriber::fmt() // tracing_subscriber クレートが必要
.with_max_level(tracing::Level::DEBUG)
.init();
let app = Router::new()
.route("/", get(|| async { "Hello, world!" }))
// .layer() メソッドでミドルウェアを追加
// Layer は Tower のものを使用
.layer(TraceLayer::new_for_http()) // HTTP リクエストのトレーシング
.layer(CorsLayer::permissive()); // CORS を許可 (実運用では適切に設定)
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
println!("Server running on http://127.0.0.1:8080");
axum::serve(listener, app).await.unwrap();
}
“`
.layer()
メソッドはチェーン可能であり、複数のミドルウェアを適用できます。ミドルウェアは.layer()
を呼び出した順に適用されます(外側から内側へ)。
3.3.7. ステート管理
Axumでアプリケーション全体で共有したいデータ(例:データベースコネクションプール、設定情報)を管理するには、axum::extract::State<T>
抽出子を使用します。まず、共有したいデータをArc
などでラップし、ルーターに.with_state()
メソッドを使って登録します。ハンドラー関数ではState<T>
抽出子を使ってデータにアクセスできます。
“`rust
use axum::{
extract::State,
routing::get,
Router,
};
use std::sync::{Arc, Mutex};
use tokio::net::TcpListener;
// 共有したいステートの構造体
[derive(Clone)] // State 抽出子を使うには Clone が必要 (Arc に derive すれば OK)
struct AppState {
app_name: String,
counter: Arc
}
// ステートを受け取るハンドラー
async fn show_app_name(State(state): State
state.app_name
}
// ステートを受け取る別のハンドラー
async fn increment_counter(State(state): State
let mut counter = state.counter.lock().unwrap();
counter += 1;
format!(“Counter: {}”, counter)
}
[tokio::main]
async fn main() {
let shared_state = AppState {
app_name: “My Axum App”.to_string(),
counter: Arc::new(Mutex::new(0)), // Arc でラップ
};
let app = Router::new()
.route("/name", get(show_app_name))
.route("/increment", get(increment_counter))
.with_state(shared_state); // ステートをルーターに登録
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
println!("Server running on http://127.0.0.1:8080");
axum::serve(listener, app).await.unwrap();
}
“`
State<T>
抽出子を使うためには、T
がClone
トレイトを実装している必要があります。共有したいデータがClone
できない場合は、Arc
でラップすることで対応できます。ミュータブルなデータはMutex
やRwLock
などで保護し、そのMutex
やRwLock
をArc
でラップするのが一般的です。
3.3.8. テストの書き方
Axumアプリケーションのテストは、標準的なTokioのテスト機能と、AxumのルーターをTowerのServiceとして扱う機能を利用します。
“`rust
[cfg(test)]
mod tests {
use super::*; // 外側のモジュール(main.rs)の要素をインポート
use axum::{
body::Body,
http::{Request, Method, StatusCode},
routing::{get, post},
Router,
};
use tower::ServiceExt; // .oneshot() メソッドを使うために必要
use serde_json::json;
#[tokio::test] // Tokio のテストマクロ
async fn test_hello_world() {
let app = Router::new().route("/", get(hello)); // テスト対象のルーター
// テストリクエストを作成
let req = Request::builder()
.method(Method::GET)
.uri("/")
.body(Body::empty())
.unwrap();
// ルーターを Service として扱い、リクエストを実行
let response = app
.oneshot(req) // oneshot() で一度だけリクエストを実行
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK); // ステータスコードを確認
let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); // レスポンスボディを読み込み (hyper クレートが必要)
assert_eq!(&body[..], b"Hello world!"); // レスポンスボディを確認
}
#[tokio::test]
async fn test_greet_name() {
let app = Router::new().route("/greet/:name", get(greet));
let req = Request::builder()
.method(Method::GET)
.uri("/greet/testuser")
.body(Body::empty())
.unwrap();
let response = app
.oneshot(req)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
assert_eq!(&body[..], b"Hello testuser!");
}
// JSON POST リクエストのテスト例
#[tokio::test]
async fn test_json_input() {
let app = Router::new().route("/json_input", post(json_input));
let payload = json!({"username": "test_json"}).to_string();
let req = Request::builder()
.method(Method::POST)
.uri("/json_input")
.header("Content-Type", "application/json") // ヘッダーを設定
.body(Body::from(payload)) // リクエストボディを設定
.unwrap();
let response = app
.oneshot(req)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
assert_eq!(&body[..], b"Received username: test_json");
}
}
“`
#[tokio::test]
マクロを使用し、axum::http::Request
を直接構築します。構築したRequest
を、テスト対象のRouter
インスタンスに対して.oneshot(req)
メソッドを使って実行します。.oneshot()
メソッドはTowerのServiceExt
トレイトによって提供されます。レスポンスボディの読み込みにはhyper::body::to_bytes
関数を使用します。
3.3.9. メリットとデメリット
-
メリット:
- シンプルさと直感的なAPI: ルーティング定義や抽出子の使用方法がシンプルで分かりやすいです。
- Tokio/Towerエコシステムとの統合: Towerレイヤーを使ったミドルウェア構成が柔軟で、Tokioベースの他のライブラリやサービスとの連携が容易です。
- 強力な抽出子: 豊富な抽出子が用意されており、様々なリクエストデータを簡単にハンドラーに渡せます。
- 優れたエラーハンドリング:
Result<T, E>
とIntoResponse
を使ったエラーハンドリングが非常にクリーンです。 - 比較的新しいながら活発な開発: Tokioプロジェクトの一部であり、積極的に開発が進められています。
-
デメリット:
- 歴史が浅い: Actix-webやWarpと比較すると、登場してからの期間が短く、大規模な本番運用事例やサードパーティライブラリのエコシステムは発展途上です。
- パフォーマンス: 一般的にActix-webにはわずかに及ばないことが多いですが、それでも非常に高性能です。
Axumは、シンプルで直感的なAPI、Towerエコシステムとの連携、優れたエラーハンドリングを重視する開発者にとって魅力的な選択肢です。特にTokioを中心とした非同期エコシステムに深くコミットしたい場合に適しています。
4. フレームワーク比較と選択ガイド
ここまで3つの主要なRust Webフレームワークを見てきました。それぞれの特徴をまとめ、どのフレームワークを選ぶべきかのガイドラインを示します。
特徴/フレームワーク | Actix-web | Warp | Axum |
---|---|---|---|
設計思想 | アクターモデルベース (Tokio利用) | 関数型/フィルターベース (Tokio利用) | Tokio/Towerベース |
パフォーマンス | 非常に高い | 高い(特にシンプルAPI) | 高い |
成熟度/歴史 | 成熟している(長い歴史) | 中程度 | 比較的新しい |
学習コスト | 中程度(抽出子、App構造など) | 中程度(フィルターの概念) | やや低め(シンプル、直感的) |
ルーティング | App::new().route().service() |
フィルターの and , or 結合 |
Router::new().route() |
リクエスト抽出 | web::Path , web::Query , web::Json など |
warp::path , warp::query , warp::body などのフィルター |
Path , Query , Json , State などの抽出子 |
JSON処理 | web::Json , .json() |
warp::body::json() , warp::reply::json() |
Json 抽出子/レスポンス型 |
エラーハンドリング | ResponseError トレイト、Result |
Rejection , .recover() |
Result , IntoResponse トレイト |
ミドルウェア | .wrap() メソッド (独自) |
フィルター結合、.with() |
.layer() メソッド (Tower Layer) |
ステート管理 | web::Data (Arc + Mutex) |
warp::any().map(move || state.clone()) (Arc + Mutex) |
State 抽出子, .with_state() (Arc + Mutex) |
テスト | actix_web::test ユーティリティ |
warp::test::request().reply() |
Request::builder().oneshot() |
エコシステム | 成熟 | 比較的小さめ | Towerエコシステムとの連携 |
パフォーマンスについて:
一般的に、ベンチマークではActix-webがわずかに速い結果を出すことが多いですが、WarpやAxumも非常に高性能であり、多くのアプリケーションにおいてパフォーマンスがボトルネックになることは少ないでしょう。極限のパフォーマンスを追求する場合を除き、フレームワークの設計思想や開発のしやすさを重視して選択するのが現実的です。
どのフレームワークを選ぶべきか?
-
Actix-web:
- 高いパフォーマンスと安定性、成熟度を最優先したい場合。
- 大規模なWebアプリケーションやAPIゲートウェイなど、豊富な機能と実績が必要なプロジェクト。
- 既存のActix-webプロジェクトを引き継ぐ場合。
-
Warp:
- 関数型プログラミングのアプローチに馴染みがあり、フィルターベースの柔軟な構成を好む場合。
- 軽量でモジュール性が高いAPIを構築したい場合。
- 独自のカスタム処理パイプラインを細かく制御したい場合。
-
Axum:
- シンプルで直感的なAPI、開発のしやすさを重視したい場合。
- TokioおよびTowerエコシステム(gRPCなど)との連携を重視したい場合。
- 比較的新しいフレームワークで、活発な開発と将来性に期待する場合。
- クリーンな
Result<T, E>
とIntoResponse
による優れたエラーハンドリングを求める場合。
どのフレームワークを選んでも、Rustの安全性やパフォーマンスといったメリットは享受できます。まずはそれぞれのチュートリアルやサンプルコードを試してみて、ご自身の開発スタイルやプロジェクトの要件に最も合ったものを選ぶのが良いでしょう。
5. その他のフレームワーク(簡単な紹介)
この記事では主要な3つに絞って解説しましたが、他にもRustにはいくつかのWebフレームワークが存在します。
- Rocket:
- 公式サイト: https://rocket.rs/
- 特徴: マクロを多用し、非常にDSL(ドメイン固有言語)ライクな記述が可能なフレームワークです。使いやすく、生産性が高いと評価されています。ただし、かつてはStable Rustに対応していなかった時期がありましたが、現在はStableで利用可能です。人気も高いですが、Actix-webやAxumと比較すると開発速度は緩やかな傾向があります。
- Tide:
- 公式サイト: https://docs.rs/tide/
- 特徴:
async-std
非同期ランタイムをベースにしたWebフレームワークです。ミドルウェアやステート管理機能を持っています。async-stdエコシステムを使いたい場合に選択肢になります。
- Salvo:
- 公式サイト: https://salvo.rs/
- 特徴: Actix-webからインスピレーションを受けつつ、よりシンプルで直感的なAPIを目指して設計されたフレームワークです。比較的新しいですが、高いパフォーマンスを目指しています。
これらのフレームワークもそれぞれの強みや特徴を持っています。興味があれば調べてみると良いでしょう。
6. 実践的な開発のヒント
実際のWebアプリケーション開発では、フレームワークだけでなく様々な要素が必要になります。
-
データベース連携:
- ほとんどのWebアプリケーションはデータベースと連携します。RustにはSQLxやDieselといった人気のORM/クエリビルダーがあります。
- SQLx:
async
/await
に対応しており、コンパイル時にSQLクエリの妥当性をチェックする機能が強力です。PostgreSQL, MySQL, SQLite, SQL Serverなどに対応しています。非同期Webフレームワークとの相性が良いです。 - Diesel: ORM寄りのアプローチで、コンパイル時の型安全なクエリビルディングに強みがあります。PostgreSQL, MySQL, SQLiteに対応しています。非同期対応は限定的です。
- データベースコネクションプールは、多数のリクエストを効率的に処理するために不可欠です。フレームワークのステート管理機能を使ってコネクションプールを共有するのが一般的です。
-
設定管理:
- 環境変数、設定ファイル(JSON, YAMLなど)、コマンドライン引数などからアプリケーションの設定を読み込むには、
config
やclap
といったクレートが便利です。
- 環境変数、設定ファイル(JSON, YAMLなど)、コマンドライン引数などからアプリケーションの設定を読み込むには、
-
ロギング:
- アプリケーションの実行状況やエラーを把握するためにロギングは重要です。Rustには
log
ファサードクレートがあり、env_logger
,tracing
などの実装と組み合わせて使用します。特にtracing
は非同期処理との相性が良く、構造化ログや分散トレーシングにも対応しています。ほとんどのWebフレームワークはtracing
またはlog
と連携するミドルウェアを提供しています。
- アプリケーションの実行状況やエラーを把握するためにロギングは重要です。Rustには
-
認証・認可:
- ユーザー認証(ログイン、APIキー)や認可(アクセス制御)は、Webアプリケーションのセキュリティに不可欠です。JWT (
jsonwebtoken
クレート)、OAuth2 (oauth2
クレート)、セッション管理 (actix-session
など) といった技術やクレートを、フレームワークのミドルウェア機能と組み合わせて実装します。
- ユーザー認証(ログイン、APIキー)や認可(アクセス制御)は、Webアプリケーションのセキュリティに不可欠です。JWT (
これらのトピックは単独で詳細な記事になるほど奥深いですが、フレームワークと組み合わせて使うことで、より本格的なWebサービスを構築できるようになります。
7. まとめ
この記事では、RustでWeb開発を始めるにあたって知っておくべき基礎知識と、主要なWebフレームワークであるActix-web、Warp、Axumの3つを徹底的に解説しました。
- Actix-web: 高いパフォーマンスと成熟度を誇る、多機能で実績のあるフレームワーク。大規模プロジェクトや極限のパフォーマンスが求められる場合に適しています。
- Warp: 関数型アプローチとフィルターベースの設計が特徴。モジュール性が高く軽量なAPIや、独自の処理パイプラインを構築したい場合に魅力的です。
- Axum: シンプルで直感的、Tokio/Towerエコシステムとの統合が強み。開発のしやすさ、Towerベースのライブラリとの連携、クリーンなエラーハンドリングを重視する場合に適しています。
どのフレームワークも、Rustの安全性、パフォーマンス、並行処理といった強力なメリットを活かしたWeb開発を可能にします。それぞれの設計思想や得意な点が異なるため、ご自身の好みやプロジェクトの要件に合わせて最適なものを選ぶことが重要です。
入門者としては、まずは一つを選んで基本的な使い方をマスターし、簡単なCRUD APIなどを実際に構築してみることをお勧めします。実際にコードを書きながら、それぞれのフレームワークの哲学や特徴を体感するのが一番の近道です。
RustのWeb開発エコシステムはまだ比較的新しいですが、急速に進化しており、今後もさらに多くのツールやライブラリが登場し、成熟していくことが期待されます。ぜひRustでのWeb開発に挑戦してみてください!
(終)