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のルーティングとハンドラーの詳細を学びます。

ルーティングの基本

単一ルート

#![allow(unused)]
fn main() {
use axum::{routing::get, Router};

let app = Router::new()
    .route("/", get(handler));
}

複数のHTTPメソッド

#![allow(unused)]
fn main() {
use axum::routing::{get, post, put, delete};

let app = Router::new()
    .route("/users", get(list_users).post(create_user))
    .route("/users/:id", get(get_user).put(update_user).delete(delete_user));
}

method_router

#![allow(unused)]
fn main() {
use axum::routing::MethodRouter;

// 同じパスに複数のメソッドを設定
let user_routes = get(list_users)
    .post(create_user);

let app = Router::new()
    .route("/users", user_routes);
}

パスパターン

静的パス

#![allow(unused)]
fn main() {
.route("/users", get(handler))      // /users
.route("/api/v1/users", get(handler)) // /api/v1/users
}

動的パス(パラメータ)

#![allow(unused)]
fn main() {
.route("/users/:id", get(handler))           // /users/123
.route("/users/:id/posts/:post_id", get(handler)) // /users/123/posts/456
}

ワイルドカード

#![allow(unused)]
fn main() {
.route("/files/*path", get(handler))  // /files/a/b/c → path = "a/b/c"
}

Extractor(データ抽出)

Path: パスパラメータ

#![allow(unused)]
fn main() {
use axum::extract::Path;

// 単一パラメータ
async fn get_user(Path(id): Path<u32>) -> String {
    format!("User: {}", id)
}

// 複数パラメータ
async fn get_post(Path((user_id, post_id)): Path<(u32, u32)>) -> String {
    format!("User: {}, Post: {}", user_id, post_id)
}

// 構造体で受け取る
#[derive(Deserialize)]
struct PathParams {
    user_id: u32,
    post_id: u32,
}

async fn get_post_struct(Path(params): Path<PathParams>) -> String {
    format!("User: {}, Post: {}", params.user_id, params.post_id)
}
}

Query: クエリパラメータ

#![allow(unused)]
fn main() {
use axum::extract::Query;
use serde::Deserialize;

#[derive(Deserialize)]
struct ListParams {
    page: Option<u32>,
    limit: Option<u32>,
    search: Option<String>,
}

// /users?page=1&limit=10&search=foo
async fn list_users(Query(params): Query<ListParams>) -> String {
    let page = params.page.unwrap_or(1);
    let limit = params.limit.unwrap_or(10);
    format!("Page: {}, Limit: {}, Search: {:?}", page, limit, params.search)
}
}

Json: リクエストボディ

#![allow(unused)]
fn main() {
use axum::Json;
use serde::Deserialize;

#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
    #[serde(default)]
    age: Option<u32>,
}

async fn create_user(Json(payload): Json<CreateUser>) -> String {
    format!("Creating: {} ({})", payload.name, payload.email)
}
}

Header: ヘッダー

#![allow(unused)]
fn main() {
use axum::http::header::HeaderMap;
use axum::extract::TypedHeader;
use axum::headers::Authorization;
use axum::headers::authorization::Bearer;

// 全ヘッダー取得
async fn all_headers(headers: HeaderMap) -> String {
    format!("{:?}", headers)
}

// 特定のヘッダー
async fn auth_header(
    TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> String {
    format!("Token: {}", auth.token())
}
}

State: 共有状態

#![allow(unused)]
fn main() {
use axum::extract::State;
use std::sync::Arc;

struct AppState {
    db_pool: Pool,
    config: Config,
}

async fn handler(State(state): State<Arc<AppState>>) -> String {
    // state.db_pool や state.config にアクセス
    "OK".to_string()
}

// メインでStateを設定
let state = Arc::new(AppState { ... });
let app = Router::new()
    .route("/", get(handler))
    .with_state(state);
}

複数のExtractor

Extractorは複数組み合わせられます。

#![allow(unused)]
fn main() {
async fn handler(
    State(state): State<AppState>,
    Path(id): Path<u32>,
    Query(params): Query<ListParams>,
    Json(body): Json<CreateUser>,
) -> impl IntoResponse {
    // すべてのデータにアクセス可能
    "OK"
}
}

順序の注意点

JsonRequestなど、リクエストボディを消費するExtractorは最後に配置します。

#![allow(unused)]
fn main() {
// ✅ 正しい: Jsonは最後
async fn handler(
    State(state): State<AppState>,
    Path(id): Path<u32>,
    Json(body): Json<Data>,  // 最後
) { }

// ❌ 間違い: Jsonの後に他のExtractor
async fn handler(
    Json(body): Json<Data>,
    Path(id): Path<u32>,  // エラー
) { }
}

レスポンス

基本的なレスポンス型

#![allow(unused)]
fn main() {
use axum::response::{Html, Json, IntoResponse};
use axum::http::StatusCode;

// 文字列
async fn text() -> &'static str {
    "Hello"
}

// HTML
async fn html() -> Html<String> {
    Html("<h1>Hello</h1>".to_string())
}

// JSON
async fn json() -> Json<serde_json::Value> {
    Json(serde_json::json!({"status": "ok"}))
}

// ステータスコードのみ
async fn no_content() -> StatusCode {
    StatusCode::NO_CONTENT
}

// ステータスコード + ボディ
async fn created() -> (StatusCode, Json<User>) {
    (StatusCode::CREATED, Json(user))
}
}

