mcpField
A
axum-web-framework

axum-web-framework

github.com/manutej開発62651 回インストール40 週間前に更新
リポジトリ

axum-web-framework について

RustのWebフレームワークAxumでWebアプリやAPIを構築するためのスキルです。非同期Rustを使う開発者向けに、ルーティング、型安全なリクエスト抽出、ミドルウェア、状態管理、エラー処理を解説します。

主な機能

  1. 01ルートとネストしたルーターの設定
  2. 02型安全なリクエストデータの抽出
  3. 03共有アプリケーション状態の管理
  4. 04Towerミドルウェアの構成
  5. 05エラーをHTTPレスポンスに変換

活用シーン

  1. 01非同期RustでREST APIを構築
  2. 02認証やレート制限を追加
  3. 03WebSocketまたはSSEサービスを作成

インストール

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

$npx skills add https://github.com/manutej/luxor-claude-marketplace --skill axum-web-framework

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

同梱ファイルEXAMPLES.mdREADME.md

詳細

Axum Web フレームワーク

Tokio と Tower を基盤とする、使いやすくモジュール化された Rust Web フレームワークである Axum を使用して、本番環境対応の Web アプリケーションと API を構築するための包括的なスキルです。ルーティング、エクストラクター、ミドルウェア、状態管理、エラー処理、デプロイのパターンを習得できます。

このスキルを使用する場面

次のような場合に、このスキルを使用してください。

  • Rust と async/await を使って REST API を構築する
  • 型安全性を備えた高性能な Web サービスを作成する
  • Tokio エコシステムとの統合を活用してマイクロサービスを開発する
  • WebSocket サーバーや Server-Sent Events(SSE)を実装する
  • Rust バックエンドで GraphQL API を構築する
  • Tower との統合が必要な、ミドルウェアを多用するアプリケーションを作成する
  • 堅牢なエラー処理が必要な、本番環境対応の Web アプリケーションを開発する
  • HTTP リクエスト/レスポンスの処理をきめ細かく制御する必要があるシステムを構築する
  • 認証、認可、セキュリティミドルウェアを実装する
  • 非同期 Rust でリアルタイム Web アプリケーションを作成する
  • リクエストの検証と変換が必要な API を開発する
  • 複雑なルーティングやネストしたルーターを備えた Web サービスを構築する
  • レート制限、タイムアウト、バックプレッシャー処理を実装する
  • カスタムエクストラクターとレスポンス型が必要な Web アプリケーションを作成する

コアコンセプト

Axum のアーキテクチャ哲学

Axum は次の 3 つの基本理念を柱として構築されています。

  1. Tower サービス:Axum のすべては Tower の Service トレイトを基盤としており、合成可能性とミドルウェア統合を実現します
  2. 型安全なエクストラクター:リクエストデータの抽出はコンパイル時にチェックされるため、実行時の解析エラーをなくせます
  3. 最小限の定型コード:明示性を保ちながら、煩雑な記述を減らす使いやすい API を提供します
ルーター

Router は Axum の中心となる構成要素です。パスとメソッドに基づいて、HTTP リクエストをハンドラーに割り当てます。

主な特性:

  • ルートは定義された順序で照合されます
  • ルーターをネストして、モジュール単位で整理できます
  • ミドルウェアはルーター、ルート、またはメソッド単位で適用できます
  • 柔軟な状態管理のため、状態型をジェネリックに指定できます
  • Tower の Service トレイトを実装しているため、合成できます

ルーターの作成:

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

let app = Router::new()
    .route("/", get(handler))
    .route("/users/:id", get(get_user))
    .route("/posts", get(list_posts).post(create_post));
ハンドラー

ハンドラーはリクエストを処理してレスポンスを返す非同期関数です。Axum は強力な型システムを通じて、複数のハンドラーシグネチャをサポートします。

ハンドラーの要件:

  • 非同期関数である必要があります
  • エクストラクターを使用してリクエストからデータを抽出できます
  • IntoResponse を実装する型を返す必要があります
  • 最大 16 個のパラメーターを指定できます(すべてエクストラクターである必要があります)

一般的なハンドラーパターン:

// Simple handler
async fn handler() -> &'static str {
    "Hello, World!"
}

// Handler with path parameter
async fn get_user(Path(user_id): Path<u32>) -> String {
    format!("User ID: {}", user_id)
}

// Handler with multiple extractors
async fn create_user(
    State(state): State<AppState>,
    Json(payload): Json<CreateUser>,
) -> Result<Json<User>, StatusCode> {
    // Implementation
}
エクストラクター

エクストラクターは FromRequest または FromRequestParts を実装する型で、リクエストから型安全にデータを抽出できます。

組み込みエクストラクター:

  1. Path - パスパラメーターを抽出します
  2. Query - クエリ文字列パラメーターを抽出します
  3. Json - リクエストボディを JSON として解析します
  4. Form - リクエストボディをフォームエンコード形式として解析します
  5. State - 共有アプリケーション状態にアクセスします
  6. Extension - リクエスト拡張にアクセスします
  7. Headers - リクエストヘッダーにアクセスします
  8. Method - HTTP メソッドを取得します
  9. Uri - リクエスト URI を取得します
  10. Request - リクエスト全体を取得します
  11. Bytes - リクエストボディをバイト列として取得します
  12. String - リクエストボディを UTF-8 文字列として取得します
  13. Multipart - multipart/form-data を処理します

エクストラクターの順序:

  • リクエストボディを消費するエクストラクターは最後に配置する必要があります
  • 1 つのハンドラーで複数のボディエクストラクターを使用すると、コンパイルエラーになります
  • State など、ボディを消費しないエクストラクターは任意の順序で配置できます
レスポンス

IntoResponse を実装する型であれば、どれでもハンドラーから返すことができます。Axum には多数の組み込み実装が用意されています。

