ルーティングとハンドラー
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"
}
}
順序の注意点
JsonやRequestなど、リクエストボディを消費する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は?
正解: B) Query<T> はURLのクエリパラメータ(?の後ろ)を取得します。Path<T> はURLパス内のパラメータ(/users/:idなど)を取得します。
Q2. ハンドラーで複数のExtractorを使う場合、Jsonの配置について正しいのは?
正解: C) Jsonはリクエストボディを消費するため、他のExtractorより後に配置する必要があります。順番が間違っているとコンパイルエラーになります。
Q3. Path<Params>(user_id: u32, post_id: u32)で GET /users/123/posts/456 にアクセスしたとき、format!("U:{},P:{}", p.user_id, p.post_id) の出力は?
正解: A) Path<T> は構造体でパスパラメータを受け取れます。:user_id と :post_id がそれぞれ構造体のフィールドにマッピングされます。
Q4. 以下のコードのエラー原因は? async fn handler(Json(body): Json<Data>, Path(id): Path<u32>, State(state): State<AppState>)
正解: B) Jsonはリクエストボディを消費するため、最後に配置する必要があります。正しい順序は: Path, State, Json です。
Q5. GET /items/:id でIDが0以下なら400 Bad Request、見つからなければ404 Not Foundを返す場合、戻り値の型として適切なのは?
正解: C) 成功時はJson<Item>、エラー時はStatusCodeを返すため、Result型を使います。これにより、異なるHTTPステータスコードを返せます。
次のドキュメント: 04_database_sqlx.md