Axum

Tokioエコシステムと完全統合されたモダンなRust Webフレームワーク。エルゴノミクスに優れ、Actix-webに匹敵する性能を実現。

RustフレームワークWeb開発非同期TowerTokioHTTP/2

GitHub概要

tokio-rs/axum

Ergonomic and modular web framework built with Tokio, Tower, and Hyper

スター22,693
ウォッチ141
フォーク1,222
作成日:2021年5月30日
言語:Rust
ライセンス:MIT License

トピックス

なし

スター履歴

tokio-rs/axum Star History
データ取得日時: 2025/8/13 01:43

フレームワーク

Axum

概要

Axumは、Tokio・Tower・Hyperエコシステムを基盤とするRust言語のWebフレームワークです。マクロフリーなAPIと強力な型システムによる安全性を重視しています。

詳細

Axum(アクサム)は2021年にTokio teamによって開発された、Rust言語向けのモダンなWebフレームワークです。TokioとTowerエコシステムを中核とし、Hyperを HTTP 実装として使用することで、業界標準の非同期処理基盤を活用しています。AxumはMacro-freeなAPIを採用しており、手続きマクロに依存せずに型安全なWeb開発を実現します。また、Tower::ServiceとTower::Layerによる堅牢なミドルウェアシステムを提供し、再利用性と組み合わせ可能性を重視した設計になっています。Extractorシステムにより、リクエストデータの型安全な抽出が可能で、各種リクエスト要素(パス、クエリ、ヘッダー、ボディ等)を構造化されたRustの型として受け取れます。さらに、WebSocketサポート、レスポンスタイプの柔軟性、JSONシリアライゼーション、ファイルサービング、エラーハンドリングなどの機能を標準搭載しています。 Tokioエコシステムとの深い統合により、高い性能と拡張性を提供し、エンタープライズレベルのWebアプリケーション開発に適しています。

メリット・デメリット

メリット

  • マクロフリーAPI: 手続きマクロに依存しない明確で理解しやすいAPI
  • Tower統合: 豊富なTowerミドルウェアエコシステムとの完全互換性
  • 型安全性: Rustの強力な型システムによるコンパイル時エラー検出
  • 高性能: Tokio/Hyperベースの極めて高速な非同期処理
  • エクストラクタシステム: 型安全なリクエストデータ抽出機能
  • 人間工学的設計: 開発者体験を重視した直感的なAPI
  • 活発な開発: Tokio teamによる継続的な改善とサポート

デメリット

  • 学習コスト: Rust言語とライフタイム概念の習得が必要
  • エコシステム: 比較的新しいため、他言語に比べライブラリが限定的
  • コンパイル時間: Rust特有の長いコンパイル時間
  • 型複雑性: 高度な型安全性ゆえの型システムの複雑さ
  • デバッグ難易度: 非同期コードとライフタイムのデバッグが困難

主要リンク

書き方の例

Hello World

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