組み込みレスポンス型:

  • String、&'static str - テキストレスポンス
  • Json<T> - JSON レスポンス
  • Html<String> - HTML レスポンス
  • StatusCode - ステータスのみのレスポンス
  • (StatusCode, T) - ステータスとボディ
  • (Parts, T) - カスタムヘッダーとボディ
  • Response - レスポンスを完全に制御します
  • Result<T, E> - エラー処理(E: IntoResponse の場合)
状態管理

Axum は State エクストラクターを使用して、ハンドラー間でデータを共有します。状態は Clone を実装する必要があり、通常は所有権を共有するために Arc でラップします。

状態のパターン:

  1. シンプルな状態:
#[derive(Clone)]
struct AppState {
    api_key: String,
}

let app = Router::new()
    .route("/", get(handler))
    .with_state(AppState {
        api_key: "secret".to_string(),
    });
  1. Arc を使った共有状態:
#[derive(Clone)]
struct AppState {
    db_pool: Arc<DatabasePool>,
    cache: Arc<RwLock<Cache>>,
}
  1. 複数の状態型:
// Define separate state types for different router sections
let api_router: Router<ApiState> = Router::new()
    .route("/api/data", get(api_handler));

let app_router: Router<AppState> = Router::new()
    .route("/app", get(app_handler));

// Combine with final state
let app = Router::new()
    .nest("/", app_router.with_state(app_state))
    .nest("/", api_router.with_state(api_state));
ミドルウェア

Axum のミドルウェアは Tower に由来し、リクエスト/レスポンスの変換、ロギング、認証などの機能を提供します。

ミドルウェアのカテゴリ:

  1. Tower ミドルウェア - tower クレートおよび tower-http クレートから提供されます
  2. カスタムミドルウェア - middleware::from_fn を使用します
  3. サービスミドルウェア - Tower の Service トレイトを実装します
  4. レイヤーパターン - Tower の Layer を使用して合成可能にします

ミドルウェアの適用順序:

  • .layer() で適用すると、下から上へ実行されます(前のレイヤーをラップします)
  • ServiceBuilder で適用すると、上から下へ実行されます(より直感的です)
  • Router::layer のミドルウェアはルーティング後に実行されます
  • Router を囲むミドルウェア(Layer::layer を使用)はルーティング前に実行されます
エラー処理

Axum のエラー処理は IntoResponse トレイトを基盤としており、カスタムエラー型を HTTP レスポンスに変換できます。

エラー処理の方法:

  1. Result 型:
async fn handler() -> Result<Json<Data>, StatusCode> {
    // Returns 200 OK or error status code
}
  1. カスタムエラー型:
enum AppError {
    Database(sqlx::Error),
    NotFound,
    Unauthorized,
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        // Convert to HTTP response
    }
}
  1. HandleErrorLayer:
use axum::error_handling::HandleErrorLayer;

let app = Router::new()
    .layer(
        ServiceBuilder::new()
            .layer(HandleErrorLayer::new(handle_error))
            .layer(TimeoutLayer::new(Duration::from_secs(30)))
    );
Tower との統合

Axum は Tower を基盤としており、強力なミドルウェアの合成とサービスの抽象化を可能にします。

Tower の主な概念:

  1. Service トレイト - 非同期のリクエスト処理
  2. Layer トレイト - ミドルウェアファクトリーパターン
  3. ServiceBuilder - 簡潔なミドルウェアの合成
  4. Timeout - リクエストのタイムアウト処理
  5. RateLimit - リクエストのレート制限
  6. LoadShed - バックプレッシャーの管理
  7. Buffer - リクエストのバッファリング

ルーティング

基本的なルーティング

ルートは HTTP メソッドとパスをハンドラーに対応付けます。

use axum::{
    Router,
    routing::{get, post, put, delete, patch},
};

let app = Router::new()
    .route("/", get(root))
    .route("/users", get(list_users).post(create_user))
    .route("/users/:id", get(get_user).put(update_user).delete(delete_user))
    .route("/posts/:id/comments", get(get_comments).post(add_comment));
パスパラメーター

パスから動的なセグメントを抽出します。

use axum::extract::Path;
use serde::Deserialize;

// Single parameter
async fn get_user(Path(user_id): Path<u32>) -> String {
    format!("User {}", user_id)
}

// Multiple parameters
#[derive(Deserialize)]
struct PostPath {
    user_id: u32,
    post_id: u32,
}

async fn get_post(Path(params): Path<PostPath>) -> String {
    format!("User {} Post {}", params.user_id, params.post_id)
}

// Using tuple for multiple params
async fn get_comment(
    Path((post_id, comment_id)): Path<(u32, u32)>
) -> String {
    format!("Post {} Comment {}", post_id, comment_id)
}
ワイルドカードルート

残りのパスセグメントをキャプチャします。

// Captures all remaining path
async fn handler(Path(path): Path<String>) -> String {
    format!("Captured path: {}", path)
}

let app = Router::new()
    .route("/{*key}", get(handler));

// GET /foo/bar/baz -> path = "foo/bar/baz"

重要: ネストしたルーターでは、一致したプレフィックスが取り除かれますが、ワイルドカードルートでは URI 全体が保持されます。

ネストしたルーター

ルーターのネストを使って、ルートをモジュールに整理します。

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

fn api_routes() -> Router {
    Router::new()
        .route("/users", get(list_users))
        .route("/users/:id", get(get_user))
        .route("/posts", get(list_posts))
}

fn admin_routes() -> Router {
    Router::new()
        .route("/dashboard", get(dashboard))
        .route("/settings", get(settings))
}

let app = Router::new()
    .nest("/api", api_routes())
    .nest("/admin", admin_routes())
    .route("/", get(root));

ネスト時の動作:

  • ネストしたルーターに渡す前に、一致したプレフィックスが URI から取り除かれます
  • ネストしたルーターのハンドラーからは、ネスト位置からの相対パスのみが見えます
  • 子で定義されていない場合、フォールバックハンドラーは親から継承されます
  • ミドルウェアはネストの前後どちらにも適用できます
