Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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リクエスト処理関数
PathURLパラメータ抽出
Queryクエリパラメータ抽出
JsonJSON送受信
Stateアプリケーション状態
Extractor用途
Path<T>/users/:id からIDを取得
Query<T>?page=1 からパラメータ取得
Json<T>リクエストボディをパース
State<T>共有状態にアクセス

確認テスト

Q1. Axumでパスパラメータ /users/123 からIDを取得するのに使うExtractorは?

Q2. Axumで POST /users にJSONデータを受け取る場合、正しいハンドラーの引数の型は?

Q3. Query(p): Query<Params>(page: u32, limit: u32)で /users?page=2&limit=5 にアクセスしたとき、format!("{}-{}", p.page, p.limit) の出力は?

Q4. 以下のコードのエラー原因は? async fn hello() -> String { "Hello" } fn main() { let app = Router::new().route("/", get(hello)); }

Q5. GET /hello/:name で "Hello, {name}!" を返すハンドラーの正しい実装は?


次のドキュメント: 03_routing_handlers.md