はじめに
Rust で Web サーバーを作るなら、axum が最高です。
- tokio チームが開発
- tower エコシステムと統合
- 型安全でエルゴノミック
- マクロなしでルーティング
この記事では axum の魅力を紹介します。
目次
axum とは
tokio チームが開発した Web フレームワーク。
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
特徴:
- マクロ不要 - 普通の関数がハンドラになる
- 型安全 - コンパイル時にエラーを検出
- tower 互換 - 豊富なミドルウェア
- 高速 - tokio ベースの非同期処理
Hello World
use axum::{Router, routing::get};
#[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
curl http://localhost:3000
# Hello, World!
これだけ! シンプル。
ルーティング
基本
use axum::{Router, routing::{get, post, put, delete}};
async fn get_users() -> &'static str { "GET /users" }
async fn create_user() -> &'static str { "POST /users" }
async fn get_user() -> &'static str { "GET /users/:id" }
async fn update_user() -> &'static str { "PUT /users/:id" }
async fn delete_user() -> &'static str { "DELETE /users/:id" }
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users", get(get_users).post(create_user))
.route("/users/:id", get(get_user).put(update_user).delete(delete_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
ネスト
fn user_routes() -> Router {
Router::new()
.route("/", get(list_users).post(create_user))
.route("/:id", get(get_user).put(update_user).delete(delete_user))
}
fn post_routes() -> Router {
Router::new()
.route("/", get(list_posts).post(create_post))
.route("/:id", get(get_post))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.nest("/users", user_routes())
.nest("/posts", post_routes());
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
リクエストの処理
パスパラメータ
use axum::{Router, routing::get, extract::Path};
async fn get_user(Path(id): Path<u64>) -> String {
format!("User ID: {}", id)
}
async fn get_post(Path((user_id, post_id)): Path<(u64, u64)>) -> String {
format!("User: {}, Post: {}", user_id, post_id)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users/:id", get(get_user))
.route("/users/:user_id/posts/:post_id", get(get_post));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
curl http://localhost:3000/users/42
# User ID: 42
curl http://localhost:3000/users/1/posts/100
# User: 1, Post: 100
クエリパラメータ
use axum::{Router, routing::get, extract::Query};
use serde::Deserialize;
#[derive(Deserialize)]
struct Pagination {
page: Option<u32>,
per_page: Option<u32>,
}
async fn list_users(Query(params): Query<Pagination>) -> String {
let page = params.page.unwrap_or(1);
let per_page = params.per_page.unwrap_or(10);
format!("Page: {}, Per page: {}", page, per_page)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users", get(list_users));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
curl "http://localhost:3000/users?page=2&per_page=20"
# Page: 2, Per page: 20
JSON ボディ
use axum::{Router, routing::post, Json};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Serialize)]
struct User {
id: u64,
name: String,
email: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
let user = User {
id: 1,
name: payload.name,
email: payload.email,
};
Json(user)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users", post(create_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com"}'
# {"id":1,"name":"Alice","email":"alice@example.com"}
ヘッダー
use axum::{Router, routing::get, http::HeaderMap};
async fn show_headers(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)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/headers", get(show_headers));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
レスポンスの返却
ステータスコード
use axum::{Router, routing::get, http::StatusCode};
async fn not_found() -> StatusCode {
StatusCode::NOT_FOUND
}
async fn created() -> (StatusCode, &'static str) {
(StatusCode::CREATED, "Created!")
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/404", get(not_found))
.route("/201", get(created));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
ヘッダー付きレスポンス
use axum::{
Router, routing::get,
http::{StatusCode, header},
response::IntoResponse,
};
async fn with_headers() -> impl IntoResponse {
(
StatusCode::OK,
[(header::CONTENT_TYPE, "text/plain")],
"Hello with headers!",
)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(with_headers));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
HTML
use axum::{Router, routing::get, response::Html};
async fn index() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(index));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
リダイレクト
use axum::{Router, routing::get, response::Redirect};
async fn redirect() -> Redirect {
Redirect::to("/new-location")
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/old", get(redirect))
.route("/new-location", get(|| async { "You were redirected!" }));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
エラーハンドリング
use axum::{
Router, routing::get,
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
// カスタムエラー型
enum AppError {
NotFound,
InternalError(String),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::NotFound => (StatusCode::NOT_FOUND, "Not found"),
AppError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.leak()),
};
let body = Json(json!({ "error": message }));
(status, body).into_response()
}
}
async fn get_user(axum::extract::Path(id): axum::extract::Path<u64>) -> Result<Json<serde_json::Value>, AppError> {
if id == 0 {
return Err(AppError::NotFound);
}
Ok(Json(json!({ "id": id, "name": "Alice" })))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users/:id", get(get_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
ミドルウェア
ロギング
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.5", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = "0.3"
use axum::{Router, routing::get};
use tower_http::trace::TraceLayer;
use tracing_subscriber;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/", get(|| async { "Hello!" }))
.layer(TraceLayer::new_for_http());
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
CORS
[dependencies]
tower-http = { version = "0.5", features = ["cors"] }
use axum::{Router, routing::get};
use tower_http::cors::{CorsLayer, Any};
#[tokio::main]
async fn main() {
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
let app = Router::new()
.route("/", get(|| async { "Hello!" }))
.layer(cors);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
状態の共有
use axum::{Router, routing::get, extract::State};
use std::sync::Arc;
use tokio::sync::RwLock;
struct AppState {
counter: RwLock<u64>,
}
async fn increment(State(state): State<Arc<AppState>>) -> String {
let mut counter = state.counter.write().await;
*counter += 1;
format!("Counter: {}", *counter)
}
async fn get_count(State(state): State<Arc<AppState>>) -> String {
let counter = state.counter.read().await;
format!("Counter: {}", *counter)
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState {
counter: RwLock::new(0),
});
let app = Router::new()
.route("/increment", get(increment))
.route("/count", get(get_count))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
実践:REST API
完全な CRUD API の例:
use axum::{
Router,
routing::{get, post},
extract::{Path, State, Json},
http::StatusCode,
response::IntoResponse,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
#[derive(Clone, Serialize)]
struct Todo {
id: u64,
title: String,
completed: bool,
}
#[derive(Deserialize)]
struct CreateTodo {
title: String,
}
#[derive(Deserialize)]
struct UpdateTodo {
title: Option<String>,
completed: Option<bool>,
}
type Db = Arc<RwLock<HashMap<u64, Todo>>>;
async fn list_todos(State(db): State<Db>) -> Json<Vec<Todo>> {
let todos = db.read().await;
Json(todos.values().cloned().collect())
}
async fn create_todo(
State(db): State<Db>,
Json(input): Json<CreateTodo>,
) -> impl IntoResponse {
let mut todos = db.write().await;
let id = todos.len() as u64 + 1;
let todo = Todo {
id,
title: input.title,
completed: false,
};
todos.insert(id, todo.clone());
(StatusCode::CREATED, Json(todo))
}
async fn get_todo(
State(db): State<Db>,
Path(id): Path<u64>,
) -> Result<Json<Todo>, StatusCode> {
let todos = db.read().await;
todos.get(&id).cloned().map(Json).ok_or(StatusCode::NOT_FOUND)
}
async fn update_todo(
State(db): State<Db>,
Path(id): Path<u64>,
Json(input): Json<UpdateTodo>,
) -> Result<Json<Todo>, StatusCode> {
let mut todos = db.write().await;
let todo = todos.get_mut(&id).ok_or(StatusCode::NOT_FOUND)?;
if let Some(title) = input.title {
todo.title = title;
}
if let Some(completed) = input.completed {
todo.completed = completed;
}
Ok(Json(todo.clone()))
}
async fn delete_todo(
State(db): State<Db>,
Path(id): Path<u64>,
) -> StatusCode {
let mut todos = db.write().await;
if todos.remove(&id).is_some() {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
#[tokio::main]
async fn main() {
let db: Db = Arc::new(RwLock::new(HashMap::new()));
let app = Router::new()
.route("/todos", get(list_todos).post(create_todo))
.route("/todos/:id", get(get_todo).put(update_todo).delete(delete_todo))
.with_state(db);
println!("Server running on http://localhost:3000");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
# 作成
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"title":"Learn axum"}'
# 一覧
curl http://localhost:3000/todos
# 取得
curl http://localhost:3000/todos/1
# 更新
curl -X PUT http://localhost:3000/todos/1 \
-H "Content-Type: application/json" \
-d '{"completed":true}'
# 削除
curl -X DELETE http://localhost:3000/todos/1
まとめ
axum vs 他のフレームワーク
| 特徴 | axum | actix-web | rocket |
|---|---|---|---|
| マクロ | 不要 | 必要 | 必要 |
| tower 互換 | ✅ | ❌ | ❌ |
| 学習曲線 | 緩やか | 普通 | 急 |
| 型安全性 | 高い | 高い | 高い |
| パフォーマンス | 高い | 高い | 普通 |
axum の魅力
- マクロ不要 - 関数がそのままハンドラ
- 型安全 - コンパイル時にエラー検出
- tower エコシステム - 豊富なミドルウェア
- tokio チーム製 - 安心感
- シンプル - 学習コストが低い
チェックリスト
-
Router::new()でルーター作成 -
Path,Query,Jsonでパラメータ抽出 -
Stateで状態共有 -
IntoResponseでカスタムレスポンス -
tower-httpでミドルウェア追加
axum、本当に神です。Rust で Web サーバー作るなら、まず axum を試してみてください!
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!