フォールバックハンドラー

一致しないルートを処理します。

use axum::{http::StatusCode, handler::Handler};

async fn fallback() -> (StatusCode, &'static str) {
    (StatusCode::NOT_FOUND, "Not Found")
}

let app = Router::new()
    .route("/", get(handler))
    .fallback(fallback);

フォールバックの継承:

async fn api_fallback() -> (StatusCode, &'static str) {
    (StatusCode::NOT_FOUND, "API endpoint not found")
}

let api = Router::new()
    .route("/users", get(list_users))
    .fallback(api_fallback);

let app = Router::new()
    .nest("/api", api)
    .fallback(fallback); // Used for non-/api routes
メソッドルーティング

同じルートで複数の HTTP メソッドを処理します。

use axum::routing::{get, post, MethodRouter};

// Multiple methods on one route
let app = Router::new()
    .route("/users", get(list_users).post(create_user));

// Different handlers per method
let app = Router::new()
    .route("/resource",
        get(get_resource)
            .post(create_resource)
            .put(update_resource)
            .delete(delete_resource)
            .patch(patch_resource)
    );

エクストラクターの詳細

Path エクストラクター

型付きのパスパラメーターを抽出します。

use axum::extract::Path;
use serde::Deserialize;

// Simple extraction
async fn user_by_id(Path(id): Path<u32>) -> String {
    format!("User {}", id)
}

// Complex extraction
#[derive(Deserialize)]
struct Params {
    org: String,
    repo: String,
    issue: u32,
}

async fn github_issue(Path(params): Path<Params>) -> String {
    format!("{}/{} issue #{}", params.org, params.repo, params.issue)
}

let app = Router::new()
    .route("/users/:id", get(user_by_id))
    .route("/repos/:org/:repo/issues/:issue", get(github_issue));
Query エクストラクター

クエリ文字列のパラメーターを抽出します。

use axum::extract::Query;
use serde::Deserialize;

#[derive(Deserialize)]
struct Pagination {
    page: Option<u32>,
    per_page: Option<u32>,
}

async fn list_users(Query(pagination): Query<Pagination>) -> String {
    let page = pagination.page.unwrap_or(1);
    let per_page = pagination.per_page.unwrap_or(20);
    format!("Page {} with {} items", page, per_page)
}

// GET /users?page=2&per_page=50
Json エクストラクター

JSON リクエストボディを解析します。

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

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

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

async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
    let user = User {
        id: 123,
        username: payload.username,
        email: payload.email,
    };
    Json(user)
}

JSON のエラー処理:

use axum::extract::rejection::JsonRejection;
use axum::http::StatusCode;

async fn create_user(
    payload: Result<Json<CreateUser>, JsonRejection>
) -> Result<Json<User>, (StatusCode, String)> {
    match payload {
        Ok(Json(create_user)) => {
            // Valid JSON
            Ok(Json(User { /* ... */ }))
        }
        Err(JsonRejection::MissingJsonContentType(_)) => {
            Err((
                StatusCode::BAD_REQUEST,
                "Missing `Content-Type: application/json`".to_string(),
            ))
        }
        Err(JsonRejection::JsonDataError(err)) => {
            Err((
                StatusCode::BAD_REQUEST,
                format!("Invalid JSON: {}", err),
            ))
        }
        Err(JsonRejection::JsonSyntaxError(err)) => {
            Err((
                StatusCode::BAD_REQUEST,
                format!("JSON syntax error: {}", err),
            ))
        }
        Err(_) => {
            Err((
                StatusCode::INTERNAL_SERVER_ERROR,
                "Unknown error".to_string(),
            ))
        }
    }
}
State エクストラクター

共有アプリケーション状態にアクセスします。

use axum::extract::State;
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    db_pool: Arc<DatabasePool>,
    api_key: String,
}

async fn handler(State(state): State<AppState>) -> String {
    format!("API Key: {}", state.api_key)
}

let state = AppState {
    db_pool: Arc::new(DatabasePool::new()),
    api_key: "secret".to_string(),
};

let app = Router::new()
    .route("/", get(handler))
    .with_state(state);
Extension エクストラクター

リクエストの拡張情報にアクセスします(ミドルウェアで便利です)。

use axum::extract::Extension;

#[derive(Clone)]
struct CurrentUser {
    id: u32,
    username: String,
}

async fn handler(Extension(user): Extension<CurrentUser>) -> String {
    format!("Hello, {}", user.username)
}

// Set by middleware:
async fn auth_middleware(
    mut req: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    let user = CurrentUser {
        id: 1,
        username: "alice".to_string(),
    };
    req.extensions_mut().insert(user);
    Ok(next.run(req).await)
}
Form エクストラクター

フォーム形式でエンコードされたリクエストボディを解析します。

use axum::extract::Form;
use serde::Deserialize;

#[derive(Deserialize)]
struct LoginForm {
    username: String,
    password: String,
}

async fn login(Form(form): Form<LoginForm>) -> String {
    format!("Logging in user: {}", form.username)
}
Headers エクストラクター

リクエストヘッダーにアクセスします。

use axum::http::HeaderMap;

async fn handler(headers: HeaderMap) -> String {
    let user_agent = headers
        .get("user-agent")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("unknown");
    format!("User-Agent: {}", user_agent)
}
カスタムエクストラクター

FromRequest または FromRequestParts を実装して、カスタムエクストラクターを作成します。

use axum::{
    extract::{FromRequest, Request},
    response::{IntoResponse, Response},
    http::StatusCode,
    async_trait,
};

struct AuthenticatedUser {
    id: u32,
    username: String,
}

