mcpField
A
axum

axum

github.com/bobmatnyc開発78491 回インストール15 週間前に更新
リポジトリ

axum について

Axumは、HTTP APIを構築するRust開発者向けのWebフレームワークです。型付きエクストラクター、Towerミドルウェアの合成、テスト可能なルーターを備えています。

主な機能

  1. 01型付きリクエストエクストラクター
  2. 02合成可能なTowerミドルウェア
  3. 03構造化HTTPエラー
  4. 04グレースフルシャットダウン
  5. 05ソケット不要のルーターテスト

活用シーン

  1. 01型付きHTTP APIルートの構築
  2. 02ハンドラー間でのアプリ状態共有
  3. 03トレース、タイムアウト、CORSの追加
  4. 04ソケットを使わないリクエスト応答テスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill axum

インストール後、Claude Code で/axumを入力して読み込まれたか確認します。

同梱ファイルmetadata.json

詳細

Axum(Rust)- 本番環境向け Web API

概要

Axum は Hyper と Tower を基盤とする Rust の Web フレームワークです。型安全なリクエスト処理、組み合わせ可能なミドルウェア、構造化されたエラー、優れたテスト容易性を備えています。

クイックスタート

最小限のサーバー

✅ 正しい例:型付きハンドラー+JSON レスポンス

use axum::{routing::get, Json, Router};
use serde::Serialize;
use std::net::SocketAddr;

#[derive(Serialize)]
struct Health {
    status: &'static str,
}

async fn health() -> Json<Health> {
    Json(Health { status: "ok" })
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/health", get(health));

    let addr: SocketAddr = "0.0.0.0:3000".parse().unwrap();
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

❌ 誤った例:非同期ランタイムをブロックする

async fn handler() {
    std::thread::sleep(std::time::Duration::from_secs(1)); // blocks executor
}

主な概念

Router とハンドラー

ハンドラーは、IntoResponse を実装するものを返す非同期関数です。

✅ 正しい例:ルートのネスト

use axum::{routing::get, Router};

fn router() -> Router {
    let api = Router::new()
        .route("/users", get(list_users))
        .route("/users/:id", get(get_user));

    Router::new().nest("/api/v1", api)
}

async fn list_users() -> &'static str { "[]" }
async fn get_user() -> &'static str { "{}" }
エクストラクター

境界での解析と検証には、エクストラクターを優先してください。

  • Path<T>:型付きパスパラメーター
  • Query<T>:クエリ文字列
  • Json<T>:JSON 本文
  • State<T>:共有アプリケーション状態

✅ 正しい例:型付きパス+JSON

use axum::{extract::Path, Json};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct CreateUser {
    email: String,
}

#[derive(Serialize)]
struct User {
    id: String,
    email: String,
}

async fn create_user(Json(body): Json<CreateUser>) -> Json<User> {
    Json(User { id: "1".into(), email: body.email })
}

async fn get_user(Path(id): Path<String>) -> Json<User> {
    Json(User { id, email: "[email protected]".into() })
}
依存関係とクレート構成

クレートをバイナリとしてビルドするだけでなく、ライブラリとして公開する場合は、ライブラリ利用者の依存関係が肥大化しないよう、HTTP スタックを分離してください。

✅ 正しい例:オプションの HTTP 機能

[dependencies]
axum = { version = "0.7", optional = true }
tower-http = { version = "0.5", optional = true }
tokio = { version = "1", features = ["full"] }

[features]
default = ["http-server"]
http-server = ["axum", "tower-http"]

[[bin]]
name = "my-service"
required-features = ["http-server"]

これにより、次のようになります。

  • ライブラリ利用者(cargo add my-lib)は、HTTP のオーバーヘッドなしでコアロジックだけを利用できます。
  • バイナリのビルドにはデフォルトでサーバーが含まれるため、cargo install my-crate は期待どおりに動作します。
  • オプトアウトは明示的です。ライブラリとして使う場合は cargo add my-crate --no-default-features を実行します。

❌ 誤った例:HTTP 依存関係を無条件に含める

# Never do this if the crate is also a library:
axum = "0.7"
tower-http = "0.5"
# Library users now pull in the entire web stack

本番環境向けパターン

1)共有状態(DB プール、設定、クライアント)

State<Arc<AppState>> を使用し、可能な限り状態を不変に保ってください。

✅ 正しい例:Arc 経由の AppState

use axum::{extract::State, routing::get, Router};
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    build_sha: &'static str,
}

async fn version(State(state): State<Arc<AppState>>) -> String {
    state.build_sha.to_string()
}

fn app(state: Arc<AppState>) -> Router {
    Router::new().route("/version", get(version)).with_state(state)
}
2)構造化されたエラー処理(IntoResponse)

