Rust Axum Webフレームワーク:ルート設計からミドルウェアまでの5つのプロダクショングレードパターン
编程语言
RustでWebサービスを書くと、いつも車輪の再発明をしている気がする
Actixを2年使ったらコミュニティの風向きが変わった。Warpを試したらFilterの組み合わせがビジネスロジックより複雑。Rocketを使おうとしたら非同期サポートがまだnightly。2026年、AxumがついにRust Webフレームワークの事実上の標準に——Tokioチーム製、Towerミドルウェアエコシステム、ゼロコスト抽象化のパフォーマンス、そして最も重要な:学習曲線がついにロッククライミングではなくなった。
本記事はルート設計から出発し、ルート構成→Extractor抽出→ミドルウェア→状態管理→エラー処理の5つのプロダクショングレードパターンを完了し、Axumを「使える」から「優れた」ものにします。
Axumコア概念
| 概念 | 説明 |
|---|---|
| Router | ルーター、URLパスをHandler関数にマッピング |
| Handler | ハンドラー関数、Extractorパラメータを受信し、Responseを返す |
| Extractor | エクストラクター、リクエストからデータを抽出(Path/Query/Json/State等) |
| Middleware | ミドルウェア、LayerでServiceをラップし横断的関心事を実装 |
| State | 共有状態、ArcでラップしHandler間で共有 |
| Layer | Tower Layer、ミドルウェアの構成に使用 |
| Service | Tower Service、リクエスト-レスポンス処理ユニット |
| Error | エラー処理、IntoResponseでエラーをHTTPレスポンスに変換 |
リクエスト処理フロー
リクエストフロー:
1. クライアントがHTTPリクエストを送信
2. Routerがパスをマッチング、対応するHandlerを見つける
3. Extractorがリクエストからパラメータを抽出(Path/Query/Body/State)
4. Handlerがビジネスロジックを実行
5. HandlerがResponseを返す(またはErrorがResponseに変換)
6. ミドルウェアLayerがリクエスト前後で実行(ロギング/認証/レート制限等)
7. レスポンスがクライアントに返される
問題分析:Axumプロダクション開発の5つの課題
- ルート構成の混乱:すべてのルートが1つの
Router::new()に詰め込まれ、数百のルートが「スパゲッティコード」に - Extractor型爆発:Handlerのパラメータが4つを超えるとコンパイルエラー、複雑なインターフェースは分割や構造体が必要
- ミドルウェア順序の罠:Layerのネスト順序が動作に影響、認証をレート制限の前か後かで大きな違い
- 状態管理の混乱:Arc、Extension、Stateの3つの共有方法のどれを使うべきか、ライフタイムの書き方
- エラー処理の不統一:各Handlerが独自のエラーレスポンスを組み立て、フロントエンドが多様なエラー形式を受け取る
ステップバイステップ:5つのプロダクショングレードパターン
パターン1:モジュラールート構成
mod routes {
pub mod users;
pub mod products;
pub mod orders;
}
use axum::{Router, routing::{get, post, put, delete}, middleware, extract::State};
use std::sync::Arc;
pub struct AppState {
pub db: DbPool,
pub config: AppConfig,
}
pub type SharedState = Arc<AppState>;
pub fn create_router(state: SharedState) -> Router {
let api_routes = Router::new()
.nest("/users", routes::users::router())
.nest("/products", routes::products::router())
.nest("/orders", routes::orders::router())
.layer(middleware::from_fn(auth_middleware))
.layer(middleware::from_fn(logging_middleware));
Router::new()
.nest("/api/v1", api_routes)
.route("/health", get(health_check))
.with_state(state)
}
pub mod users {
use axum::{Router, routing::{get, post, put, delete}, extract::State};
use crate::{SharedState, routes::extractors::*};
pub fn router() -> Router<SharedState> {
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<SharedState>,
Query(params): Query<ListUsersParams>,
) -> Result<Json<Vec<User>>, AppError> {
let users = state.db.fetch_users(params.offset, params.limit).await?;
Ok(Json(users))
}
async fn create_user(
State(state): State<SharedState>,
Json(payload): Json<CreateUserRequest>,
) -> Result<Json<User>, AppError> {
let user = state.db.create_user(payload).await?;
Ok(Json(user))
}
async fn get_user(
State(state): State<SharedState>,
Path(id): Path<Uuid>,
) -> Result<Json<User>, AppError> {
let user = state.db.get_user(id).await?;
Ok(Json(user))
}
async fn update_user(
State(state): State<SharedState>,
Path(id): Path<Uuid>,
Json(payload): Json<UpdateUserRequest>,
) -> Result<Json<User>, AppError> {
let user = state.db.update_user(id, payload).await?;
Ok(Json(user))
}
async fn delete_user(
State(state): State<SharedState>,
Path(id): Path<Uuid>,
) -> Result<StatusCode, AppError> {
state.db.delete_user(id).await?;
Ok(StatusCode::NO_CONTENT)
}
}
パターン2:カスタムExtractorでパラメータ爆発を解決
use axum::extract::{FromRequestParts, Query};
use axum::http::request::Parts;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct PaginationParams {
pub offset: Option<u32>,
pub limit: Option<u32>,
}
#[derive(Deserialize)]
pub struct ListUsersParams {
pub offset: Option<u32>,
pub limit: Option<u32>,
pub role: Option<String>,
pub search: Option<String>,
}
pub struct PaginatedRequest<T> {
pub params: T,
pub offset: u32,
pub limit: u32,
}
#[axum::async_trait]
impl<T: serde::de::DeserializeOwned + Send + Sync> FromRequestParts<AppState> for PaginatedRequest<T> {
type Rejection = AppError;
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
let Query(params): Query<T> = Query::from_request_parts(parts, state).await
.map_err(|e| AppError::BadRequest(e.body_text()))?;
let Query(pagination): Query<PaginationParams> = Query::from_request_parts(parts, state).await
.unwrap_or(Query(PaginationParams {
offset: Some(0),
limit: Some(20),
}));
Ok(PaginatedRequest {
params,
offset: pagination.offset.unwrap_or(0),
limit: pagination.limit.unwrap_or(20).min(100),
})
}
}
パターン3:ミドルウェア構成と実行順序
use axum::{middleware, extract::Request, response::Response};
use axum::http::{HeaderValue, header};
use tower_http::cors::{CorsLayer, Any};
use tower_http::trace::TraceLayer;
use tower_http::limit::RateLimitLayer;
use tower::ServiceBuilder;
use std::time::Duration;
pub fn create_app_router(state: SharedState) -> Router {
let cors = CorsLayer::new()
.allow_origin(HeaderValue::from_static("https://example.com"))
.allow_methods(Any)
.allow_headers(Any);
Router::new()
.nest("/api/v1", api_routes())
.layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(cors)
.layer(RateLimitLayer::new(100, Duration::from_secs(60)))
.layer(middleware::from_fn(request_id_middleware))
.layer(middleware::from_fn(auth_middleware))
.into_inner(),
)
.with_state(state)
}
async fn request_id_middleware(
mut request: Request,
next: middleware::Next,
) -> Response {
let request_id = uuid::Uuid::new_v4().to_string();
request.headers_mut().insert(
"x-request-id",
HeaderValue::from_str(&request_id).unwrap(),
);
let mut response = next.run(request).await;
response.headers_mut().insert(
"x-request-id",
HeaderValue::from_str(&request_id).unwrap(),
);
response
}
async fn auth_middleware(
request: Request,
next: middleware::Next,
) -> Result<Response, AppError> {
let auth_header = request.headers()
.get("authorization")
.and_then(|v| v.to_str().ok())
.ok_or(AppError::Unauthorized)?;
if !auth_header.starts_with("Bearer ") {
return Err(AppError::Unauthorized);
}
let token = &auth_header[7..];
validate_token(token)?;
Ok(next.run(request).await)
}
パターン4:型安全な状態管理
use std::sync::Arc;
use tokio::sync::RwLock;
pub struct AppState {
pub db: DbPool,
pub redis: RedisClient,
pub config: AppConfig,
pub cache: Arc<RwLock<lru::LruCache<String, Vec<u8>>>>,
}
impl AppState {
pub fn new(db: DbPool, redis: RedisClient, config: AppConfig) -> Self {
Self {
db,
redis,
config,
cache: Arc::new(RwLock::new(
lru::LruCache::new(NonZeroUsize::new(10000).unwrap())
)),
}
}
}
pub type SharedState = Arc<AppState>;
async fn handler_with_cache(
State(state): State<SharedState>,
Path(key): Path<String>,
) -> Result<Response, AppError> {
{
let cache = state.cache.read().await;
if let Some(value) = cache.get(&key) {
return Ok((*value.clone()).into_response());
}
}
let value = state.db.fetch_by_key(&key).await?;
{
let mut cache = state.cache.write().await;
cache.put(key.clone(), value.clone());
}
Ok(value.into_response())
}
パターン5:統一エラー処理
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Serialize;
#[derive(Debug)]
pub enum AppError {
BadRequest(String),
Unauthorized,
Forbidden(String),
NotFound(String),
Conflict(String),
InternalServerError(String),
DatabaseError(sqlx::Error),
}
#[derive(Serialize)]
struct ErrorResponse {
error: ErrorDetail,
}
#[derive(Serialize)]
struct ErrorDetail {
code: u16,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<Vec<FieldError>>,
}
#[derive(Serialize)]
struct FieldError {
field: String,
message: String,
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match &self {
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized".to_string()),
AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg.clone()),
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
AppError::Conflict(msg) => (StatusCode::CONFLICT, msg.clone()),
AppError::InternalServerError(msg) => {
tracing::error!("Internal error: {}", msg);
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string())
}
AppError::DatabaseError(e) => {
tracing::error!("Database error: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string())
}
};
let body = ErrorResponse {
error: ErrorDetail {
code: status.as_u16(),
message,
details: None,
},
};
(status, Json(body)).into_response()
}
}
impl From<sqlx::Error> for AppError {
fn from(e: sqlx::Error) -> Self {
match e {
sqlx::Error::RowNotFound => AppError::NotFound("Resource not found".to_string()),
sqlx::Error::Database(db_err) => {
if db_err.code().map(|c| c == "23505").unwrap_or(false) {
AppError::Conflict("Duplicate entry".to_string())
} else {
AppError::DatabaseError(sqlx::Error::Database(db_err))
}
}
other => AppError::DatabaseError(other),
}
}
}
落とし穴ガイド
落とし穴1:Handlerパラメータが4つを超える
// ❌ 誤り:Handlerは最大4つのExtractorパラメータをサポート
async fn handler(
Path(id): Path<Uuid>, // 1
Query(params): Query<Params>, // 2
State(state): State<SharedState>, // 3
Json(body): Json<RequestBody>, // 4
Extension(ctx): Extension<RequestContext>, // 5! コンパイルエラー
) { ... }
// ✅ 正しい:構造体でパラメータをマージ
#[derive(Deserialize)]
struct CreateUserRequest {
name: String,
email: String,
role: Option<String>,
}
async fn handler(
Path(id): Path<Uuid>,
State(state): State<SharedState>,
Json(body): Json<CreateUserRequest>,
) { ... }
落とし穴2:State型の不一致
// ❌ 誤り:RouterとHandlerのState型が一致しない
let app = Router::new()
.route("/users", get(handler))
.with_state(Arc::new(AppState { ... }));
async fn handler(State(state): State<Arc<OtherState>>) { ... }
// コンパイルエラー:型の不一致
// ✅ 正しい:統一された型エイリアスを使用
type SharedState = Arc<AppState>;
let app = Router::new()
.route("/users", get(handler))
.with_state(Arc::new(AppState { ... }));
async fn handler(State(state): State<SharedState>) { ... }
落とし穴3:ミドルウェア順序の誤り
// ❌ 誤り:認証がレート制限の後、未認証リクエストもクォータを消費
.layer(middleware::from_fn(rate_limit))
.layer(middleware::from_fn(auth))
// ✅ 正しい:認証を先に、その後認証済みリクエストのみレート制限
.layer(middleware::from_fn(auth))
.layer(middleware::from_fn(rate_limit))
落とし穴4:ボディ変更後のContent-Length更新忘れ
// ❌ 誤り:ボディ変更後Content-Lengthが不一致
async fn body_transform(req: Request, next: Next) -> Response {
let (parts, body) = req.into_parts();
let bytes = axum::body::to_bytes(body, 1024 * 1024).await.unwrap();
let modified = transform(bytes);
next.run(Request::from_parts(parts, Body::from(modified))).await
}
// ✅ 正しい:Content-Lengthを削除し、フレームワークに処理させる
async fn body_transform(req: Request, next: Next) -> Response {
let (mut parts, body) = req.into_parts();
let bytes = axum::body::to_bytes(body, 1024 * 1024).await.unwrap();
let modified = transform(bytes);
parts.headers.remove("content-length");
next.run(Request::from_parts(parts, Body::from(modified))).await
}
落とし穴5:RwLockデッドロック
// ❌ 誤り:read guardがawaitをまたぎデッドロック
async fn handler(State(state): State<SharedState>) {
let cache = state.cache.read().await;
let result = fetch_from_db().await; // .await with lock held!
cache.get(&key);
}
// ✅ 正しい:awaitの前にロックを解放
async fn handler(State(state): State<SharedState>) {
let value = {
let cache = state.cache.read().await;
cache.get(&key).cloned()
}; // lock released here
let result = fetch_from_db().await;
}
エラートラブルシューティング
| # | エラーメッセージ | 原因 | 解決方法 |
|---|---|---|---|
| 1 | handler has too many arguments |
HandlerのExtractorパラメータが4つを超える | 構造体でパラメータをマージまたはカスタムExtractor |
| 2 | mismatched types: expected X, found Y |
State型の不一致 | 統一された型エイリアスを使用、RouterとHandlerの一致を確認 |
| 3 | the trait FromRequest is not implemented |
型がExtractorを実装していない | FromRequestPartsを実装または既存のExtractorを使用 |
| 4 | cannot borrow as mutable |
不変参照で変更を試みる | RwLockまたはArcで可変状態をラップ |
| 5 | future cannot be sent between threads safely |
非Send型がawaitをまたぐ | awaitをまたぐデータがSend traitを実装することを確認 |
| 6 | request was dropped before response |
ミドルウェアがnext.run()を正しく呼び出していない | ミドルウェアが常にnextを呼び出しResponseを返すことを確認 |
| 7 | missing state in router |
ルートがwith_stateを呼び出していない | Routerで.with_state(state)を呼び出す |
| 8 | route not found |
ルートパスが一致しない | パス形式を確認、Axumは:idではなく{id}を使用 |
| 9 | payload too large |
リクエストボディがデフォルト制限を超過 | DefaultBodyLimit::max()で制限を調整 |
| 10 | connection pool timed out |
DB接続プール枯渇 | プールサイズを増やすか接続タイムアウト設定を追加 |
高度な最適化
1. Tower Service構成によるリクエストパイプライン
use tower::ServiceBuilder;
use tower_http::compression::CompressionLayer;
use tower_http::cors::CorsLayer;
use tower_http::limit::RequestBodyLimitLayer;
use tower_http::timeout::TimeoutLayer;
use tower_http::trace::TraceLayer;
use std::time::Duration;
pub fn production_layers() -> tower::ServiceBuilder<
tower::layer::layer_fn::LayerFn<...>,
> {
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.layer(CompressionLayer::new())
.layer(RequestBodyLimitLayer::new(10 * 1024 * 1024))
.layer(CorsLayer::permissive())
}
2. グレースフルシャットダウンとコネクションドレイン
use axum::Router;
use tokio::signal;
use tokio::net::TcpListener;
pub async fn run_server(app: Router, addr: &str) -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind(addr).await?;
tracing::info!("Server listening on {}", addr);
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await?;
tracing::info!("Server shutdown complete");
Ok(())
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => { tracing::info!("Received Ctrl+C"); },
_ = terminate => { tracing::info!("Received SIGTERM"); },
}
}
3. リクエストレベルのコンテキスト伝播
use axum::extract::Request;
use axum::middleware::Next;
use axum::response::Response;
#[derive(Clone)]
pub struct RequestContext {
pub request_id: String,
pub user_id: Option<Uuid>,
pub trace_id: Option<String>,
}
async fn context_middleware(
request: Request,
next: Next,
) -> Response {
let request_id = request.headers()
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.unwrap_or("unknown")
.to_string();
let ctx = RequestContext {
request_id,
user_id: None,
trace_id: None,
};
let mut response = next.run(request).await;
response.headers_mut().insert(
"x-request-id",
HeaderValue::from_str(&ctx.request_id).unwrap(),
);
response
}
比較分析
| 次元 | Axum | Actix Web | Warp | Rocket | Gin (Go) |
|---|---|---|---|---|---|
| 非同期ランタイム | Tokio | Tokio | Tokio | 内蔵 | Goroutine |
| ミドルウェアエコシステム | ✅Tower完全 | ⚠️カスタム | ⚠️Filter構成 | ⚠️Fairing | ✅豊富 |
| 学習曲線 | ⭐中程度 | ⭐急 | ⭐非常に急 | ⭐緩やか | ⭐緩やか |
| パフォーマンス | ⭐非常に高い | ⭐非常に高い | ⭐非常に高い | ⭐高い | ⭐高い |
| 型安全性 | ✅コンパイル時 | ⚠️部分的 | ✅コンパイル時 | ✅ | ❌ランタイム |
| マクロ依存 | 最小 | 多い | なし | 多い | 最小 |
| コミュニティ活発度 | ⭐非常に高い | ⭐高い | ⭐中程度 | ⭐中程度 | ⭐非常に高い |
| ドキュメント品質 | ⭐良い | ⭐良い | ⭐中程度 | ⭐優秀 | ⭐良い |
まとめ:Axumのコアアドバンテージはパフォーマンスではなく(Rust Webフレームワークはすべて高速)、** composability **です。Towerベースのミドルウェアエコシステムにより、ブロックのようにリクエスト処理パイプラインを組み立てられます。2026年のプロダクションプラクティス:モジュラーRouterでルート構成→カスタムExtractorでHandlerを簡素化→ServiceBuilderでミドルウェア構成→統一AppError処理→Arcで状態共有。鍵はTowerのService/Layerモデルを理解すること——これがAxumの基盤であり、他のフレームワークと区別する魂です。
オンラインツール推奨
- JSONフォーマッター:/ja/json/format
- Base64エンコード/デコード:/ja/encode/base64
- Hash計算:/ja/encode/hash
- JWTデコード:/ja/encode/jwt-decode
ブラウザローカルツールを無料で試す →
#Rust#Axum#Web框架#中间件#异步HTTP#2026#Tower