#[async_trait]
impl<S> FromRequestParts<S> for AuthenticatedUser
where
    S: Send + Sync,
{
    type Rejection = Response;

    async fn from_request_parts(
        parts: &mut Parts,
        state: &S,
    ) -> Result<Self, Self::Rejection> {
        // Extract and validate auth token
        let auth_header = parts
            .headers
            .get("authorization")
            .and_then(|v| v.to_str().ok())
            .ok_or_else(|| {
                (StatusCode::UNAUTHORIZED, "Missing authorization header")
                    .into_response()
            })?;

        // Validate token and return user
        Ok(AuthenticatedUser {
            id: 1,
            username: "alice".to_string(),
        })
    }
}

async fn protected_route(user: AuthenticatedUser) -> String {
    format!("Hello, {}", user.username)
}

ミドルウェア

ミドルウェアの適用

ミドルウェアを適用する 3 つの方法:

  1. ルーターレベル - すべてのルートに影響します
  2. ルートレベル - 特定のルートに影響します
  3. メソッドレベル - ルート上の特定のメソッドに影響します
use axum::{Router, routing::get, middleware};
use tower_http::trace::TraceLayer;

// 1. Router-level
let app = Router::new()
    .route("/", get(handler))
    .layer(TraceLayer::new_for_http());

// 2. Route-level
let app = Router::new()
    .route("/protected", get(handler))
    .route_layer(middleware::from_fn(auth_middleware));

// 3. Method-level
let app = Router::new()
    .route("/resource",
        get(handler)
            .route_layer(middleware::from_fn(read_only_auth))
            .post(create_handler)
            .route_layer(middleware::from_fn(write_auth))
    );
ミドルウェアの実行順序

.layer() を順次呼び出す場合(下から上へ):

let app = Router::new()
    .route("/", get(handler))
    .layer(layer_three)  // Executes third
    .layer(layer_two)    // Executes second
    .layer(layer_one);   // Executes first

ServiceBuilder を使う場合(上から下へ):

use tower::ServiceBuilder;

let app = Router::new()
    .route("/", get(handler))
    .layer(
        ServiceBuilder::new()
            .layer(layer_one)    // Executes first
            .layer(layer_two)    // Executes second
            .layer(layer_three)  // Executes third
    );
よく使われる Tower ミドルウェア

TraceLayer - HTTP リクエストのトレース:

use tower_http::trace::TraceLayer;

let app = Router::new()
    .route("/", get(handler))
    .layer(TraceLayer::new_for_http());

CompressionLayer - レスポンスの圧縮:

use tower_http::compression::CompressionLayer;

let app = Router::new()
    .route("/", get(handler))
    .layer(CompressionLayer::new());

CorsLayer - CORS の処理:

use tower_http::cors::{CorsLayer, Any};

let cors = CorsLayer::new()
    .allow_origin(Any)
    .allow_methods(Any)
    .allow_headers(Any);

let app = Router::new()
    .route("/api/data", get(handler))
    .layer(cors);

TimeoutLayer - リクエストのタイムアウト:

use tower::timeout::TimeoutLayer;
use std::time::Duration;

let app = Router::new()
    .route("/", get(handler))
    .layer(TimeoutLayer::new(Duration::from_secs(30)));
HandleErrorLayer

ミドルウェアのエラーを HTTP レスポンスに変換します。

use axum::{
    error_handling::HandleErrorLayer,
    http::{StatusCode, Method, Uri},
    BoxError,
};
use tower::ServiceBuilder;
use std::time::Duration;

async fn handle_timeout_error(
    method: Method,
    uri: Uri,
    err: BoxError,
) -> (StatusCode, String) {
    if err.is::<tower::timeout::error::Elapsed>() {
        (
            StatusCode::REQUEST_TIMEOUT,
            format!("`{} {}` request timed out", method, uri),
        )
    } else {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("`{} {}` failed with {}", method, uri, err),
        )
    }
}

let app = Router::new()
    .route("/", get(handler))
    .layer(
        ServiceBuilder::new()
            .layer(HandleErrorLayer::new(handle_timeout_error))
            .layer(TimeoutLayer::new(Duration::from_secs(30)))
    );
from_fn によるカスタムミドルウェア

非同期関数を使ってカスタムミドルウェアを作成します:

use axum::{
    middleware::{self, Next},
    extract::Request,
    response::Response,
    http::StatusCode,
};

async fn auth_middleware(
    req: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    let auth_header = req.headers()
        .get("authorization")
        .and_then(|h| h.to_str().ok());

    if let Some(auth_header) = auth_header {
        if validate_token(auth_header).await {
            Ok(next.run(req).await)
        } else {
            Err(StatusCode::UNAUTHORIZED)
        }
    } else {
        Err(StatusCode::UNAUTHORIZED)
    }
}

let app = Router::new()
    .route("/protected", get(handler))
    .layer(middleware::from_fn(auth_middleware));

ミドルウェアからハンドラーにデータを渡す:

use axum::extract::Extension;

#[derive(Clone)]
struct CurrentUser {
    id: u32,
    username: String,
}

async fn auth_middleware(
    mut req: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    let auth_header = req.headers()
        .get("authorization")
        .and_then(|h| h.to_str().ok())
        .ok_or(StatusCode::UNAUTHORIZED)?;

    if let Some(user) = authorize_user(auth_header).await {
        req.extensions_mut().insert(user);
        Ok(next.run(req).await)
    } else {
        Err(StatusCode::UNAUTHORIZED)
    }
}

async fn handler(Extension(user): Extension<CurrentUser>) -> String {
    format!("Hello, {}", user.username)
}

let app = Router::new()
    .route("/", get(handler))
    .layer(middleware::from_fn(auth_middleware));
カスタム Tower ミドルウェア

Tower の Service トレイトと Layer トレイトを実装して、完全に制御します:

use tower::{Service, Layer};
use axum::{response::Response, extract::Request};
use std::task::{Context, Poll};
use futures_core::future::BoxFuture;