エラーから HTTP ステータスコードおよび JSON へのマッピングを一元化します。

✅ 正しい例:AppError をレスポンスに変換

use axum::{http::StatusCode, response::IntoResponse, Json};
use serde::Serialize;

#[derive(Debug)]
enum AppError {
    NotFound,
    BadRequest(&'static str),
    Internal,
}

#[derive(Serialize)]
struct ErrorBody {
    error: &'static str,
}

impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        let (status, msg) = match self {
            AppError::NotFound => (StatusCode::NOT_FOUND, "not_found"),
            AppError::BadRequest(_) => (StatusCode::BAD_REQUEST, "bad_request"),
            AppError::Internal => (StatusCode::INTERNAL_SERVER_ERROR, "internal"),
        };

        (status, Json(ErrorBody { error: msg })).into_response()
    }
}
3)ミドルウェア(Tower レイヤー)

本番環境に適したレイヤー(トレーシング、タイムアウト、リクエスト ID、CORS)には tower-http を使用してください。

✅ 正しい例:トレース+タイムアウト+CORS

use axum::{routing::get, Router};
use std::time::Duration;
use tower::ServiceBuilder;
use tower_http::{
    cors::{Any, CorsLayer},
    timeout::TimeoutLayer,
    trace::TraceLayer,
};

fn app() -> Router {
    let layers = ServiceBuilder::new()
        .layer(TraceLayer::new_for_http())
        .layer(TimeoutLayer::new(Duration::from_secs(10)))
        .layer(CorsLayer::new().allow_origin(Any));

    Router::new()
        .route("/health", get(|| async { "ok" }))
        .layer(layers)
}
4)グレースフルシャットダウン

SIGINT/SIGTERM で終了し、処理中のリクエストが完了するまで待ちます。

✅ 正しい例:with_graceful_shutdown

async fn shutdown_signal() {
    let ctrl_c = async {
        tokio::signal::ctrl_c().await.ok();
    };

    #[cfg(unix)]
    let terminate = async {
        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
            .ok()
            .and_then(|mut s| s.recv().await);
    };

    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        _ = ctrl_c => {}
        _ = terminate => {}
    }
}

#[tokio::main]
async fn main() {
    let app = app();
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();

    axum::serve(listener, app)
        .with_graceful_shutdown(shutdown_signal())
        .await
        .unwrap();
}
運用:監視下の環境でのグレースフルシャットダウン

上記のシグナル処理はアプリケーションレベルのものです。本番環境では、スーパーバイザー(systemd、launchd、コンテナオーケストレーター)が実際の終了猶予時間を制御します。

systemd(Linux):.service ファイルで KillSignal=SIGTERM と TimeoutStopSec=120(またはそれ以上)を設定します。

  • デフォルトの 90 秒のタイムアウトは、ネットワークストレージ(EBS/EFS)での fsync の途中に発生し、書き込みが切り詰められる可能性があります。
  • 120 秒あれば、処理中の HTTP リクエストを完了させる時間に加え、ファイルシステムの同期に備える余裕も確保できます。

launchd(macOS):launchctl kickstart -k(SIGKILL を送信し、処理中の I/O を切り詰める)の代わりに、launchctl bootout(SIGTERM を送信して終了を待つ)を使用します。

クライアント側での再接続:サービスに接続する HTTP クライアントと MCP ブリッジには、指数バックオフ(200ms から開始し、上限は 30s)を実装してください。短時間の再起動を問題なく処理でき、上流へのエラーの連鎖を防げます。

テスト

tower::ServiceExt を使うと、ソケットなしでルーターをテストできます。

✅ 正しい例:リクエスト/レスポンステスト

use axum::{body::Body, http::Request, Router};
use tower::ServiceExt;

#[tokio::test]
async fn health_returns_ok() {
    let app: Router = super::app();

    let res = app
        .oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
        .await
        .unwrap();

    assert_eq!(res.status(), 200);
}

選択ガイド

Axum と他の Rust フレームワークの比較
  • Tower ミドルウェアの組み合わせと型付きエクストラクターを重視する場合は、Axum を選んでください。
  • 成熟したエコシステムとアクタースタイルのランタイムモデルを重視する場合は、Actix Web を選んでください。
  • 関数型フィルターとミニマリズムを重視する場合は、Warp を選んでください。

アンチパターン

  • 非同期ランタイムをブロックする(std::thread::sleep、ハンドラー内でのブロッキング I/O)。
  • リクエスト処理で unwrap() を使う。代わりに構造化されたエラーを返してください。
  • タイムアウトなしで実行する。リクエストのタイムアウトと上流の期限を設定してください。

リソース

コメント