カスタムレスポンス

#![allow(unused)]
fn main() {
use axum::response::{Response, IntoResponse};
use axum::http::{StatusCode, header};

async fn custom_response() -> impl IntoResponse {
    (
        StatusCode::OK,
        [(header::CONTENT_TYPE, "text/plain")],
        "Hello with custom headers"
    )
}
}

Result型でエラー処理

#![allow(unused)]
fn main() {
use axum::response::IntoResponse;
use axum::http::StatusCode;

async fn get_user(Path(id): Path<u32>) -> Result<Json<User>, StatusCode> {
    let user = find_user(id)
        .ok_or(StatusCode::NOT_FOUND)?;

    Ok(Json(user))
}
}

エラーハンドリング

カスタムエラー型

#![allow(unused)]
fn main() {
use axum::response::{IntoResponse, Response};
use axum::http::StatusCode;
use axum::Json;

// カスタムエラー型
enum AppError {
    NotFound,
    BadRequest(String),
    Internal(String),
}

// IntoResponseを実装
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            AppError::NotFound => (StatusCode::NOT_FOUND, "Not found"),
            AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.as_str()),
            AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.as_str()),
        };

        let body = Json(serde_json::json!({
            "error": message
        }));

        (status, body).into_response()
    }
}

// ハンドラーで使用
async fn get_user(Path(id): Path<u32>) -> Result<Json<User>, AppError> {
    let user = find_user(id)
        .ok_or(AppError::NotFound)?;

    Ok(Json(user))
}
}

anyhowとの連携

#![allow(unused)]
fn main() {
use anyhow::Result;
use axum::response::IntoResponse;

// anyhowエラーをラップ
struct AppError(anyhow::Error);

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("Error: {}", self.0)
        ).into_response()
    }
}

impl<E> From<E> for AppError
where
    E: Into<anyhow::Error>,
{
    fn from(err: E) -> Self {
        Self(err.into())
    }
}

async fn handler() -> Result<Json<Data>, AppError> {
    let data = fetch_data()?;  // ?でエラーを伝播
    Ok(Json(data))
}
}

ルーターのネスト

nest: サブルーターを結合

#![allow(unused)]
fn main() {
// ユーザー関連のルート
fn user_routes() -> Router {
    Router::new()
        .route("/", get(list_users).post(create_user))
        .route("/:id", get(get_user).delete(delete_user))
}

// 投稿関連のルート
fn post_routes() -> Router {
    Router::new()
        .route("/", get(list_posts).post(create_post))
        .route("/:id", get(get_post))
}

// メインルーター
let app = Router::new()
    .nest("/users", user_routes())    // /users, /users/:id
    .nest("/posts", post_routes());   // /posts, /posts/:id
}

merge: ルーターを統合

#![allow(unused)]
fn main() {
let api_routes = Router::new()
    .route("/users", get(list_users))
    .route("/posts", get(list_posts));

let web_routes = Router::new()
    .route("/", get(home))
    .route("/about", get(about));

let app = api_routes.merge(web_routes);
}

ミドルウェア

基本的なミドルウェア

#![allow(unused)]
fn main() {
use axum::middleware::{self, Next};
use axum::http::Request;
use axum::response::Response;

async fn logging_middleware<B>(
    request: Request<B>,
    next: Next<B>,
) -> Response {
    println!("Request: {} {}", request.method(), request.uri());

    let response = next.run(request).await;

    println!("Response: {}", response.status());
    response
}

let app = Router::new()
    .route("/", get(handler))
    .layer(middleware::from_fn(logging_middleware));
}

tower層の使用

#![allow(unused)]
fn main() {
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;

let app = Router::new()
    .route("/", get(handler))
    .layer(CorsLayer::permissive())
    .layer(TraceLayer::new_for_http());
}

まとめ

Extractor用途
Path<T>URLパラメータ
Query<T>クエリパラメータ
Json<T>JSONボディ
State<T>共有状態
HeaderMap全ヘッダー
Response用途
&str / Stringテキスト
Html<T>HTML
Json<T>JSON
StatusCodeステータスのみ
(StatusCode, T)ステータス + ボディ

確認テスト

Q1. Axumで /users?page=2&search=foo からパラメータを取得するExtractorは?

Q2. ハンドラーで複数のExtractorを使う場合、Jsonの配置について正しいのは?

Q3. Path<Params>(user_id: u32, post_id: u32)で GET /users/123/posts/456 にアクセスしたとき、format!("U:{},P:{}", p.user_id, p.post_id) の出力は?

Q4. 以下のコードのエラー原因は? async fn handler(Json(body): Json<Data>, Path(id): Path<u32>, State(state): State<AppState>)

Q5. GET /items/:id でIDが0以下なら400 Bad Request、見つからなければ404 Not Foundを返す場合、戻り値の型として適切なのは?


次のドキュメント: 04_database_sqlx.md