#[derive(Clone)]
struct MyLayer;

impl<S> Layer<S> for MyLayer {
    type Service = MyMiddleware<S>;

    fn layer(&self, inner: S) -> Self::Service {
        MyMiddleware { inner }
    }
}

#[derive(Clone)]
struct MyMiddleware<S> {
    inner: S,
}

impl<S> Service<Request> for MyMiddleware<S>
where
    S: Service<Request, Response = Response> + Send + 'static,
    S::Future: Send + 'static,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, request: Request) -> Self::Future {
        // Process request
        let future = self.inner.call(request);
        Box::pin(async move {
            let response: Response = future.await?;
            // Process response
            Ok(response)
        })
    }
}

let app = Router::new()
    .route("/", get(handler))
    .layer(MyLayer);
状態にアクセスするミドルウェア

アプリケーションの状態にアクセスするミドルウェアを作成します:

use axum::extract::State;
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    db: Arc<Database>,
}

#[derive(Clone)]
struct MyLayer {
    state: AppState,
}

impl<S> Layer<S> for MyLayer {
    type Service = MyService<S>;

    fn layer(&self, inner: S) -> Self::Service {
        MyService {
            inner,
            state: self.state.clone(),
        }
    }
}

#[derive(Clone)]
struct MyService<S> {
    inner: S,
    state: AppState,
}

impl<S> Service<Request> for MyService<S>
where
    S: Service<Request>,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = S::Future;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: Request) -> Self::Future {
        // Use self.state here
        self.inner.call(req)
    }
}

let state = AppState {
    db: Arc::new(Database::new()),
};

let app = Router::new()
    .route("/", get(handler))
    .layer(MyLayer { state: state.clone() })
    .with_state(state);
ルーティング前のミドルウェア

ルーティング前にミドルウェアを適用します(例:URI の書き換え):

use tower::Layer;
use axum::ServiceExt;

fn rewrite_request_uri(req: Request) -> Request {
    // Modify request URI
    req
}

let middleware = tower::util::MapRequestLayer::new(rewrite_request_uri);

let app = Router::new()
    .route("/", get(handler));

// Apply layer around entire router
let app_with_middleware = middleware.layer(app);

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app_with_middleware.into_make_service()).await?;

状態管理

基本的な状態

クローン可能な型を使ったシンプルな状態です:

use axum::extract::State;

#[derive(Clone)]
struct AppState {
    config: Config,
    api_key: String,
}

async fn handler(State(state): State<AppState>) -> String {
    format!("Config: {:?}, Key: {}", state.config, state.api_key)
}

let state = AppState {
    config: Config::default(),
    api_key: "secret".to_string(),
};

let app = Router::new()
    .route("/", get(handler))
    .with_state(state);
Arc による共有状態

クローンにコストのかかる型の共有所有権には Arc を使います:

use std::sync::Arc;
use tokio::sync::RwLock;

#[derive(Clone)]
struct AppState {
    db_pool: Arc<DatabasePool>,
    cache: Arc<RwLock<HashMap<String, String>>>,
    config: Config, // Cheap to clone
}

async fn handler(State(state): State<AppState>) -> Result<String, StatusCode> {
    // Access database pool
    let conn = state.db_pool.get().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    // Access cache (read)
    let cache = state.cache.read().await;
    let value = cache.get("key");

    // Access cache (write)
    drop(cache); // Release read lock
    let mut cache = state.cache.write().await;
    cache.insert("new_key".to_string(), "value".to_string());

    Ok("Success".to_string())
}
複数の状態型

ルーターの各セクションで異なる状態型を使います:

#[derive(Clone)]
struct ApiState {
    api_key: String,
}

#[derive(Clone)]
struct AppState {
    db: Arc<Database>,
}

fn api_routes() -> Router<ApiState> {
    Router::new()
        .route("/data", get(|State(state): State<ApiState>| async move {
            format!("API Key: {}", state.api_key)
        }))
}

fn app_routes() -> Router<AppState> {
    Router::new()
        .route("/users", get(|State(state): State<AppState>| async move {
            "Users".to_string()
        }))
}

let api_state = ApiState { api_key: "secret".to_string() };
let app_state = AppState { db: Arc::new(Database::new()) };

let app = Router::new()
    .nest("/api", api_routes().with_state(api_state))
    .nest("/app", app_routes().with_state(app_state));
関数内のジェネリックな状態

柔軟性を高めるため、状態型をジェネリックにしたルーターを返します:

fn routes<S>() -> Router<S>
where
    S: Clone + Send + Sync + 'static,
{
    Router::new()
        .route("/health", get(|| async { "OK" }))
        .route("/version", get(|| async { "1.0.0" }))
}

// Can be combined with any state type
let app = Router::new()
    .merge(routes())
    .route("/", get(handler))
    .with_state(AppState { /* ... */ });
状態の遷移

異なる状態要件を持つルーターを連結します:

#[derive(Clone)]
struct StateA {
    data_a: String,
}

#[derive(Clone)]
struct StateB {
    data_b: String,
}

let router_a: Router<StateA> = Router::new()
    .route("/a", get(|State(s): State<StateA>| async move { s.data_a }));

// Provide StateA, next missing state is StateB
let router_b: Router<StateB> = router_a.with_state(StateA {
    data_a: "A".to_string(),
});

// Add routes needing StateB
let router_b = router_b
    .route("/b", get(|State(s): State<StateB>| async move { s.data_b }));

// Provide StateB, now we have Router<()>
let app: Router<()> = router_b.with_state(StateB {
    data_b: "B".to_string(),
});

エラー処理

Result による基本的なエラー処理

ハンドラー内のエラーを処理するには Result を使います:

use axum::http::StatusCode;

async fn handler() -> Result<String, StatusCode> {
    let result = some_operation().await;
    match result {
        Ok(data) => Ok(format!("Success: {}", data)),
        Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
    }
}
カスタムエラー型

