Danh sách bài viết

Bài 21: Route Methods: GET, POST, PUT, DELETE, PATCH

Bài 21 của series Rust RESTful API — bài ĐẦU Group 3 Routing Cơ Bản, đi sâu method router helper function trong axum::routing: 5 method cốt lõi REST get, post, put, delete, patch + 3 method bổ sung head, options, trace, mỗi function trả về MethodRouter<S, E> — service kết hợp method matching + handler dispatch, internal là HashMap method → service + fallback; pattern multi-method cùng path chain .get(h1).post(h2).delete(h3) trong cùng .route() call gom code grouped clean — đăng ký 2 lần cùng path qua 2 .route() tách rời sẽ bị axum reject với error Overlapping method route; any(handler) matcher catch-all match mọi HTTP method cho use case debug/webhook nhưng Shop API quyết định KHÔNG dùng cho data endpoint (mỗi endpoint phải explicit method để client/CDN/monitoring nhận diện đúng); route ordering pitfall — axum match theo thứ tự đăng ký nên đặt /products/:slug trước /products/popular sẽ làm request /products/popular rơi vào handler get_product với slug = "popular", quy tắc lock cho Shop API: static path > parameter path > wildcard *rest trong cùng base path; axum tự set Allow header khi trả 405 Method Not Allowed (MethodRouter biết tập method nào hỗ trợ); apply Shop API: refactor thêm Product CRUD route skeleton ở crates/shop-api/src/routes/products.rs với 7 endpoint GET/POST /api/v1/products + GET /api/v1/products/popular + GET/PUT/PATCH/DELETE /api/v1/products/:slug, handler trả demo JSON chuẩn bị G7 implement full với DB; pattern lock 5-step workflow add resource áp dụng cho Product, Order, Cart, User tương lai.

13/06/2026
10 phút đọc
1 lượt xem
1

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ổ sung head, options, trace.
  • Hiểu MethodRouter trait: kết quả khi gọi get(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 skeletonroutes/products.rs, chuẩn bị G7 implement đầy đủ với DB.
  • Biết axum tự set Allow header khi trả 405 Method Not Allowed (lock từ B16).
2

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).

3

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 accept State<S> extractor đúng kiểu.
  • E — Error type, default Infallible (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.

4

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 + Allow header tự động. MethodRouter đó biết tập {GET, PUT, PATCH, DELETE}, request gửi POST /products/phone-x sẽ 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))
5

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 Allow header tự độngany() 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.

6

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
7

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.rsrouter.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.rsdemo_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.

8

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.

9

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 panic Overlapping method route lú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 Allow header khi trả 405 — từ MethodRouter biế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>.rsroutes/mod.rsrouter.rs merge → DTO G7 → tách handlers khi >200 dòng.
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. 5 method helper function REST API thường dùng nhất là gì? Mỗi method dùng cho action CRUD nào?
  2. 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()?
  3. Đăng ký /products/:slug trước /products/popular. Vấn đề gì xảy ra với request GET /products/popular? Cách fix?
  4. 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?
  5. 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
  1. 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ùng Option<Option<T>> qua serde_with::rust::double_option phâ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.
  2. 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ự sinh Allow header khi 405MethodRouter đó biết tập {GET, POST, DELETE}, request gửi method khác (vd PUT) trả 405 + Allow: GET, POST, DELETE tự động không cần code thủ công; (c) Compiler enforce signature handler — generic MethodRouter<S, E> cố định state type, mọi handler trong chain phải accept cùng State<S> extractor đúng kiểu. Tách thành 2 .route() riêng cùng path bị axum reject: panic Overlapping method route. Cannot add two method routes for the same path lúc khởi động router — axum không tự merge MethodRouter cũ + mới để tránh ambiguity.
  3. Vấn đề: request GET /products/popular sẽ rơi vào handler get_product với slug = "popular", KHÔNG vào list_popular. Lý do: axum match route theo thứ tự đăng ký, route đầu tiên match thắng. Đặt /products/:slug trước, parameter :slug match được mọi string trong segment (bao gồm "popular"), nên axum dispatch get_product luôn. 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 (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.
  4. 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ủa MethodRouter đó, axum tự build response 405 với body rỗng và set header Allow liệ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ửi TRACE /api/v1/products trả: HTTP/1.1 405 Method Not Allowed + header allow: GET, POST + body rỗng. Header Allow tuâ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: MethodRouter giữ HashMap Method → 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ủa MethodRouter. Lưu ý: AppError::MethodNotAllowed(String) trong shop-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.
  5. 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ùng any() 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 theo Cache-Control: public lock 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ất Allow header tự độngany() 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ùng tower-http::TraceLayer (đã lock G15) cho structured log per request, không cần endpoint catch-all riêng.
11

Bài Tiếp Theo

— 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.