Rust Axum Webフレームワーク入門
Axumは、tokioエコシステム上に構築されたRustのWebフレームワークです。型安全性、パフォーマンス、開発者体験を重視した設計で、モダンなWeb APIの構築に最適です。本記事では、Axumの基礎から実践的な使い方まで詳しく解説します。
Axumとは
AxumはTokioチームが開発したWebアプリケーションフレームワークで、以下の特徴があります。
主な特徴
1. Tokioベース
- 高性能な非同期ランタイム
- スケーラブルな並行処理
2. 型安全なエクストラクタ
- コンパイル時のエラー検出
- ボイラープレートの削減
3. Towerとの統合
- ミドルウェアの豊富なエコシステム
- 柔軟な構成
4. 最小限の依存関係
- 必要な機能だけを選択
- 高速なコンパイル
セットアップ
プロジェクト作成
cargo new axum-api
cd axum-api
依存関係の追加
Cargo.toml:
[package]
name = "axum-api"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
Hello World
src/main.rs:
use axum::{
routing::get,
Router,
};
#[tokio::main]
async fn main() {
// ルーティング設定
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }));
// サーバー起動
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
cargo run
# http://localhost:3000 にアクセス
ルーティング
基本的なルート
use axum::{
routing::{get, post, put, delete},
Router,
};
async fn handler() -> &'static str {
"Hello!"
}
let app = Router::new()
.route("/", get(handler))
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user).put(update_user).delete(delete_user));
パスパラメータ
use axum::{
extract::Path,
response::IntoResponse,
};
// 単一パラメータ
async fn get_user(Path(id): Path<u32>) -> String {
format!("User ID: {}", id)
}
// 複数パラメータ
async fn get_post(
Path((user_id, post_id)): Path<(u32, u32)>
) -> String {
format!("User: {}, Post: {}", user_id, post_id)
}
let app = Router::new()
.route("/users/:id", get(get_user))
.route("/users/:user_id/posts/:post_id", get(get_post));
クエリパラメータ
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(10);
format!("Page: {}, Per page: {}", page, per_page)
}
let app = Router::new()
.route("/users", get(list_users));
// GET /users?page=2&per_page=20
リクエスト処理
JSON処理
use axum::{
extract::Json,
http::StatusCode,
};
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>
) -> (StatusCode, Json<User>) {
let user = User {
id: 1,
username: payload.username,
email: payload.email,
};
(StatusCode::CREATED, Json(user))
}
フォームデータ
use axum::extract::Form;
#[derive(Deserialize)]
struct LoginForm {
username: String,
password: String,
}
async fn login(Form(form): Form<LoginForm>) -> String {
format!("Logging in user: {}", form.username)
}
let app = Router::new()
.route("/login", post(login));
リクエストボディ
use axum::body::Bytes;
async fn handle_raw_body(body: Bytes) -> String {
format!("Received {} bytes", body.len())
}
レスポンス
様々なレスポンス型
use axum::{
http::{StatusCode, header},
response::{IntoResponse, Response, Html},
};
// テキストレスポンス
async fn text() -> &'static str {
"Plain text"
}
// HTMLレスポンス
async fn html() -> Html<&'static str> {
Html("<h1>Hello, HTML!</h1>")
}
// JSONレスポンス
async fn json() -> Json<User> {
Json(User {
id: 1,
username: "alice".to_string(),
email: "alice@example.com".to_string(),
})
}
// カスタムステータスコード
async fn not_found() -> (StatusCode, &'static str) {
(StatusCode::NOT_FOUND, "Not found")
}
// カスタムヘッダー
async fn with_header() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "text/plain")],
"Hello with header"
)
}
カスタムレスポンス
use axum::{
response::{IntoResponse, Response},
http::StatusCode,
};
use serde::Serialize;
#[derive(Serialize)]
struct ApiResponse<T> {
success: bool,
data: Option<T>,
error: Option<String>,
}
impl<T: Serialize> IntoResponse for ApiResponse<T> {
fn into_response(self) -> Response {
let status = if self.success {
StatusCode::OK
} else {
StatusCode::BAD_REQUEST
};
(status, Json(self)).into_response()
}
}
async fn handler() -> ApiResponse<User> {
ApiResponse {
success: true,
data: Some(User {
id: 1,
username: "alice".to_string(),
email: "alice@example.com".to_string(),
}),
error: None,
}
}
状態管理
アプリケーション状態
use axum::{
extract::State,
routing::get,
Router,
};
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
struct AppState {
db: Arc<RwLock<Vec<User>>>,
}
async fn list_users(State(state): State<AppState>) -> Json<Vec<User>> {
let users = state.db.read().await;
Json(users.clone())
}
async fn create_user(
State(state): State<AppState>,
Json(payload): Json<CreateUser>,
) -> (StatusCode, Json<User>) {
let user = User {
id: 1,
username: payload.username,
email: payload.email,
};
state.db.write().await.push(user.clone());
(StatusCode::CREATED, Json(user))
}
#[tokio::main]
async fn main() {
let state = AppState {
db: Arc::new(RwLock::new(vec![])),
};
let app = Router::new()
.route("/users", get(list_users).post(create_user))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
ミドルウェア
Tower HTTPミドルウェア
use tower_http::{
cors::CorsLayer,
trace::TraceLayer,
};
let app = Router::new()
.route("/", get(handler))
.layer(CorsLayer::permissive())
.layer(TraceLayer::new_for_http());
カスタムミドルウェア
use axum::{
middleware::{self, Next},
http::Request,
response::Response,
};
async fn auth_middleware<B>(
req: Request<B>,
next: Next<B>,
) -> Result<Response, StatusCode> {
let auth_header = req.headers()
.get("authorization")
.and_then(|v| v.to_str().ok());
match auth_header {
Some(token) if token.starts_with("Bearer ") => {
Ok(next.run(req).await)
}
_ => Err(StatusCode::UNAUTHORIZED)
}
}
let app = Router::new()
.route("/protected", get(protected_handler))
.layer(middleware::from_fn(auth_middleware));
エラーハンドリング
カスタムエラー型
use axum::{
response::{IntoResponse, Response},
http::StatusCode,
};
enum ApiError {
NotFound,
BadRequest(String),
InternalError,
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, message) = match self {
ApiError::NotFound => (StatusCode::NOT_FOUND, "Not found"),
ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.as_str()),
ApiError::InternalError => (
StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error"
),
};
(status, message).into_response()
}
}
async fn handler() -> Result<Json<User>, ApiError> {
// エラーケース
Err(ApiError::NotFound)
// 成功ケース
// Ok(Json(user))
}
データベース統合
SQLxとの統合
[dependencies]
sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "postgres"] }
use sqlx::{PgPool, FromRow};
#[derive(FromRow, Serialize)]
struct User {
id: i32,
username: String,
email: String,
}
#[derive(Clone)]
struct AppState {
db: PgPool,
}
async fn list_users(
State(state): State<AppState>
) -> Result<Json<Vec<User>>, ApiError> {
let users = sqlx::query_as::<_, User>("SELECT * FROM users")
.fetch_all(&state.db)
.await
.map_err(|_| ApiError::InternalError)?;
Ok(Json(users))
}
async fn create_user(
State(state): State<AppState>,
Json(payload): Json<CreateUser>,
) -> Result<(StatusCode, Json<User>), ApiError> {
let user = sqlx::query_as::<_, User>(
"INSERT INTO users (username, email) VALUES ($1, $2) RETURNING *"
)
.bind(&payload.username)
.bind(&payload.email)
.fetch_one(&state.db)
.await
.map_err(|_| ApiError::InternalError)?;
Ok((StatusCode::CREATED, Json(user)))
}
#[tokio::main]
async fn main() {
let db = PgPool::connect("postgres://user:pass@localhost/db")
.await
.unwrap();
let state = AppState { db };
let app = Router::new()
.route("/users", get(list_users).post(create_user))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
テスト
#[cfg(test)]
mod tests {
use super::*;
use axum::{
body::Body,
http::{Request, StatusCode},
};
use tower::ServiceExt;
#[tokio::test]
async fn test_hello() {
let app = Router::new()
.route("/", get(|| async { "Hello!" }));
let response = app
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_create_user() {
let state = AppState {
db: Arc::new(RwLock::new(vec![])),
};
let app = Router::new()
.route("/users", post(create_user))
.with_state(state);
let payload = serde_json::json!({
"username": "alice",
"email": "alice@example.com"
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/users")
.header("content-type", "application/json")
.body(Body::from(payload.to_string()))
.unwrap()
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
}
}
まとめ
Axumは、Rustの強力な型システムと非同期処理を活かした優れたWebフレームワークです。
主な利点
- 型安全性: コンパイル時のエラー検出
- 高性能: Tokioベースの非同期処理
- 柔軟性: Towerミドルウェアとの統合
- 開発体験: エクストラクタによる簡潔なコード
高性能で保守性の高いWeb APIを構築したい場合、Axumは優れた選択肢となるでしょう。