カスタムエラーに IntoResponse を実装します:

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

#[derive(Debug)]
enum AppError {
    Database(sqlx::Error),
    NotFound,
    Unauthorized,
    ValidationError(String),
}

#[derive(Serialize)]
struct ErrorResponse {
    error: String,
    message: String,
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, error_message) = match self {
            AppError::Database(e) => {
                tracing::error!("Database error: {}", e);
                (StatusCode::INTERNAL_SERVER_ERROR, "Database error")
            }
            AppError::NotFound => (StatusCode::NOT_FOUND, "Resource not found"),
            AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized"),
            AppError::ValidationError(msg) => (StatusCode::BAD_REQUEST, &msg),
        };

        let body = Json(ErrorResponse {
            error: status.to_string(),
            message: error_message.to_string(),
        });

        (status, body).into_response()
    }
}

// Use in handler
async fn get_user(Path(id): Path<u32>) -> Result<Json<User>, AppError> {
    let user = db.get_user(id).await.map_err(AppError::Database)?;
    user.ok_or(AppError::NotFound).map(Json)
}
エクストラクターの拒否処理

より適切なエラーメッセージを表示できるよう、エクストラクターによる拒否を処理します:

use axum::extract::rejection::JsonRejection;

async fn create_user(
    payload: Result<Json<CreateUser>, JsonRejection>,
) -> Result<Json<User>, AppError> {
    let Json(create_user) = payload.map_err(|err| match err {
        JsonRejection::MissingJsonContentType(_) => {
            AppError::ValidationError("Content-Type must be application/json".to_string())
        }
        JsonRejection::JsonDataError(e) => {
            AppError::ValidationError(format!("Invalid JSON: {}", e))
        }
        JsonRejection::JsonSyntaxError(e) => {
            AppError::ValidationError(format!("JSON syntax error: {}", e))
        }
        _ => AppError::ValidationError("Invalid request body".to_string()),
    })?;

    // Process create_user
    Ok(Json(user))
}
エラー処理を伴うカスタムエクストラクター

カスタムの拒否型を持つエクストラクターを作成します:

use axum::{
    extract::{FromRequest, Request},
    response::{IntoResponse, Response},
    async_trait,
};

struct ValidatedJson<T>(T);

#[async_trait]
impl<S, T> FromRequest<S> for ValidatedJson<T>
where
    T: serde::de::DeserializeOwned + Validate,
    S: Send + Sync,
{
    type Rejection = Response;

    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
        let Json(data) = Json::<T>::from_request(req, state)
            .await
            .map_err(|err| {
                (
                    StatusCode::BAD_REQUEST,
                    format!("Invalid JSON: {}", err),
                ).into_response()
            })?;

        data.validate().map_err(|err| {
            (
                StatusCode::BAD_REQUEST,
                format!("Validation error: {}", err),
            ).into_response()
        })?;

        Ok(ValidatedJson(data))
    }
}

async fn handler(ValidatedJson(data): ValidatedJson<MyData>) -> String {
    // data is validated
    format!("Received: {:?}", data)
}
ミドルウェアのエラー処理

失敗する可能性があるミドルウェアのエラーを処理します:

use axum::error_handling::HandleErrorLayer;
use tower::ServiceBuilder;

async fn handle_timeout_error(err: BoxError) -> (StatusCode, String) {
    if err.is::<tower::timeout::error::Elapsed>() {
        (
            StatusCode::REQUEST_TIMEOUT,
            "Request took too long".to_string(),
        )
    } else {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("Unhandled error: {}", err),
        )
    }
}

let app = Router::new()
    .route("/", get(handler))
    .layer(
        ServiceBuilder::new()
            .layer(HandleErrorLayer::new(handle_timeout_error))
            .layer(TimeoutLayer::new(Duration::from_secs(30)))
    );
失敗する可能性のあるサービス

失敗する可能性のあるサービスにルーティングします:

use axum::error_handling::HandleError;

async fn fallible_operation() -> Result<(), anyhow::Error> {
    // Operation that might fail
    Ok(())
}

let fallible_service = tower::service_fn(|_req| async {
    fallible_operation().await?;
    Ok::<_, anyhow::Error>(Response::new(Body::empty()))
});

async fn handle_error(err: anyhow::Error) -> (StatusCode, String) {
    (
        StatusCode::INTERNAL_SERVER_ERROR,
        format!("Something went wrong: {}", err),
    )
}

let app = Router::new().route_service(
    "/",
    HandleError::new(fallible_service, handle_error),
);

レスポンスの構築

JSON レスポンス

型安全性を保ちながら JSON を返します:

use axum::Json;
use serde::Serialize;

#[derive(Serialize)]
struct ApiResponse {
    success: bool,
    data: String,
}

async fn handler() -> Json<ApiResponse> {
    Json(ApiResponse {
        success: true,
        data: "Hello".to_string(),
    })
}
HTML レスポンス

HTML コンテンツを配信します:

use axum::response::Html;

async fn handler() -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}

async fn dynamic_html(Path(name): Path<String>) -> Html<String> {
    Html(format!("<h1>Hello, {}</h1>", name))
}
ステータスコードのレスポンス

さまざまなステータスコードを返します:

use axum::http::StatusCode;

async fn handler() -> StatusCode {
    StatusCode::NO_CONTENT
}

async fn with_body() -> (StatusCode, String) {
    (StatusCode::CREATED, "Resource created".to_string())
}

async fn json_with_status() -> (StatusCode, Json<ApiResponse>) {
    (StatusCode::CREATED, Json(ApiResponse { /* ... */ }))
}
カスタムヘッダー

レスポンスにカスタムヘッダーを追加します:

use axum::http::{HeaderMap, header};

