RustにおけるAxumを用いたモジュラーWeb APIの構築
Daniel Hayes
Full-Stack Engineer · Leapcell

バックエンド開発の進化し続ける状況において、堅牢でスケーラブル、かつ保守性の高いWeb APIの構築は最重要です。Rustは、パフォーマンス、安全性、並行性への注力により、この分野で魅力的な選択肢として浮上しています。しかし、Rustの本来のパワーは、特に複雑なアプリケーションロジックを調整する際には、習得に時間がかかることがしばしばあります。ここで、AxumのようなモダンなWebフレームワークが輝きます。Axumは、強力なTokioランタイムと汎用性の高いTowerエコシステムの上に構築されており、RustでWebサービスを構築するための高レベルで人間工学的な方法を提供します。モジュラリティの概念を採用し、開発者がAPIエンドポイントを整理し、共有状態を効率的に管理し、強力なミドルウェアを統一された方法で統合できるようにします。この記事では、AxumでモジュラーWeb APIを構築するプロセスを案内し、ルーティングを効果的に処理し、アプリケーション状態を共有し、Towerサービスによって提供される拡張性を活用する方法を実証します。
コアコンセプトの理解
実装に飛び込む前に、AxumでAPIを構築する上で中心となるいくつかの基本的な概念を明確にしましょう。
- Axum: RustのためのWebアプリケーションフレームワークです。
tokio
(非同期ランタイム)とhyper
(HTTPライブラリ)の上に構築されており、ミドルウェアとサービスコンポジションのためにtower
エコシステムをヘビーに活用しています。 - ルーティング: URLパスとHTTPメソッドに基づいて、受信HTTPリクエストを適切なハンドラー関数にディスパッチするメカニズムです。Axumは、ルーティングを定義するための宣言的で型安全な方法を提供します。
- 状態管理: Webアプリケーションでは、異なるリクエストハンドラー間でデータ(例:データベース接続、設定、キャッシュ)を共有する必要があることがよくあります。Axumは、アプリケーション全体およびリクエストスコープの状態を管理するための堅牢なメカニズムを提供します。
- Towerサービス: Towerは、堅牢なネットワークアプリケーションを構築するためのモジュラーで再利用可能なコンポーネントのライブラリです。Axumでは、ハンドラーは基本的に
Tower.Service
の実装であり、ミドルウェアはサービスをラップするTower.Layer
です。このアーキテクチャは、コンポジションと再利用性を促進します。 - ミドルウェア: サーバーとハンドラーの間に配置される関数またはサービスであり、リクエストの前処理やレスポンスの後処理を可能にします。一般的な用途としては、認証、ロギング、エラー処理、レート制限などがあります。
モジュラーWeb APIの構築
ユーザーリストを管理するためのシンプルなAPIを構築し、モジュラールーティング、状態共有、およびTowerサービスの適用を実証しましょう。
プロジェクトセットアップ
まず、新しいRustプロジェクトを作成します。
cargo new axum_modular_api cd axum_modular_api
Cargo.toml
に必要な依存関係を追加します。
[dependencies] axum = { version = "0.7", features = ["macros"] } tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tracing = "0.1" tracing-subscriber = "0.3"
アプリケーション状態の定義
ユーザー管理APIのために、ユーザーを保存する方法が必要になります。Arc<Mutex<...>>
でラップされたシンプルなVec<User>
は、インメモリ状態の良好な出発点となります。
src/models.rs
ファイルを作成します。
// src/models.rs use serde::{Deserialize, Serialize}; use std::sync::{Arc, Mutex}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct User { pub id: u32, pub name: String, pub email: String, } pub type AppState = Arc<Mutex<Vec<User>>>; pub fn initialize_state() -> AppState { Arc::new(Mutex::new(vec![ User { id: 1, name: "Alice".to_string(), email: "alice@example.com".to_string() }, User { id: 2, name: "Bob".to_string(), email: "bob@example.com".to_string() }, ])) }
モジュラールーティング
保守性を高めるために、ルーティングを個別のモジュールに整理します。src/routes/mod.rs
とsrc/routes/users.rs
ファイルを作成します。
src/routes/users.rs
: このモジュールには、すべてのユーザー関連のエンドポイントが含まれます。
// src/routes/users.rs use axum::{ extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::{get, post}, Json, Router, }; use serde_json::json; use crate::models::{AppState, User}; pub fn users_router() -> Router<AppState> { Router::new() .route("/", get(list_users).post(create_user)) .route("/:id", get(get_user).put(update_user).delete(delete_user)) } async fn list_users(State(state): State<AppState>) -> Json<Vec<User>> { let users = state.lock().unwrap(); Json(users.clone()) } async fn get_user(State(state): State<AppState>, Path(id): Path<u32>) -> Result<Json<User>, StatusCode> { let users = state.lock().unwrap(); if let Some(user) = users.iter().find(|u| u.id == id) { Ok(Json(user.clone())) } else { Err(StatusCode::NOT_FOUND) } } async fn create_user(State(state): State<AppState>, Json(mut new_user): Json<User>) -> impl IntoResponse { let mut users = state.lock().unwrap(); let next_id = users.iter().map(|u| u.id).max().unwrap_or(0) + 1; new_user.id = next_id; users.push(new_user.clone()); (StatusCode::CREATED, Json(new_user)) } async fn update_user( State(state): State<AppState>, Path(id): Path<u32>, Json(updated_user): Json<User>, ) -> impl IntoResponse { let mut users = state.lock().unwrap(); if let Some(user) = users.iter_mut().find(|u| u.id == id) { user.name = updated_user.name; user.email = updated_user.email; (StatusCode::OK, Json(user.clone())) } else { (StatusCode::NOT_FOUND, Json(json!({"message": "User not found"}))) } } async fn delete_user(State(state): State<AppState>, Path(id): Path<u32>) -> StatusCode { let mut users = state.lock().unwrap(); let initial_len = users.len(); users.retain(|u| u.id != id); if users.len() < initial_len { StatusCode::NO_CONTENT } else { StatusCode::NOT_FOUND } }
src/routes/mod.rs
: このモジュールは、サブルーターを再エクスポートし、共通のルートを含む場合があります。
// src/routes/mod.rs pub mod users;
メインアプリケーションの構成
次に、src/main.rs
でこれらをすべてまとめます。アプリケーション状態を初期化し、ルーターを構成し、tracing
を使用して基本的なロギングを追加します。
// src/main.rs mod models; mod routes; use axum::{ routing::get, Router, }; use tower_http::trace::{self, TraceLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use std::time::Duration; #[tokio::main] async fn main() { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "axum_modular_api=debug,tower_http=debug".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); let app_state = models::initialize_state(); // ルートを持つアプリケーションを構築 let app = Router::new() .route("/", get(|| async { "Hello, Modular Axum API!" })) // /users パスの下に users ルーターをマウント .nest("/users", routes::users::users_router()) .with_state(app_state) // Towerサービス(ミドルウェア)を追加 .layer( TraceLayer::new_for_http() .make_span_with(trace::DefaultMakeSpan::new().include_headers(true)) .on_request(trace::DefaultOnRequest::new().level(tracing::Level::INFO)) .on_response(trace::DefaultOnResponse::new().level(tracing::Level::INFO).latency_300_ms(Duration::from_millis(300))), ); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); tracing::info!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); }
APIの実行
cargo run
を使用してこのAPIを実行できます。
cargo run
その後、curl
のようなツールを使用して対話できます。
- すべてのユーザーを取得:
curl http://localhost:3000/users
- IDでユーザーを取得:
curl http://localhost:3000/users/1
- 新規ユーザーをPOST:
curl -X POST -H "Content-Type: application/json" -d '{"name": "Charlie", "email": "charlie@example.com"}' http://localhost:3000/users
- ユーザーを更新するためにPUT:
curl -X PUT -H "Content-Type: application/json" -d '{"name": "Alice Smith", "email": "alice.smith@example.com"}' http://localhost:3000/users/1
- ユーザーをDELETE:
curl -X DELETE http://localhost:3000/users/2
主要機能の説明
-
モジュラールーティング:
src/routes/users.rs
のusers_router()
関数はaxum::Router
を返します。このルーターは、すべてのユーザー関連ロジックをカプセル化します。main.rs
では、.nest("/users", routes::users::users_router())
を使用してこのサブルーターを/users
パスの下にマウントします。これにより、明確な階層が作成され、main.rs
ファイルがクリーンに保たれます。Router<AppState>
の型シグネチャは、AppState
が一貫してそのルーターのすべてのルートに渡されることを保証します。
-
状態共有:
AppState
をArc<Mutex<Vec<User>>>
として定義します。Arc
は状態の複数所有者を可能にし、Mutex
は安全な並行アクセスを処理します。- メイン
Router
のwith_state(app_state)
メソッドは、AppState
をアプリケーションに注入します。 - ハンドラー関数では、
State(state): State<AppState>
をエクストラクタとして使用して共有状態を取得します。これは型安全でAxumらしい方法です。
-
Towerサービスとミドルウェア:
tower_http::trace::TraceLayer
を使用して、リクエスト/レスポンスロギングを追加しました。これはTowerのLayer
の強力な例です。Router
の.layer(...)
メソッドは、このミドルウェアをwith_state
の後、および.layer
の前に定義されたすべてのルートに適用します。with_state
の前に適用された場合、ミドルウェアは状態にアクセスできません。- Towerサービスはチェーン可能であり、認証、レート制限、CORS、圧縮などの複雑なミドルウェアパイプラインを、コアビジネスロジックを散らかすことなく構成できます。
アプリケーションシナリオ
このモジュラーアプローチは、以下に役立ちます。
- 大規模API: APIが大きくなるにつれて、関心事を個別のルーティングモジュールに分離することで、
main.rs
がモノリシックなファイルになるのを防ぐことができます。 - チームコラボレーション: 異なるチームや開発者は、大幅なマージコンフリクトなしに個別のAPIモジュールで作業できます。
- 保守性: APIの1つの部分への変更が、無関係な部分に影響を与える可能性が低くなります。
- テスト容易性: 個々のルーターとそのハンドラーを分離してテストできます。
結論
RustでAxumを使用してモジュラーWeb APIを構築することは、複雑さを管理し、スケーラビリティと保守性を保証するための強力で整理された方法を提供します。Axumの宣言的ルーティング、堅牢な状態管理、およびTowerエコシステムのコンポーズ可能なサービスを効果的に活用することで、開発者は高性能で、型安全的で、簡単に拡張可能なバックエンドシステムを構築できます。このアプローチは開発を合理化するだけでなく、クリーンなアーキテクチャを促進し、Rust Webアプリケーションの構築と保守を容易にします。