#[tokio::main]
async fn main() {
    // ルーターを作成
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .route("/html", get(html_handler));

    // サーバー起動
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("サーバー起動: http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}

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

ルーティングとパス抽出

use axum::{
    extract::{Path, Query},
    http::StatusCode,
    response::Json,
    routing::{get, post},
    Router,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

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

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

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(root))
        .route("/users/:id", get(get_user))
        .route("/users/:id/:action", post(user_action))
        .route("/search", get(search_users))
        .route("/users", post(create_user));

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

async fn root() -> &'static str {
    "Axum APIサーバー"
}

// パスパラメータの抽出
async fn get_user(Path(user_id): Path<u32>) -> Json<User> {
    let user = User {
        id: user_id,
        name: format!("ユーザー{}", user_id),
        email: format!("user{}@example.com", user_id),
    };
    Json(user)
}

// 複数のパスパラメータ
async fn user_action(
    Path((user_id, action)): Path<(u32, String)>,
) -> Result<String, StatusCode> {
    match action.as_str() {
        "activate" => Ok(format!("ユーザー{}をアクティベートしました", user_id)),
        "deactivate" => Ok(format!("ユーザー{}を非アクティブにしました", user_id)),
        "delete" => Ok(format!("ユーザー{}を削除しました", user_id)),
        _ => Err(StatusCode::BAD_REQUEST),
    }
}

// クエリパラメータの抽出
async fn search_users(Query(params): Query<Pagination>) -> Json<serde_json::Value> {
    let page = params.page.unwrap_or(1);
    let per_page = params.per_page.unwrap_or(10);
    
    Json(serde_json::json!({
        "page": page,
        "per_page": per_page,
        "results": [
            {"id": 1, "name": "ユーザー1"},
            {"id": 2, "name": "ユーザー2"}
        ]
    }))
}

// JSONボディの抽出
async fn create_user(Json(payload): Json<User>) -> Result<(StatusCode, Json<User>), StatusCode> {
    // バリデーション
    if payload.name.is_empty() {
        return Err(StatusCode::BAD_REQUEST);
    }
    
    // 新しいユーザーを作成
    let user = User {
        id: 42, // データベースから取得したID
        name: payload.name,
        email: payload.email,
    };
    
    Ok((StatusCode::CREATED, Json(user)))
}

ミドルウェアとレイヤー

use axum::{
    extract::Request,
    http::{header, HeaderValue, Method, StatusCode},
    middleware::{self, Next},
    response::Response,
    routing::get,
    Router,
};
use std::time::Duration;
use tower::{
    buffer::BufferLayer,
    limit::RateLimitLayer,
    ServiceBuilder,
};
use tower_http::{
    cors::{Any, CorsLayer},
    timeout::TimeoutLayer,
    trace::TraceLayer,
};

#[tokio::main]
async fn main() {
    // Towerミドルウェアスタック
    let middleware_stack = ServiceBuilder::new()
        .layer(TraceLayer::new_for_http())
        .layer(
            CorsLayer::new()
                .allow_origin(Any)
                .allow_methods([Method::GET, Method::POST])
                .allow_headers([header::CONTENT_TYPE]),
        )
        .layer(BufferLayer::new(1024))
        .layer(RateLimitLayer::new(5, Duration::from_secs(1)))
        .layer(TimeoutLayer::new(Duration::from_secs(30)));

    let app = Router::new()
        .route("/", get(|| async { "Hello" }))
        .route("/protected", get(protected_handler))
        .layer(middleware_stack)
        .layer(middleware::from_fn(custom_middleware))
        .route_layer(middleware::from_fn(auth_middleware));

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

// カスタムミドルウェア
async fn custom_middleware(request: Request, next: Next) -> Response {
    println!("リクエスト処理前: {} {}", request.method(), request.uri());
    
    let response = next.run(request).await;
    
    println!("レスポンス処理後: {}", response.status());
    response
}

// 認証ミドルウェア
async fn auth_middleware(mut request: Request, next: Next) -> Result<Response, StatusCode> {
    let auth_header = request
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|header| header.to_str().ok());

    match auth_header {
        Some(token) if token.starts_with("Bearer ") => {
            if token == "Bearer valid_token" {
                // ユーザー情報をリクエストに挿入
                request.extensions_mut().insert(UserId(123));
                Ok(next.run(request).await)
            } else {
                Err(StatusCode::UNAUTHORIZED)
            }
        }
        _ => Err(StatusCode::UNAUTHORIZED),
    }
}

#[derive(Clone)]
struct UserId(u32);

async fn protected_handler() -> &'static str {
    "保護されたリソース"
}

状態管理とデータベース統合

use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::Json,
    routing::{get, post},
    Router,
};
use serde::{Deserialize, Serialize};
use sqlx::{PgPool, Row};
use std::sync::Arc;

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

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

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

#[tokio::main]
async fn main() {
    // データベース接続プール
    let database_url = std::env::var("DATABASE_URL")
        .unwrap_or_else(|_| "postgres://user:password@localhost/axum_db".to_string());
    
    let pool = PgPool::connect(&database_url)
        .await
        .expect("データベースに接続できませんでした");

    // アプリケーション状態
    let app_state = AppState { db: pool };

    let app = Router::new()
        .route("/users", get(get_users).post(create_user))
        .route("/users/:id", get(get_user))
        .with_state(app_state);

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

// 全ユーザー取得
async fn get_users(State(state): State<AppState>) -> Result<Json<Vec<User>>, StatusCode> {
    let rows = sqlx::query("SELECT id, name, email FROM users")
        .fetch_all(&state.db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    let users: Vec<User> = rows
        .iter()
        .map(|row| User {
            id: row.get("id"),
            name: row.get("name"),
            email: row.get("email"),
        })
        .collect();

    Ok(Json(users))
}

// 特定ユーザー取得
async fn get_user(
    Path(user_id): Path<i32>,
    State(state): State<AppState>,
) -> Result<Json<User>, StatusCode> {
    let row = sqlx::query("SELECT id, name, email FROM users WHERE id = $1")
        .bind(user_id)
        .fetch_optional(&state.db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    match row {
        Some(row) => {
            let user = User {
                id: row.get("id"),
                name: row.get("name"),
                email: row.get("email"),
            };
            Ok(Json(user))
        }
        None => Err(StatusCode::NOT_FOUND),
    }
}

// ユーザー作成
async fn create_user(
    State(state): State<AppState>,
    Json(payload): Json<CreateUser>,
) -> Result<(StatusCode, Json<User>), StatusCode> {
    let row = sqlx::query(
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email"
    )
    .bind(&payload.name)
    .bind(&payload.email)
    .fetch_one(&state.db)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    let user = User {
        id: row.get("id"),
        name: row.get("name"),
        email: row.get("email"),
    };

    Ok((StatusCode::CREATED, Json(user)))
}

エラーハンドリングとカスタムレスポンス

use axum::{
    extract::rejection::JsonRejection,
    http::StatusCode,
    response::{IntoResponse, Response},
    routing::post,
    Json, Router,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("バリデーションエラー: {0}")]
    ValidationError(String),
    #[error("データベースエラー")]
    DatabaseError(#[from] sqlx::Error),
    #[error("JSON解析エラー")]
    JsonExtractorError(#[from] JsonRejection),
    #[error("認証エラー")]
    AuthenticationError,
}

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

// AppErrorをレスポンスに変換
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status_code, message) = match self {
            AppError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg),
            AppError::DatabaseError(_) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "データベースエラーが発生しました".to_string(),
            ),
            AppError::JsonExtractorError(_) => (
                StatusCode::BAD_REQUEST,
                "無効なJSONフォーマットです".to_string(),
            ),
            AppError::AuthenticationError => (
                StatusCode::UNAUTHORIZED,
                "認証が必要です".to_string(),
            ),
        };

        let error_response = ErrorResponse {
            error: self.to_string(),
            message,
            code: status_code.as_u16(),
        };

        (status_code, Json(error_response)).into_response()
    }
}

