跳轉到

3.2 路由與 Handler:從 HTTP 語意到型別安全的提取器#

HTTP 語意:不只是字串#

REST 的本質是把 HTTP 動詞對應到領域操作,其約定來自 Fielding 的博士論文(2000):

動詞 語意 冪等 安全
GET 讀取 是 是
POST 建立/觸發 否 否
PUT 整體替換 是 否
PATCH 部分更新 否 否
DELETE 刪除 是 否

冪等性(idempotency)在分散式系統中是重試安全的前提:網路不可靠,客戶端必然重試;只有冪等的操作可以安全重試。這是為什麼金流 API 的「建立訂單」常用 PUT /orders/{id} 或帶 idempotency key 的 POST。

提取器:以函數簽名宣告依賴#

Axum 的核心創見:handler 的參數列表就是請求的解析規格,由型別系統驅動:

use axum::{extract::{Path, Query, State}, Json};

#[derive(Deserialize)]
struct ListQuery { page: Option<u32>, q: Option<String> }

async fn list_users(
    State(pool): State<PgPool>,        // 架構狀態
    Query(q): Query<ListQuery>,        // ?page=1&q=abc
) -> Result<Json<Vec<User>>, AppError> {
    Ok(Json(repo::list(&pool, q.page.unwrap_or(1), q.q.as_deref()).await?))
}

async fn get_user(
    State(pool): State<PgPool>,
    Path(id): Path<Uuid>,              // /users/:id → Uuid 型別解析
) -> Result<Json<User>, AppError> {
    let user = repo::find(&pool, id).await?
        .ok_or(AppError::NotFound)?;   // Option → 錯誤的顯式轉換
    Ok(Json(user))
}

當 Path<Uuid> 解析失敗(URL 帶了不是 UUID 的字串),Axum 自動回 400,你的 handler 根本不會被呼叫 —— 輸入驗證被推進型別層。

提取器的順序規則#

實作 FromRequestParts 的提取器(Path、Query、State…)必須在前;消耗整個 body 的(Json<T>)只能最後且至多一個 —— 因為 body 串流只能讀一次。這條規則由型別系統在編譯期強制。

完整的 CRUD 路由#

let app = Router::new()
    .route("/users", get(list_users).post(create_user))
    .route("/users/:id", get(get_user).put(update_user).delete(delete_user))
    .route_layer(middleware::from_fn(auth::require_login))  // 整組受保護
    .with_state(pool.clone());

資料驗證:型別之後、資料庫之前的第三道防線#

型別系統保證「形狀正確」,但業務規則(email 格式、年齡範圍)需要驗證層。使用 validator crate:

#[derive(Deserialize, Validate)]
struct CreateUser {
    #[validate(email)]
    email: String,
    #[validate(length(min = 2, max = 50))]
    name: String,
}

async fn create_user(Json(body): Json<CreateUser>) -> Result<Json<User>, AppError> {
    body.validate()?;   // 驗證失敗 → 422
    // ...
}

三道防線的完整模型:

\[ \text{請求} \xrightarrow{\text{提取器:形狀}} \xrightarrow{\text{驗證:語意}} \xrightarrow{\text{資料庫:約束}} \text{寫入} \]

防禦縱深(defense in depth)原則:每一層獨立設防,任何一層失守都不直接導致資料污染。

流式回應與 SSE#

對於即時推送(聊天、通知),Axum 支援 Server-Sent Events:

use axum::response::sse::{Sse, Event};
use futures::stream::{self, Stream};

async fn events() -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
    Sse::new(stream::repeat_with(|| Ok(Event::default().data("tick")))
        .map(|e| e))
}

SSE vs WebSocket 的取捨:單向推送用 SSE(簡單、走 HTTP、自動重連),雙向即時才用 WebSocket。

習題#

  1. 為什麼「轉帳」API 不能只靠 HTTP 冪等語意保證?設計一個 idempotency key 機制。
  2. 寫一個 GET /posts?author=x&tag=y&page=2 的 handler,含查詢參數驗證。
  3. 解釋為什麼兩個 body 提取器在同一 handler 中是編譯錯誤。