async fn handler() -> (HeaderMap, String) {
    let mut headers = HeaderMap::new();
    headers.insert(header::CACHE_CONTROL, "max-age=3600".parse().unwrap());
    headers.insert("X-Custom-Header", "value".parse().unwrap());
    (headers, "Response body".to_string())
}
レスポンスの完全な制御

完全なレスポンスを構築します:

use axum::{
    response::Response,
    http::{StatusCode, header},
    body::Body,
};

async fn handler() -> Response {
    Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, "application/json")
        .header("X-Custom", "value")
        .body(Body::from(r#"{"status":"ok"}"#))
        .unwrap()
}
ストリーミングレスポンス

クライアントにデータをストリーミングします:

use axum::response::sse::{Event, Sse};
use futures::stream::{self, Stream};
use std::convert::Infallible;

async fn sse_handler() -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
    let stream = stream::iter(0..10).map(|i| {
        Ok(Event::default().data(format!("Event {}", i)))
    });
    Sse::new(stream)
}
ファイルのダウンロード

ダウンロード用のファイルを配信します:

use axum::{
    response::{Response, IntoResponse},
    http::{header, StatusCode},
    body::Body,
};
use tokio::fs::File;

async fn download_file() -> Result<Response, StatusCode> {
    let file = File::open("path/to/file.pdf")
        .await
        .map_err(|_| StatusCode::NOT_FOUND)?;

    let body = Body::from_stream(tokio_util::io::ReaderStream::new(file));

    Ok(Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, "application/pdf")
        .header(
            header::CONTENT_DISPOSITION,
            "attachment; filename=\"file.pdf\"",
        )
        .body(body)
        .unwrap())
}

本番環境向けのパターン

データベース連携

データベースプールと連携します:

use sqlx::{PgPool, postgres::PgPoolOptions};
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    db: PgPool,
}

async fn get_user(
    State(state): State<AppState>,
    Path(id): Path<i64>,
) -> Result<Json<User>, AppError> {
    let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
        .fetch_optional(&state.db)
        .await
        .map_err(AppError::Database)?
        .ok_or(AppError::NotFound)?;

    Ok(Json(user))
}

#[tokio::main]
async fn main() {
    let db = PgPoolOptions::new()
        .max_connections(5)
        .connect(&env::var("DATABASE_URL").unwrap())
        .await
        .unwrap();

    let state = AppState { db };

    let app = Router::new()
        .route("/users/:id", get(get_user))
        .with_state(state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
構成管理

環境変数と構成を使用します:

use serde::Deserialize;
use config::{Config, ConfigError, Environment};

#[derive(Debug, Deserialize, Clone)]
struct Settings {
    database_url: String,
    redis_url: String,
    jwt_secret: String,
    port: u16,
}

impl Settings {
    fn new() -> Result<Self, ConfigError> {
        Config::builder()
            .add_source(Environment::default())
            .build()?
            .try_deserialize()
    }
}

#[derive(Clone)]
struct AppState {
    settings: Settings,
    db: PgPool,
}

#[tokio::main]
async fn main() {
    let settings = Settings::new().expect("Failed to load configuration");

    let db = PgPoolOptions::new()
        .connect(&settings.database_url)
        .await
        .expect("Failed to connect to database");

    let state = AppState {
        settings: settings.clone(),
        db,
    };

    let app = Router::new()
        .route("/", get(handler))
        .with_state(state);

    let addr = format!("0.0.0.0:{}", settings.port);
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
構造化ログ

包括的なログ機能を実装します:

use tracing::{info, error, debug, instrument};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

#[tokio::main]
async fn main() {
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "info".into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    info!("Starting server");

    let app = Router::new()
        .route("/", get(handler))
        .layer(TraceLayer::new_for_http());

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    info!("Listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await.unwrap();
}

#[instrument]
async fn handler(Path(id): Path<u32>) -> Result<String, AppError> {
    debug!("Handling request for user {}", id);

    match get_user_from_db(id).await {
        Ok(user) => {
            info!("Successfully retrieved user {}", id);
            Ok(format!("User: {}", user))
        }
        Err(e) => {
            error!("Failed to get user {}: {}", id, e);
            Err(AppError::Database(e))
        }
    }
}
グレースフルシャットダウン

グレースフルシャットダウン処理を実装します:

use tokio::signal;

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 => {},
        _ = terminate => {},
    }

    println!("Signal received, starting graceful shutdown");
}

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

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app)
        .with_graceful_shutdown(shutdown_signal())
        .await
        .unwrap();
}
ヘルスチェック

ヘルスチェック用のエンドポイントを実装します:

use serde::Serialize;

#[derive(Serialize)]
struct HealthResponse {
    status: String,
    database: String,
    cache: String,
}

async fn health_check(State(state): State<AppState>) -> Json<HealthResponse> {
    let db_status = match sqlx::query("SELECT 1").fetch_one(&state.db).await {
        Ok(_) => "healthy",
        Err(_) => "unhealthy",
    };

    let cache_status = match state.redis.ping().await {
        Ok(_) => "healthy",
        Err(_) => "unhealthy",
    };

    Json(HealthResponse {
        status: if db_status == "healthy" && cache_status == "healthy" {
            "healthy".to_string()
        } else {
            "degraded".to_string()
        },
        database: db_status.to_string(),
        cache: cache_status.to_string(),
    })
}

let app = Router::new()
    .route("/health", get(health_check))
    .route("/ready", get(readiness_check))
    .with_state(state);
レート制限

レート制限を実装します:

use tower::limit::RateLimitLayer;
use std::time::Duration;

let app = Router::new()
    .route("/api/data", get(handler))
    .layer(RateLimitLayer::new(
        100, // max requests
        Duration::from_secs(60), // per minute
    ));
リクエストの検証

カスタムエクストラクターでリクエストを検証します:

use validator::{Validate, ValidationError};

#[derive(Debug, Deserialize, Validate)]
struct CreateUserRequest {
    #[validate(length(min = 3, max = 50))]
    username: String,
    #[validate(email)]
    email: String,
    #[validate(length(min = 8))]
    password: String,
}

