Axum入門
RustのモダンなWebフレームワーク「Axum」を学びます。
Axumとは
Axumは、tokioチームが開発したWebフレームワークです。
特徴
- 型安全: コンパイル時に多くのエラーを検出
- 非同期: tokioベースで高性能
- モジュラー: 必要な機能だけ使える
- エコシステム: tower/hyperとの連携
セットアップ
Cargo.toml
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Hello World
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
// ルーターを作成
let app = Router::new()
.route("/", get(hello));
// サーバーを起動
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();
}
// ハンドラー関数
async fn hello() -> &'static str {
"Hello, Axum!"
}
実行:
cargo run
# 別のターミナルで
curl http://localhost:3000
# => Hello, Axum!
基本構造
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(handler1)) // GET /
.route("/users", get(handler2)) // GET /users
.route("/users", post(handler3)); // POST /users
// サーバー起動...
}
ルーティングのパターン
#![allow(unused)]
fn main() {
use axum::routing::{get, post, put, delete};
let app = Router::new()
// 基本的なルート
.route("/", get(root))
// CRUDパターン
.route("/users", get(list_users)) // GET /users
.route("/users", post(create_user)) // POST /users
.route("/users/:id", get(get_user)) // GET /users/123
.route("/users/:id", put(update_user)) // PUT /users/123
.route("/users/:id", delete(delete_user)); // DELETE /users/123
}
ハンドラー関数
基本形
#![allow(unused)]
fn main() {
async fn handler() -> impl IntoResponse {
"Hello, World!"
}
}
戻り値の種類
#![allow(unused)]
fn main() {
use axum::response::{Html, Json};
use axum::http::StatusCode;
// 文字列
async fn text() -> &'static str {
"Plain text"
}
// HTML
async fn html() -> Html<&'static str> {
Html("<h1>Hello</h1>")
}
// JSON
async fn json() -> Json<serde_json::Value> {
Json(serde_json::json!({"message": "Hello"}))
}
// ステータスコード付き
async fn with_status() -> (StatusCode, &'static str) {
(StatusCode::CREATED, "Created!")
}
// エラー
async fn not_found() -> StatusCode {
StatusCode::NOT_FOUND
}
}
JSONレスポンス
構造体をJSONで返す
#![allow(unused)]
fn main() {
use axum::Json;
use serde::Serialize;
#[derive(Serialize)]
struct User {
id: u32,
name: String,
}
async fn get_user() -> Json<User> {
let user = User {
id: 1,
name: "Taro".to_string(),
};
Json(user)
}
}
レスポンス例
{
"id": 1,
"name": "Taro"
}
パスパラメータ
URLの一部を変数として受け取ります。
#![allow(unused)]
fn main() {
use axum::extract::Path;
// /users/123 → id = 123
async fn get_user(Path(id): Path<u32>) -> String {
format!("User ID: {}", id)
}
// /users/123/posts/456 → 複数のパラメータ
async fn get_post(Path((user_id, post_id)): Path<(u32, u32)>) -> String {
format!("User: {}, Post: {}", user_id, post_id)
}
}
ルート定義
#![allow(unused)]
fn main() {
let app = Router::new()
.route("/users/:id", get(get_user))
.route("/users/:user_id/posts/:post_id", get(get_post));
}
クエリパラメータ
?key=value 形式のパラメータを受け取ります。
#![allow(unused)]
fn main() {
use axum::extract::Query;
use serde::Deserialize;
#[derive(Deserialize)]
struct Pagination {
page: Option<u32>,
limit: Option<u32>,
}
// /users?page=1&limit=10
async fn list_users(Query(params): Query<Pagination>) -> String {
let page = params.page.unwrap_or(1);
let limit = params.limit.unwrap_or(10);
format!("Page: {}, Limit: {}", page, limit)
}
}
JSONリクエストボディ
POSTやPUTでJSONデータを受け取ります。
#![allow(unused)]
fn main() {
use axum::Json;
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> String {
format!("Created user: {} ({})", payload.name, payload.email)
}
}
リクエスト例
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name": "Taro", "email": "taro@example.com"}'
完全な例: CRUD API
use axum::{
extract::{Path, Query, State},
http::StatusCode,
routing::{get, post, put, delete},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
// データモデル
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
#[derive(Deserialize)]
struct CreateUserRequest {
name: String,
email: String,
}
// アプリケーション状態(簡易的なインメモリDB)
type AppState = Arc<RwLock<Vec<User>>>;
#[tokio::main]
async fn main() {
let state: AppState = Arc::new(RwLock::new(vec![]));
let app = Router::new()
.route("/users", get(list_users))
.route("/users", post(create_user))
.route("/users/:id", get(get_user))
.route("/users/:id", delete(delete_user))
.with_state(state);
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();
}
// ユーザー一覧
async fn list_users(State(state): State<AppState>) -> Json<Vec<User>> {
let users = state.read().await;
Json(users.clone())
}
// ユーザー作成
async fn create_user(
State(state): State<AppState>,
Json(payload): Json<CreateUserRequest>,
) -> (StatusCode, Json<User>) {
let mut users = state.write().await;
let id = users.len() as u32 + 1;
let user = User {
id,
name: payload.name,
email: payload.email,
};
users.push(user.clone());
(StatusCode::CREATED, Json(user))
}
// ユーザー取得
async fn get_user(
State(state): State<AppState>,
Path(id): Path<u32>,
) -> Result<Json<User>, StatusCode> {
let users = state.read().await;
users
.iter()
.find(|u| u.id == id)
.cloned()
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
// ユーザー削除
async fn delete_user(
State(state): State<AppState>,
Path(id): Path<u32>,
) -> StatusCode {
let mut users = state.write().await;
let original_len = users.len();
users.retain(|u| u.id != id);
if users.len() < original_len {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
テスト
# ユーザー作成
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name": "Taro", "email": "taro@example.com"}'
# ユーザー一覧
curl http://localhost:3000/users
# 特定のユーザー取得
curl http://localhost:3000/users/1
# ユーザー削除
curl -X DELETE http://localhost:3000/users/1
まとめ
| 概念 | 説明 |
|---|---|
| Router | ルーティング定義 |
| Handler | リクエスト処理関数 |
| Path | URLパラメータ抽出 |
| Query | クエリパラメータ抽出 |
| Json | JSON送受信 |
| State | アプリケーション状態 |
| Extractor | 用途 |
|---|---|
Path<T> | /users/:id からIDを取得 |
Query<T> | ?page=1 からパラメータ取得 |
Json<T> | リクエストボディをパース |
State<T> | 共有状態にアクセス |
確認テスト
Q1. Axumでパスパラメータ /users/123 からIDを取得するのに使うExtractorは?
正解: B) Path<T> はURLのパスからパラメータを抽出します。Query<T> はクエリパラメータ(?key=value)、Json<T> はリクエストボディ、State<T> は共有状態です。
Q2. Axumで POST /users にJSONデータを受け取る場合、正しいハンドラーの引数の型は?
正解: B) POSTでJSONデータを受け取るには Json<T> Extractorを使います。PathはURL、QueryはURLのクエリパラメータ、Stateは共有状態です。
Q3. Query(p): Query<Params>(page: u32, limit: u32)で /users?page=2&limit=5 にアクセスしたとき、format!("{}-{}", p.page, p.limit) の出力は?
正解: C) Query<T> はURLのクエリパラメータ ?page=2&limit=5 を構造体にデシリアライズします。page=2, limit=5 なので出力は "2-5" です。
Q4. 以下のコードのエラー原因は? async fn hello() -> String { "Hello" } fn main() { let app = Router::new().route("/", get(hello)); }
正解: A) 2つのエラーがあります。mainが非同期でない(#[tokio::main] async fnが必要)点と、hello()の戻り値がStringなのに&strを返している点です。
Q5. GET /hello/:name で "Hello, {name}!" を返すハンドラーの正しい実装は?
正解: D) URLパスのパラメータ(:name)を取得するには Path<T> Extractorを使います。QueryはURLクエリパラメータ、Jsonはリクエストボディ用です。
次のドキュメント: 03_routing_handlers.md