#[derive(Deserialize)]
struct UserInput {
    name: String,
    email: String,
    age: u32,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/users", post(create_user_with_validation))
        .route("/test-error", post(test_error_handler));

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

// バリデーション付きユーザー作成
async fn create_user_with_validation(
    Json(input): Json<UserInput>,
) -> Result<(StatusCode, Json<serde_json::Value>), AppError> {
    // バリデーション
    if input.name.trim().is_empty() {
        return Err(AppError::ValidationError("名前は必須です".to_string()));
    }
    
    if !input.email.contains('@') {
        return Err(AppError::ValidationError(
            "有効なメールアドレスを入力してください".to_string(),
        ));
    }
    
    if input.age < 18 {
        return Err(AppError::ValidationError(
            "18歳以上である必要があります".to_string(),
        ));
    }

    // 成功レスポンス
    Ok((
        StatusCode::CREATED,
        Json(serde_json::json!({
            "message": "ユーザーが正常に作成されました",
            "user": {
                "name": input.name,
                "email": input.email,
                "age": input.age
            }
        })),
    ))
}

// エラーテスト用ハンドラー
async fn test_error_handler() -> Result<&'static str, AppError> {
    // 意図的にエラーを発生
    Err(AppError::DatabaseError(sqlx::Error::RowNotFound))
}

WebSocketとファイルサービング

use axum::{
    extract::{ws::{Message, WebSocket, WebSocketUpgrade}, Path},
    http::StatusCode,
    response::{Html, IntoResponse},
    routing::{get, post},
    Router,
};
use futures_util::{sink::SinkExt, stream::StreamExt};
use tower_http::services::ServeDir;
use tokio::fs;

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index_handler))
        .route("/ws", get(websocket_handler))
        .route("/upload", post(upload_handler))
        .nest_service("/static", ServeDir::new("static"));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("サーバー起動: http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}

// メインページ
async fn index_handler() -> Html<&'static str> {
    Html(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <title>Axum WebSocket Demo</title>
        <meta charset="utf-8">
    </head>
    <body>
        <h1>Axum WebSocketとファイルサービングデモ</h1>
        
        <h2>WebSocketテスト</h2>
        <div>
            <input type="text" id="messageInput" placeholder="メッセージを入力">
            <button onclick="sendMessage()">送信</button>
        </div>
        <div id="messages"></div>
        
        <h2>ファイルアップロード</h2>
        <form action="/upload" method="post" enctype="multipart/form-data">
            <input type="file" name="file" required>
            <button type="submit">アップロード</button>
        </form>
        
        <script>
            const ws = new WebSocket('ws://localhost:3000/ws');
            const messages = document.getElementById('messages');
            
            ws.onmessage = function(event) {
                const div = document.createElement('div');
                div.textContent = 'サーバー: ' + event.data;
                messages.appendChild(div);
            };
            
            function sendMessage() {
                const input = document.getElementById('messageInput');
                ws.send(input.value);
                
                const div = document.createElement('div');
                div.textContent = 'クライアント: ' + input.value;
                messages.appendChild(div);
                
                input.value = '';
            }
        </script>
    </body>
    </html>
    "#)
}

// WebSocketハンドラー
async fn websocket_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
    ws.on_upgrade(handle_socket)
}

async fn handle_socket(mut socket: WebSocket) {
    println!("WebSocket接続が確立されました");
    
    while let Some(msg) = socket.recv().await {
        match msg {
            Ok(Message::Text(text)) => {
                println!("受信したメッセージ: {}", text);
                let response = format!("エコー: {}", text);
                
                if socket.send(Message::Text(response)).await.is_err() {
                    println!("クライアントが切断されました");
                    break;
                }
            }
            Ok(Message::Close(_)) => {
                println!("WebSocket接続が閉じられました");
                break;
            }
            Err(e) => {
                println!("WebSocketエラー: {}", e);
                break;
            }
            _ => {}
        }
    }
}

// ファイルアップロードハンドラー
async fn upload_handler() -> impl IntoResponse {
    // 実際の実装では multipart 解析を行う
    // ここでは簡単な例として固定レスポンスを返す
    Html(r#"
    <h1>ファイルアップロード完了</h1>
    <p>ファイルが正常にアップロードされました。</p>
    <a href="/">戻る</a>
    "#)
}