Axum
Tokioエコシステムと完全統合されたモダンなRust Webフレームワーク。エルゴノミクスに優れ、Actix-webに匹敵する性能を実現。
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
トピックス
なし
スター履歴
データ取得日時: 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>
"#)
}