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