struct ValidatedJson<T>(T);

#[async_trait]
impl<S, T> FromRequest<S> for ValidatedJson<T>
where
    T: DeserializeOwned + Validate,
    S: Send + Sync,
{
    type Rejection = (StatusCode, String);

    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
        let Json(data) = Json::<T>::from_request(req, state)
            .await
            .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;

        data.validate()
            .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;

        Ok(ValidatedJson(data))
    }
}

async fn create_user(
    ValidatedJson(user): ValidatedJson<CreateUserRequest>,
) -> Result<Json<User>, AppError> {
    // user is validated
    Ok(Json(User { /* ... */ }))
}

テスト

ハンドラーの単体テスト

ハンドラーを単独でテストします:

#[cfg(test)]
mod tests {
    use super::*;
    use axum::body::Body;
    use axum::http::{Request, StatusCode};
    use tower::ServiceExt;

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

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

        assert_eq!(response.status(), StatusCode::OK);
    }
}
統合テスト

アプリケーション全体をテストします:

#[cfg(test)]
mod tests {
    use super::*;
    use axum::body::Body;
    use axum::http::{Request, StatusCode, Method};
    use tower::ServiceExt;
    use serde_json::json;

    #[tokio::test]
    async fn test_create_user() {
        let state = AppState::test();
        let app = create_app(state);

        let request_body = json!({
            "username": "testuser",
            "email": "[email protected]"
        });

        let response = app
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/users")
                    .header("content-type", "application/json")
                    .body(Body::from(request_body.to_string()))
                    .unwrap()
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::CREATED);
    }
}

ベストプラクティス

ハンドラーの構成
  1. ハンドラーは薄く保つ - ビジネスロジックをサービス層に移します
  2. エクストラクターを使う - 型システムに抽出処理を任せます
  3. Result 型を返す - カスタムエラー型を使います
  4. ハンドラーを計測する - 可観測性のためにトレーシングを追加します
状態管理
  1. コストの高い型には Arc を使う - データベースプール、キャッシュなど
  2. 状態を最小限に保つ - 本当に共有が必要なものだけにします
  3. Clone を実装する - State エクストラクターに必要です
  4. 変更を避ける - 必要に応じて内部可変性(RwLock、Mutex)を使います
エラー処理
  1. カスタムエラー型を作成する - IntoResponse を実装します
  2. コンテキストを提供する - 役立つエラーメッセージを含めます
  3. エラーを適切にログに記録する - tracing を使います
  4. 適切なステータスコードを返す - HTTP のセマンティクスに合わせます
ミドルウェア
  1. ServiceBuilder を使う - より直感的な順序で構成できます
  2. 適切なレベルで適用する - Router、route、method のいずれかに適用します
  3. エラーを処理する - 失敗する可能性があるミドルウェアには HandleErrorLayer を使います
  4. ミドルウェアの責務を絞る - 単一の責務に専念させます
パフォーマンス
  1. コネクションプーリングを使う - データベースや外部サービス向けに使います
  2. 圧縮を有効にする - レスポンスには CompressionLayer を使います
  3. キャッシュを実装する - 冗長な処理を減らします
  4. バックプレッシャーを使う - LoadShedLayer、RateLimitLayer を使います
  5. シリアライズを最適化する - 効率的な JSON ライブラリを使います
セキュリティ
  1. 入力を検証する - 検証機能を備えたカスタムエクストラクターを使用します
  2. 認証を実装する - 認証にはミドルウェアを使用します
  3. HTTPS を使用する - 本番環境で使用します
  4. セキュリティヘッダーを設定する - CORS、CSP など
  5. レート制限を設定する - 悪用を防ぎます

一般的なパターン

リポジトリパターン
trait UserRepository {
    async fn get(&self, id: u32) -> Result<User, AppError>;
    async fn create(&self, user: CreateUser) -> Result<User, AppError>;
    async fn update(&self, id: u32, user: UpdateUser) -> Result<User, AppError>;
    async fn delete(&self, id: u32) -> Result<(), AppError>;
}

struct PostgresUserRepository {
    pool: PgPool,
}

impl UserRepository for PostgresUserRepository {
    async fn get(&self, id: u32) -> Result<User, AppError> {
        // Implementation
    }
}

#[derive(Clone)]
struct AppState {
    user_repo: Arc<dyn UserRepository + Send + Sync>,
}

async fn get_user(
    State(state): State<AppState>,
    Path(id): Path<u32>,
) -> Result<Json<User>, AppError> {
    let user = state.user_repo.get(id).await?;
    Ok(Json(user))
}
サービスレイヤーパターン
struct UserService {
    repo: Arc<dyn UserRepository + Send + Sync>,
    email_service: Arc<EmailService>,
}

impl UserService {
    async fn create_user(&self, data: CreateUser) -> Result<User, AppError> {
        let user = self.repo.create(data).await?;
        self.email_service.send_welcome_email(&user).await?;
        Ok(user)
    }
}

#[derive(Clone)]
struct AppState {
    user_service: Arc<UserService>,
}
バージョン管理された API
fn v1_routes() -> Router<AppState> {
    Router::new()
        .route("/users", get(v1::list_users))
        .route("/users/:id", get(v1::get_user))
}

fn v2_routes() -> Router<AppState> {
    Router::new()
        .route("/users", get(v2::list_users))
        .route("/users/:id", get(v2::get_user))
}

let app = Router::new()
    .nest("/api/v1", v1_routes())
    .nest("/api/v2", v2_routes())
    .with_state(state);

スキルのバージョン: 1.0.0 最終更新日: October 2025 スキルのカテゴリ: Web 開発、REST API、Rust、非同期プログラミング 対応環境: Axum 0.7+、Tokio 1.0+、Tower 0.4+

コメント