Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Biết 5 method router cốt lõi REST:
get,post,put,delete,patch; và 3 method bổ sunghead,options,trace. - Hiểu
MethodRoutertrait: kết quả khi gọiget(handler)/post(handler)/... và là service kết hợp method matching + handler dispatch. - Áp dụng multi-method cùng path: pattern chain
get(h1).post(h2).delete(h3)trong cùng một.route()call. - Biết
any()matcher catch-all method và lý do Shop API KHÔNG dùng cho data endpoint. - Tránh route ordering pitfall: đặt specific path trước parameter/wildcard.
- Refactor shop-api thêm Product CRUD route skeleton ở
routes/products.rs, chuẩn bị G7 implement đầy đủ với DB. - Biết axum tự set
Allowheader khi trả 405 Method Not Allowed (lock từ B16).
Method Router Helper Function
axum cung cấp một helper function cho mỗi HTTP method trong module axum::routing. Mỗi function nhận một handler và trả về MethodRouter — service chỉ match đúng method tương ứng. Bảng mapping đầy đủ:
Helper HTTP Method Dùng cho REST
-----------------+----------------+-------------------------
get(handler) GET read resource / list
post(handler) POST create / action non-idempotent
put(handler) PUT replace full resource
delete(handler) DELETE remove resource
patch(handler) PATCH partial update
head(handler) HEAD metadata only, no body
options(handler) OPTIONS CORS preflight, capability query
trace(handler) TRACE debug echo (gần như không dùng)
5 method đầu (get, post, put, delete, patch) là cốt lõi REST đã lock từ B2 (CRUD ↔ method ↔ URL). 3 method còn lại ít dùng cho data endpoint thực tế nhưng axum vẫn cung cấp đầy đủ: head cho client kiểm tra resource tồn tại không tải body (sweet spot cho ETag check), options cho browser preflight CORS (tower-http CorsLayer xử lý tự động, B5 lock policy), trace hầu như không dùng và thường bị edge proxy chặn vì lý do bảo mật.
Usage cơ bản — đăng ký route GET đơn lẻ và route nhiều method chain trên cùng path:
use axum::{
routing::{delete, get, post, put},
Router,
};
let app: Router = Router::new()
.route("/products", get(list_products).post(create_product))
.route(
"/products/:slug",
get(get_product).put(update_product).delete(delete_product),
);
Một dòng .route() đăng ký 1 path với 1 hoặc nhiều method. Cùng path mà tách thành 2 .route() riêng → axum panic lúc khởi động router (chi tiết bước 4).
MethodRouter Trait Cốt Lõi
MethodRouter<S, E> là struct chính trong axum::routing — Service kết hợp method matching và handler dispatch. Khi bạn gọi get(handler), axum tạo một MethodRouter mới chứa duy nhất entry cho method GET trỏ tới handler. Khi chain thêm .post(handler2), một entry mới cho POST được thêm vào cùng MethodRouter đó.
// Signature đơn giản hóa
pub struct MethodRouter<S = (), E = Infallible> {
// internal: HashMap<Method, Service> + fallback Service
// ...
}
impl<S, E> MethodRouter<S, E> {
pub fn get<H, T>(self, handler: H) -> Self where H: Handler<T, S> { ... }
pub fn post<H, T>(self, handler: H) -> Self where H: Handler<T, S> { ... }
pub fn put<H, T>(self, handler: H) -> Self where H: Handler<T, S> { ... }
// ... patch, delete, head, options, trace
}
Hai generic parameter:
S— State type (AppStateđã lock B17 cho Shop API). Compiler enforce handler có signature acceptState<S>extractor đúng kiểu.E— Error type, defaultInfallible(handler không bao giờ fail ở tầng Service — fail là HTTP response). Hầu như không bao giờ cần đổi.
Khi request đến axum Router, internal flow chia 2 bước rạch ròi:
Request → match path → tìm MethodRouter tương ứng path
→ match method → lookup HashMap<Method, Service>
→ match found → dispatch handler service
→ match miss → trả 405 Method Not Allowed
+ Allow header liệt kê method support
Điểm hay: axum chia tách path matching (do Router đảm nhiệm) và method matching (do MethodRouter đảm nhiệm). Nếu path match nhưng method không support, axum biết chính xác tập method nào support và tự set header Allow trong response 405. Bạn không phải tự xử lý — đây là behavior built-in của MethodRouter.
Đó cũng là lý do AppError::MethodNotAllowed(String) trong shop-common::error (lock B16) chỉ phục vụ case bạn chủ động trả 405 từ handler (vd: business rule reject method trong runtime check). Trường hợp method không đăng ký, axum tự xử lý hoàn toàn không cần handler.
Multi-Method Cùng Path
Một resource REST thường có nhiều method chia sẻ cùng path. Pattern chuẩn Shop API: gộp tất cả method cho path đó vào một .route() call thông qua chain helper trên MethodRouter:
// Pattern Shop API — GET/PUT/DELETE cho resource detail
Router::new().route(
"/api/v1/products/:slug",
get(get_product)
.put(replace_product)
.patch(update_product)
.delete(delete_product),
)
Lợi ích cụ thể:
- 1 path — nhiều method gom 1 chỗ. Khi đọc code, mọi behavior của
/products/:slugđứng cạnh nhau, không phải tìm rải rác. - Đăng ký 405 +
Allowheader tự động.MethodRouterđó biết tập{GET, PUT, PATCH, DELETE}, request gửiPOST /products/phone-xsẽ trả 405 +Allow: GET, PUT, PATCH, DELETE. - Refactor an toàn. Thêm/bớt method chỉ sửa 1 chain, không phải sửa nhiều dòng
.route()tách rời.
Anti-pattern: tách thành 2 .route() call cùng path:
// ❌ KHÔNG được — axum panic lúc khởi động
Router::new()
.route("/products/:slug", get(get_product))
.route("/products/:slug", post(create_product)); // ← duplicate path
Khi chạy cargo run -p shop-api, axum panic với message tương đương: Overlapping method route. Cannot add two method routes for the same path. Lý do: .route("/x", X) insert một entry (path, MethodRouter) vào Router; gọi lại với cùng path không merge MethodRouter cũ + mới mà reject để tránh ambiguity. Pattern đúng luôn là chain trên cùng một MethodRouter:
// ✅ Đúng — 1 path, 2 method chain
Router::new().route("/products/:slug", get(get_product).post(create_product))
any() Matcher Cho Catch-All Method
any(handler) là helper đặc biệt match mọi HTTP method trên path đó (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE, hoặc method tùy ý client gửi). Use case:
- Catch-all logging/debug endpoint trong môi trường dev — ghi mọi request đến một path nhất định bất kể method.
- Webhook receiver nhận bất kỳ method (mặc dù Stripe gửi POST, đôi khi test cần GET để kiểm tra endpoint sống).
- Method-agnostic redirect/proxy chuyển hướng mọi request đến service khác (gateway pattern).
use axum::{
extract::Path,
http::{HeaderMap, Method, StatusCode},
routing::any,
Router,
};
async fn debug_request(
method: Method,
Path(path): Path<String>,
headers: HeaderMap,
) -> StatusCode {
tracing::info!(method = %method, path = %path, headers = ?headers, "debug request");
StatusCode::OK
}
let app: Router = Router::new().route("/debug/*path", any(debug_request));
Trong handler trên, Method extractor lấy HTTP method runtime, Path bắt nguyên phần wildcard *path, HeaderMap đọc toàn bộ header. Bạn có thể quan sát mọi request gửi vào /debug/... không quan tâm method.
Decision Shop API: KHÔNG dùng any() cho data endpoint nào. Lý do:
- Phá vỡ semantic REST — client/CDN/monitoring dựa vào method để phân loại (GET cacheable, POST mutation).
any()làm mọi method dispatch về cùng handler, mất khả năng cache/track riêng từng loại. - Mất
Allowheader tự động —any()không khai báo tập method cụ thể, không trả 405 cho method không hợp lý. - Khó audit security — middleware auth/rate-limit thường có policy khác cho từng method (GET catalog mở public, POST/PUT/DELETE require Bearer);
any()bypass phân biệt này.
Nếu cần debug, dùng tower-http::TraceLayer (đã lock G15) log mọi request structured, không cần endpoint catch-all riêng.
Route Ordering Pitfall
axum match route theo thứ tự đăng ký — route đầu tiên match sẽ thắng. Nếu bạn đặt route có path parameter :slug trước route static /popular, request /products/popular sẽ rơi vào handler nhận :slug = "popular", không phải list_popular:
// ❌ Sai thứ tự — /products/popular match :slug
Router::new()
.route("/products/:slug", get(get_product)) // ← match trước
.route("/products/popular", get(list_popular)) // ← không bao giờ match
Khi request GET /products/popular đến, axum so khớp /products/:slug trước, match → dispatch get_product với slug = "popular". Handler list_popular không bao giờ chạy. Bug âm thầm — không có error compile, không có panic runtime, chỉ là behavior sai.
Quy tắc đúng: static path trước, parameter path sau, wildcard cuối cùng:
// ✅ Đúng — static trước, parameter sau
Router::new()
.route("/products/popular", get(list_popular)) // static
.route("/products/me", get(my_products)) // static
.route("/products/:slug", get(get_product)) // parameter cuối
Khi request /products/popular đến, axum so khớp /products/popular trước (exact match), dispatch list_popular đúng. Request /products/phone-x không match static nào → fallback xuống /products/:slug, dispatch get_product với slug = "phone-x".
Quy tắc lock vĩnh viễn cho Shop API: trong cùng base path, đăng ký theo độ specific giảm dần — static path > parameter path :x > wildcard *rest. Áp dụng cho Product, Order, Cart, User và mọi resource tương lai. Ví dụ Order tương lai:
// Áp dụng cho Order — chú ý "me" + "history" static trước :id
Router::new()
.route("/api/v1/orders/me", get(my_orders)) // static
.route("/api/v1/orders/history", get(order_history)) // static
.route("/api/v1/orders/:id", get(get_order)) // parameter
.route("/api/v1/orders/:id/cancel", post(cancel_order)) // parameter + action
Refactor shop-api Thêm Product Route Skeleton
Apply kiến thức trên vào Shop API: thêm Product CRUD route skeleton chuẩn bị G7 implement đầy đủ với DB. State sau B17-B18: cấu trúc crates/shop-api/src/{app,router,state,routes/,handlers/,dto/,middleware/,extractors/,responses/}; routes/ hiện có health.rs, version.rs, demo_error.rs, demo_async.rs. B21 thêm file mới routes/products.rs, update routes/mod.rs và router.rs.
File mới crates/shop-api/src/routes/products.rs:
// File: crates/shop-api/src/routes/products.rs
use axum::{
extract::Path,
http::StatusCode,
routing::{delete, get, patch, post, put},
Json, Router,
};
use serde_json::json;
use crate::state::AppState;
pub fn routes() -> Router<AppState> {
Router::new()
// Collection routes — GET list + POST create
.route(
"/api/v1/products",
get(list_products).post(create_product),
)
// Static path TRƯỚC parameter — /popular trước :slug
.route("/api/v1/products/popular", get(list_popular))
// Resource detail — parameter cuối, 4 method chain
.route(
"/api/v1/products/:slug",
get(get_product)
.put(replace_product)
.patch(update_product)
.delete(delete_product),
)
}
// Skeleton handlers — G7 sẽ implement chi tiết với sqlx
async fn list_products() -> Json<serde_json::Value> {
Json(json!({ "items": [], "total": 0 }))
}
async fn create_product() -> (StatusCode, Json<serde_json::Value>) {
(
StatusCode::CREATED,
Json(json!({ "id": 1, "name": "demo product" })),
)
}
async fn list_popular() -> Json<serde_json::Value> {
Json(json!({ "items": [] }))
}
async fn get_product(Path(slug): Path<String>) -> Json<serde_json::Value> {
Json(json!({ "slug": slug, "name": "demo product" }))
}
async fn replace_product(Path(slug): Path<String>) -> Json<serde_json::Value> {
Json(json!({ "slug": slug, "name": "replaced product" }))
}
async fn update_product(Path(slug): Path<String>) -> Json<serde_json::Value> {
Json(json!({ "slug": slug, "updated": true }))
}
async fn delete_product(Path(_slug): Path<String>) -> StatusCode {
StatusCode::NO_CONTENT
}
Update crates/shop-api/src/routes/mod.rs thêm pub mod products;:
// File: crates/shop-api/src/routes/mod.rs
pub mod demo_async;
pub mod demo_error;
pub mod health;
pub mod products; // ← NEW B21
pub mod version;
Update crates/shop-api/src/router.rs merge sub-router mới:
// File: crates/shop-api/src/router.rs (trích đoạn build_router)
pub fn build_router(state: AppState) -> Router {
Router::new()
.route("/", get(root))
.merge(routes::health::routes())
.merge(routes::version::routes())
.merge(routes::demo_error::routes())
.merge(routes::demo_async::routes())
.merge(routes::products::routes()) // ← NEW B21
.with_state(state)
}
Pattern lock cho Shop API: mỗi resource là một file routes/<name>.rs export pub fn routes() -> Router<AppState>, master router.rs chỉ thêm 1 dòng .merge(routes::<name>::routes()). Order code grouped theo resource, dễ test, dễ refactor.
Demo route demo_error.rs và demo_async.rs giữ nguyên — vẫn hữu ích để verify error envelope (B16) và fire-and-forget pattern (B18). Khi DTO + DB sẵn sàng từ G7, các route demo sẽ remove dần. Skeleton handler hiện trả JSON tĩnh — đủ để test method routing + path parameter + ordering hoạt động đúng trước khi gắn DB.
Test Verify
Chạy server và verify 7 endpoint Product skeleton:
cargo run -p shop-api
Log kỳ vọng giống B12-B17: shop-api listening addr=0.0.0.0:3000. Mở terminal khác test từng method:
# GET list — collection
curl -i http://localhost:3000/api/v1/products
# HTTP/1.1 200 OK
# content-type: application/json
# {"items":[],"total":0}
# POST create — 201 Created
curl -i -X POST http://localhost:3000/api/v1/products \
-H 'Content-Type: application/json' -d '{}'
# HTTP/1.1 201 Created
# content-type: application/json
# {"id":1,"name":"demo product"}
Verify ordering — request /popular phải vào list_popular, không phải get_product:
# Static path WIN — list_popular handler
curl http://localhost:3000/api/v1/products/popular
# {"items":[]}
# Parameter path — get_product handler với slug = "phone-x"
curl http://localhost:3000/api/v1/products/phone-x
# {"slug":"phone-x","name":"demo product"}
Verify 4 method cùng path resource detail:
# PUT replace
curl -i -X PUT http://localhost:3000/api/v1/products/phone-x
# 200 {"name":"replaced product","slug":"phone-x"}
# PATCH partial update
curl -i -X PATCH http://localhost:3000/api/v1/products/phone-x
# 200 {"slug":"phone-x","updated":true}
# DELETE — 204 No Content (body rỗng)
curl -i -X DELETE http://localhost:3000/api/v1/products/phone-x
# HTTP/1.1 204 No Content
# (no body)
Verify 405 + Allow header tự động — axum biết /api/v1/products chỉ support {GET, POST}:
# TRACE method không đăng ký → 405
curl -i -X TRACE http://localhost:3000/api/v1/products
# HTTP/1.1 405 Method Not Allowed
# allow: GET, POST
# DELETE trên collection không đăng ký → 405
curl -i -X DELETE http://localhost:3000/api/v1/products
# HTTP/1.1 405 Method Not Allowed
# allow: GET, POST
Header Allow được axum tự sinh từ MethodRouter — không có dòng code nào trong Shop API set Allow thủ công. Đây là behavior built-in confirm ở bước 3.
Tổng Kết
- 8 method helper function trong
axum::routing:get,post,put,delete,patch,head,options,trace; 5 đầu là cốt lõi REST (lock B2). MethodRouter<S, E>là Service kết hợp method matching + handler dispatch; internal là HashMap method → service + fallback.- Chain method cùng path:
get(h1).post(h2).delete(h3)trong 1.route()call — code grouped theo resource, clean. - Đăng ký 2
.route("/x", ...)cùng path → axum panicOverlapping method routelúc khởi động. any(handler)matcher match mọi method — REST API hiếm dùng; Shop API KHÔNG dùng cho data endpoint (mất semantic + Allow header + audit security).- Route ordering rule: static path > parameter path
:x> wildcard*rest; axum match theo thứ tự đăng ký. - axum tự set
Allowheader khi trả 405 — từMethodRouterbiết tập method nào hỗ trợ (lock B16). - Shop API: Product CRUD skeleton ở
crates/shop-api/src/routes/products.rs— 7 endpoint chuẩn bị G7 implement đầy đủ với sqlx + DB. - Pattern lock 5-step add resource (xem chi tiết shop-state.md note): file
routes/<name>.rs→routes/mod.rs→router.rsmerge → DTO G7 → tách handlers khi >200 dòng.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- 5 method helper function REST API thường dùng nhất là gì? Mỗi method dùng cho action CRUD nào?
- Chain
.get(h1).post(h2).delete(h3)cùng path có hợp lý không? Lợi ích cụ thể gì so với tách 3 dòng.route()? - Đăng ký
/products/:slugtrước/products/popular. Vấn đề gì xảy ra với requestGET /products/popular? Cách fix? - axum tự xử lý 405 Method Not Allowed như thế nào? Header gì được set tự động và lấy thông tin từ đâu?
any(handler)matcher use case nào hợp lý? Shop API có dùng cho data endpoint không? Tại sao?
Đáp án
- 5 method helper REST cốt lõi trong
axum::routing:get(GET — Read: list collection hoặc đọc resource theo ID/slug),post(POST — Create: tạo resource mới, hoặc action non-idempotent như checkout/cancel/login),put(PUT — Replace: thay toàn bộ resource bằng body mới, idempotent),delete(DELETE — Remove: xóa resource, idempotent),patch(PATCH — Update partial: cập nhật một số field, dùngOption<Option<T>>quaserde_with::rust::double_optionphân biệt missing/null/value theo lock B6). Mapping CRUD ↔ method ↔ URL đã lock từ B2:POST /api/v1/products= create + 201 + Location,GET /api/v1/products/:slug= read,PUT /api/v1/products/:slug= replace,PATCH /api/v1/products/:slug= partial update,DELETE /api/v1/products/:slug= remove. Action không map CRUD dùng POST verb-noun:POST /api/v1/orders/:id/cancel,POST /api/v1/checkout. - Chain
.get(h1).post(h2).delete(h3)cùng path là pattern chuẩn axum — bắt buộc dùng khi cần nhiều method cho cùng path. Lợi ích so với tách 3 dòng: (a) Code grouped theo resource — mọi behavior của path đó đứng cạnh nhau, đọc 1 chỗ thấy hết, dễ refactor; (b) axum tự sinhAllowheader khi 405 —MethodRouterđó biết tập{GET, POST, DELETE}, request gửi method khác (vd PUT) trả 405 +Allow: GET, POST, DELETEtự động không cần code thủ công; (c) Compiler enforce signature handler — genericMethodRouter<S, E>cố định state type, mọi handler trong chain phải accept cùngState<S>extractor đúng kiểu. Tách thành 2.route()riêng cùng path bị axum reject: panicOverlapping method route. Cannot add two method routes for the same pathlúc khởi động router — axum không tự merge MethodRouter cũ + mới để tránh ambiguity. - Vấn đề: request
GET /products/popularsẽ rơi vào handlerget_productvớislug = "popular", KHÔNG vàolist_popular. Lý do: axum match route theo thứ tự đăng ký, route đầu tiên match thắng. Đặt/products/:slugtrước, parameter:slugmatch được mọi string trong segment (bao gồm "popular"), nên axum dispatchget_productluôn. Handlerlist_popularkhông bao giờ chạy. Bug âm thầm — không có error compile, không có panic runtime, chỉ là behavior sai (có thể trả về data sản phẩm tên "popular" nếu thực sự tồn tại, hoặc 404 nếu không). Fix: đảo thứ tự — đăng ký static path TRƯỚC parameter:.route("/products/popular", get(list_popular)).route("/products/:slug", get(get_product)). Quy tắc lock vĩnh viễn Shop API (B21): trong cùng base path, đăng ký theo độ specific giảm dần — static > parameter:x> wildcard*rest. Áp dụng cho mọi resource Product, Order, Cart, User, Admin tương lai. - axum tự xử lý 405 Method Not Allowed thông qua
MethodRouter: khi request match path nhưng method không có trong HashMap method → service củaMethodRouterđó, axum tự build response 405 với body rỗng và set headerAllowliệt kê tập method nào hỗ trợ path đó. Ví dụ/api/v1/productsđăng kýget(list_products).post(create_product), request gửiTRACE /api/v1/productstrả:HTTP/1.1 405 Method Not Allowed+ headerallow: GET, POST+ body rỗng. HeaderAllowtuân RFC 9110 mục 10.2.1 (server PHẢI gửi Allow khi trả 405), value là comma-separated list các HTTP method. Thông tin lấy từ đâu:MethodRoutergiữ HashMapMethod → Service+ fallback service; khi method không match, axum iterate keys của HashMap thành list, format thành header value"GET, POST, DELETE, PATCH". Không cần code thủ công — đây là behavior built-in củaMethodRouter. Lưu ý:AppError::MethodNotAllowed(String)trongshop-common::error(lock B16) chỉ phục vụ case bạn chủ động trả 405 từ handler khi business rule reject method (rất hiếm); trường hợp method không đăng ký, axum tự xử lý hoàn toàn không cần handler chạm tay. any(handler)matcher use case hợp lý: (a) Catch-all logging/debug endpoint môi trường dev — ghi mọi request đến path bất kể method để debug; (b) Webhook receiver nhận bất kỳ method (mặc dù chuẩn là POST nhưng test cần GET ping endpoint sống); (c) Method-agnostic redirect/proxy gateway pattern. Shop API KHÔNG dùngany()cho data endpoint nào. 3 lý do: (1) Phá vỡ semantic REST — client/CDN/monitoring dựa method để phân loại behavior (GET cacheable theoCache-Control: publiclock B4, POST/PUT/DELETE mutation cần auth + idempotency key lock B4);any()dispatch mọi method về cùng handler mất khả năng cache/track riêng; (2) MấtAllowheader tự động —any()không khai báo tập method cụ thể, axum không thể trả 405 cho method không hợp lý vìany()match hết; (3) Khó audit security — middleware auth/rate-limit thường có policy khác cho từng method (GET catalog public mở, POST/PUT/DELETE require Bearer JWT lock B4);any()bypass phân biệt này, mở lỗ hổng. Nếu cần debug log toàn bộ request, dùngtower-http::TraceLayer(đã lock G15) cho structured log per request, không cần endpoint catch-all riêng.
Bài Tiếp Theo
Bài 22: Path Parameters Với Path<T> — chi tiết Path extractor: :id parse i64, :slug parse String, tuple Path<(u64, String)> cho multi-parameter, optional path missing, URL encode/decode pitfall, error case parse fail.
