Danh sách bài viết

Bài 26: Route Ordering — Pitfall Thứ Tự

Bài 26 của series Rust RESTful API — đi sâu vào quy tắc first match wins của axum khi match request URL với route table (KHÔNG có concept "best match" như nginx hay Express), ba loại path segment với độ specific giảm dần (static literal match exact như /products/popular, parameter :name match bất kỳ chuỗi không chứa slash như /products/:slug, wildcard *rest catch-all đến cuối path bao gồm slash như /files/*rest), priority đúng static > parameter > wildcard trong cùng base path (lock B21), pitfall phổ biến nhất khi mới làm việc với axum: đặt parameter trước static cùng base path → static bị "ăn" bởi parameter (vd khai báo /products/:slug trước /products/popular, request GET /products/popular match /products/:slug với slug = "popular" invoke get_product thay vì list_popular — silent bug không error compile/runtime), pitfall lan tới nested router multi-level cùng rule, debug strategy: compile-time axum KHÔNG detect ordering issue (route đăng ký runtime qua .route() chain), runtime phát hiện qua request đến (handler sai chạy thay handler đúng) → strategy đúng là integration test mandatory cho mỗi static path conflict với parameter + log middleware (G15 TraceLayer) verify handler name khớp path expected + audit checklist code review pull request, audit Shop API routes/products.rs hiện tại confirm đã đúng pattern lock B21 (/products/popular static TRƯỚC /products/:slug parameter, /products/:slug/related/:related_slug nhiều segment cũng TRƯỚC /products/:slug), lock convention cho mọi resource mới ở G7+ (Order/Cart/User/Admin): file routes/<name>.rs LUÔN order static (1) → parameter (2) → wildcard (3), pattern URL Shop API với static keyword phổ biến (/me, /recent, /popular, /items, /related) phải đặt trước parameter cùng base path tương ứng.

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

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Hiểu axum match route theo quy tắc first match wins (route đăng ký trước thắng), KHÔNG phải "best match".
  • Nắm 3 loại path segment: static literal, parameter :name, wildcard *rest — và độ specific giảm dần của mỗi loại.
  • Biết priority đăng ký đúng: static > parameter > wildcard trong cùng base path (lock B21).
  • Phát hiện pitfall phổ biến nhất — static path bị parameter "ăn" khi đặt sai thứ tự.
  • Debug ordering issue qua integration test + log middleware thay vì rely runtime behavior.
  • Áp dụng convention Shop API cho mọi route mới ở G7+ (Order, Cart, User, Admin) với static URL keyword chuẩn (/me, /recent, /popular, /items, /related).
2

Quy Tắc First Match Wins

axum match request URL với route table theo thứ tự đăng ký. Route nào khớp đầu tiên được dispatch — không có concept "best match" như nginx location block (longest prefix wins) hay Express app.use (also sequential nhưng với regex precedence). axum đơn giản hơn: quét tuần tự, dừng ở route đầu tiên match cả path và method.

Mô hình mental đúng cho axum routing:

┌─────────────────────────────────────┐
│ Router::new()                       │
│   .route("/users/me",       ...)    │  ← Check 1st
│   .route("/users/:id",      ...)    │  ← Check 2nd
│   .route("/users/:id/cart", ...)    │  ← Check 3rd
│                                     │
└─────────────────────────────────────┘
              │
              ▼
   GET /users/me request
              │
              ▼
   Check 1: "/users/me" — MATCH → dispatch
   (Check 2 và 3 không bao giờ chạy)

So sánh với framework có "best match" algorithm hypothetical:

┌──────────────────────────────────────────┐
│ Hypothetical "best match" framework      │
│   route("/users/:id",  ...)              │
│   route("/users/me",   ...)              │
└──────────────────────────────────────────┘
              │
              ▼
   GET /users/me request
              │
              ▼
   Quét toàn bộ table → tìm route specific nhất
   Static "/users/me" specific hơn "/users/:id"
   → dispatch /users/me handler (đúng intent)

axum CHỌN sequential first match vì 2 lý do thiết kế: (1) predictable — bạn đọc router.rs top-down biết chính xác request được dispatch handler nào, không phải lookup precedence table; (2) performance — match O(n) với n nhỏ thường nhanh hơn build và query precedence tree O(log n) cho route table dưới 100 entry.

Implication quan trọng: thứ tự bạn đăng ký matter. Đảo 2 dòng .route() có thể đổi behavior runtime mà rustc không phát hiện. Đây là gốc rễ của mọi pitfall trong bài này.

3

3 Loại Path Segment

axum 0.8 hỗ trợ ba loại path segment với độ specific giảm dần. Hiểu đặc tính match của mỗi loại là tiền đề để áp dụng priority rule ở bước 4.

Static segment — literal string match exact. Path /products/popular match đúng request URL có path đúng /products/popular, không hơn không kém:

Router::new()
    .route("/products/popular", get(list_popular));

// Match:    GET /products/popular        → list_popular
// Không match:
//   GET /products/Popular       (case-sensitive)
//   GET /products/popular/      (trailing slash khác segment)
//   GET /products/popular/extra (path dài hơn)

Parameter segment :name — match bất kỳ chuỗi không chứa slash, giá trị capture vào extractor Path<T> (lock B22):

Router::new()
    .route("/products/:slug", get(get_product));

// Match:
//   GET /products/phone-x        → slug = "phone-x"
//   GET /products/abc            → slug = "abc"
//   GET /products/popular        → slug = "popular"  ← chú ý
//   GET /products/123            → slug = "123"
// Không match:
//   GET /products/phone/x        (slash trong slug → 2 segment)
//   GET /products                (thiếu segment)

Pitfall hé lộ ở dòng thứ ba: parameter :slug match cả chuỗi "popular" — đây là gốc rễ của ordering bug ở bước 5.

Wildcard segment *rest — catch-all đến cuối path, bao gồm slash:

Router::new()
    .route("/files/*rest", get(serve_file));

// Match:
//   GET /files/foo               → rest = "foo"
//   GET /files/foo/bar           → rest = "foo/bar"
//   GET /files/foo/bar/baz.jpg   → rest = "foo/bar/baz.jpg"
// Không match:
//   GET /files                   (cần ít nhất 1 ký tự sau /files/)

Bảng tổng hợp độ specific:

┌────────────┬──────────────┬───────────────────┬──────────┐
│ Loại       │ Cú pháp       │ Match phạm vi     │ Specific │
├────────────┼──────────────┼───────────────────┼──────────┤
│ Static     │ /products    │ Chuỗi đúng exact  │ Cao nhất │
│ Parameter  │ /:slug       │ 1 segment, no /   │ Trung    │
│ Wildcard   │ /*rest       │ Đến cuối path     │ Thấp nhất│
└────────────┴──────────────┴───────────────────┴──────────┘

Shop API decision (lock B22): wildcard *rest KHÔNG dùng cho data endpoint nào — phá vỡ semantic REST + dễ overlap. Wildcard chỉ xuất hiện implicit qua tower-http::ServeDir mount cho static file (B27). Bài này thảo luận wildcard để bạn hiểu trọn priority rule, không phải để khuyến khích dùng cho domain endpoint.

4

Priority: Static > Parameter > Wildcard

Quy tắc lock B21 cho Shop API: trong cùng base path, đăng ký route theo độ specific giảm dần — static > parameter > wildcard. Nguyên nhân: static path cụ thể hơn parameter (match exact 1 chuỗi vs match nhiều chuỗi), parameter cụ thể hơn wildcard (match 1 segment vs match nhiều segment).

Mẫu chuẩn cho 1 resource hỗn hợp đầy đủ 3 loại segment:

// Pattern chuẩn lock B21 — đăng ký theo độ specific giảm dần
Router::new()
    .route("/products/popular",  get(list_popular))   // 1. Static FIRST
    .route("/products/me",       get(my_products))    // 2. Static
    .route("/products/:slug",    get(get_product))    // 3. Parameter
    .route("/products/*rest",    get(catch_all));     // 4. Wildcard LAST

Lookup behavior cho 3 request đại diện:

  • Request GET /products/popular — quét tuần tự: dòng 1 match static /products/popular → invoke list_popular đúng intent.
  • Request GET /products/phone-x — dòng 1 không match, dòng 2 không match, dòng 3 match parameter :slug = "phone-x" → invoke get_product.
  • Request GET /products/category/electronics (2 segment sau /products/) — dòng 1-3 không match, dòng 4 match wildcard *rest = "category/electronics" → invoke catch_all.

Áp dụng vào Shop API thực tế — đây là state hiện tại của routes/products.rs sau B24 nest refactor:

// File: crates/shop-api/src/routes/products.rs (state hiện tại sau B25)
pub fn routes() -> Router<AppState> {
    Router::new()
        // Collection (multi-method)
        .route(
            "/products",
            get(list_products).post(create_product),
        )
        // Static specific TRƯỚC parameter
        .route("/products/popular", get(list_popular))
        // Parameter multi-segment specific hơn single parameter
        .route(
            "/products/:slug/related/:related_slug",
            get(get_related_product),
        )
        // Parameter single CUỐI cùng cho phần dynamic
        .route(
            "/products/:slug",
            get(get_product)
                .put(replace_product)
                .patch(update_product)
                .delete(delete_product),
        )
}

Ba quan sát:

  • /products/popular static đăng ký trước /products/:slug parameter — request GET /products/popular match dòng 2 đúng intent invoke list_popular, KHÔNG rơi xuống dòng 4 với slug = "popular".
  • /products/:slug/related/:related_slug (2 dynamic segment) đăng ký trước /products/:slug (1 dynamic segment) — request GET /products/phone-x/related/case-y match dòng 3 đúng intent, KHÔNG rơi xuống dòng 4 (vốn không match cho path 3 segment).
  • KHÔNG có wildcard nào — Shop API decision lock B22, đúng pattern data endpoint.

Quy tắc mở rộng cho route nhiều dynamic segment: nhiều segment specific hơn ít segment. /products/:slug/related/:related_slug match đúng 3 segment dưới /products/, /products/:slug match đúng 1 segment. Trường hợp request /products/phone-x/related/case-y, axum quét: dòng 2 không match (path không phải /products/popular), dòng 3 match → invoke get_related_product. Nếu đăng ký /products/:slug trước, dòng đó cũng KHÔNG match cho path 3 segment (parameter chỉ match 1 segment, không match qua slash) → fall through xuống dòng tiếp. Nhưng quy tắc đặt nhiều segment trước ít segment giữ cho code đọc dễ và đồng bộ thói quen.

5

Pitfall: Specific Path Bị Parameter Ăn

Pitfall phổ biến nhất khi mới làm việc với axum: đặt parameter trước static cùng base path. Bug âm thầm — code biên dịch sạch, server khởi động bình thường, không có warning runtime — nhưng handler sai chạy thay handler đúng.

// Anti-pattern — đảo thứ tự
Router::new()
    .route("/products/:slug",   get(get_product))    // ← Đặt TRƯỚC
    .route("/products/popular", get(list_popular));  // ← Bị IGNORE

Phân tích flow request GET /products/popular:

  • axum quét route table top-down.
  • Dòng 1 /products/:slug: parameter :slug match chuỗi "popular" (parameter match bất kỳ chuỗi không chứa slash, "popular" thỏa) → MATCH, capture slug = "popular".
  • axum invoke get_product với Path("popular") — handler chạy với slug rỗm "popular" rồi query DB SELECT * FROM products WHERE slug = 'popular' → trả 404 (sản phẩm slug "popular" không tồn tại) hoặc tệ hơn, trả nhầm sản phẩm test trùng slug.
  • Dòng 2 list_popular không bao giờ được gọi cho bất kỳ request nào.

Symptom debug từ phía dev:

  • Frontend báo endpoint /products/popular trả 404, dev nghi handler logic sai → debug query DB → confirm slug "popular" không có → kết luận "feature chưa implement" — nhầm gốc rễ.
  • Hoặc tệ hơn: dev seed một sản phẩm test có slug = "popular" rồi nghi data corruption, không nhận ra routing dispatch sai handler.
  • Log handler entry không có dòng "list_popular invoked" dù request trùng URL nhìn bằng mắt — đây là tín hiệu rõ ràng nhất nhưng dễ bị bỏ qua nếu không có structured log.

Fix đơn giản — đảo thứ tự đăng ký:

// Fix — static TRƯỚC parameter
Router::new()
    .route("/products/popular", get(list_popular))   // ← Static FIRST
    .route("/products/:slug",   get(get_product));   // ← Parameter SAU

Curl test demonstrate trước và sau fix:

# Trước fix — bug
curl -i http://localhost:3000/products/popular
# HTTP/1.1 404 Not Found
# {"error":"not found: product slug 'popular' not in database",...}
# (get_product chạy với slug="popular" → DB miss → 404 từ handler)

# Sau fix
curl -i http://localhost:3000/products/popular
# HTTP/1.1 200 OK
# {"items":[{"slug":"phone-x","sold":1234}, ...]}
# (list_popular chạy đúng intent)

Vì sao bug âm thầm? Hai lý do gốc: (a) cú pháp .route(...) chain hợp lệ cả 2 thứ tự, rustc compile pass; (b) runtime không có cơ chế nào báo "route X bị shadow bởi route Y" — axum trust thứ tự bạn đăng ký là thứ tự bạn muốn. Vẹn trách nhiệm cho developer + tooling pipeline.

6

Multi-Level Pitfall Khi Nest

Pitfall ordering lan tới nested router (lock B24). Quy tắc first match wins áp dụng cả trong sub-router, không tự "phẳng hóa" qua nest. Anti-pattern:

// Anti-pattern trong sub-router
let products_router = Router::new()
    .route("/:slug",   get(get_product))      // ← Parameter trong sub-router
    .route("/popular", get(list_popular));    // ← Static bị ignore

// Mount qua nest
Router::new()
    .nest("/api/v1/products", products_router);

// Request GET /api/v1/products/popular
// → axum prefix-match /api/v1/products → dispatch vào sub-router
// → trong sub-router, quét route table top-down
// → :slug match "popular" → invoke get_product (BUG)

Nest KHÔNG thay đổi quy tắc match — chỉ thêm prefix khi tính path match. Bên trong sub-router, axum vẫn quét tuần tự theo thứ tự đăng ký. Đặt parameter trước static trong sub-router gây bug giống hệt như flat router.

Fix cùng quy tắc: static trước parameter, ngay trong sub-router:

// Fix — static trước parameter trong sub-router
let products_router = Router::new()
    .route("/popular", get(list_popular))     // ← Static FIRST
    .route("/:slug",   get(get_product));     // ← Parameter SAU

Router::new()
    .nest("/api/v1/products", products_router);

Implication cho design pattern Shop API: review ordering rule áp dụng từng sub-router độc lập, không bỏ qua khi đang ở trong nested context. Mỗi file routes/<name>.rs phải tự đảm bảo ordering đúng cho route khai báo trong scope của mình. Nest chỉ là cơ chế gắn prefix, không "tự sửa" ordering bug.

7

Debug Strategy: Log + Test

Debug ordering bug khó vì hai cản trở:

  • Compile-time KHÔNG detect — route khai báo runtime qua .route() chain trên Router builder. rustc không hiểu semantic ordering, chỉ check signature handler. Compile pass cho mọi thứ tự — không có warning, không có lint nào tích hợp sẵn trong axum 0.8.
  • Runtime silent bug — chỉ phát hiện khi request đến và handler sai chạy thay handler đúng. Không có exception, không có log error tự sinh từ axum. Frontend báo "endpoint trả sai data" — dev phải truy ngược về routing.

Strategy đúng phân làm 4 lớp phòng thủ:

Lớp 1 — Convention khi thêm route mới. Khi add route trong sub-router, list tất cả route trong file theo priority: static → parameter → wildcard. Đảm bảo thứ tự đăng ký match đúng priority. Quy ước này đưa vào checklist code review.

Lớp 2 — Code review pull request. Reviewer flag bất kỳ thay đổi nào trong routes/<name>.rs có đảo thứ tự đăng ký route, hoặc thêm route mới không tuân priority. Inline comment trong file nhắc convention:

// File: crates/shop-api/src/routes/products.rs
//
// ORDERING RULE (lock B21/B26): static > parameter > wildcard
//   1. /products              — static collection
//   2. /products/popular      — static specific
//   3. /products/:slug/...    — parameter multi-segment
//   4. /products/:slug        — parameter single
// KHÔNG đảo thứ tự — sẽ shadow static path bằng parameter.
pub fn routes() -> Router<AppState> {
    // ... route khai báo theo priority ...
}

Lớp 3 — Integration test mandatory. Đây là biện pháp duy nhất tự động bắt ordering bug. Viết integration test với cả static path và parameter cho mỗi static endpoint conflict tiềm năng:

// File: crates/shop-api/tests/routing_ordering.rs (preview, sẽ implement đầy đủ ở G7)
#[tokio::test]
async fn list_popular_route_dispatches_correct_handler() {
    let app = build_router(test_state());

    let response = test_request(&app, "GET", "/api/v1/products/popular").await;

    assert_eq!(response.status(), 200);
    let body: serde_json::Value = response.json().await;

    // Verify body shape của list_popular (envelope items array),
    // KHÔNG phải get_product (single product object).
    assert!(body.get("items").is_some(),
        "expected list_popular envelope, got get_product shape");
}

#[tokio::test]
async fn get_product_route_dispatches_correct_handler() {
    let app = build_router(test_state());

    let response = test_request(&app, "GET", "/api/v1/products/phone-x").await;

    assert_eq!(response.status(), 200);
    let body: serde_json::Value = response.json().await;

    // Verify body shape của get_product (slug field),
    // KHÔNG phải list_popular (items array).
    assert_eq!(body["slug"], "phone-x");
}

Pattern test này có 2 đặc điểm bắt buộc: (a) chạy CẢ static path (/products/popular) VÀ parameter path (/products/phone-x) — chỉ test 1 trong 2 sẽ miss bug; (b) assert body shape đặc trưng của handler đúng — assert status 200 không đủ vì handler sai cũng có thể trả 200 với data lạc. Detail implementation full integration test với axum-test crate ở B253.

Lớp 4 — Log middleware verify runtime. tower-http::TraceLayer (G15) log structured field gồm method, URI matched, handler name (qua span name). Chạy server local, gửi request qua curl, đọc log xác nhận handler đúng được dispatch:

cargo run -p shop-api
# Terminal 1: server log
#   INFO request{method=GET uri=/api/v1/products/popular} list_popular: handling
#   INFO request{method=GET uri=/api/v1/products/popular} list_popular: 200 in 3ms

# Terminal 2: curl test
curl -i http://localhost:3000/api/v1/products/popular

Dòng log list_popular: handling xác nhận handler đúng được invoke. Nếu thấy get_product: handling cho cùng URL → routing dispatch sai → fix ordering.

8

Apply Vào Shop API: Audit Checklist

Audit routes/products.rs hiện tại của Shop API — file này đã đúng pattern lock B21, B26 không cần thay đổi code:

┌─────────────────────────────────────────────────────────┐
│ routes/products.rs — Ordering Audit                     │
├─────────────────────────────────────────────────────────┤
│ ✓ 1. /products                       (static collection)│
│ ✓ 2. /products/popular               (static specific)  │
│ ✓ 3. /products/:slug/related/:rel    (multi-param)      │
│ ✓ 4. /products/:slug                 (single param)     │
│                                                         │
│ Verdict: tuân thủ static > parameter, multi > single   │
└─────────────────────────────────────────────────────────┘

Confirm flow request thực tế:

  • GET /api/v1/products → dòng 1 match → list_products.
  • GET /api/v1/products/popular → dòng 2 match (static thắng trước parameter) → list_popular.
  • GET /api/v1/products/phone-x/related/case-y → dòng 3 match (multi-segment thắng trước single) → get_related_product.
  • GET /api/v1/products/phone-x → dòng 4 match → get_product.

Lock convention cho mọi resource mới ở G7+ — pattern URL với static keyword Shop API sẽ dùng xuyên suốt:

// User routes (preview G11 Auth Basic)
Router::new()
    .route("/users/me",      get(get_current_user))    // static
    .route("/users/recent",  get(list_recent_users))   // static (admin)
    .route("/users/:id",     get(get_user_by_id));     // parameter

// Order routes (preview G7 CRUD)
Router::new()
    .route("/orders",         get(list_orders).post(create_order))
    .route("/orders/recent",  get(list_recent_orders))    // static
    .route("/orders/:id",     get(get_order));            // parameter

// Cart routes (preview G7)
Router::new()
    .route("/cart",                  get(get_cart))
    .route("/cart/items",            post(add_item))      // static
    .route("/cart/items/:item_id",   patch(update_item)
                                       .delete(remove_item));  // parameter

Audit checklist khi review pull request thêm route mới:

  • List tất cả route trong file theo priority: static (1) → parameter (2) → wildcard (3).
  • Identify route có same base path conflict tiềm năng giữa static và parameter.
  • Verify static path đăng ký trước parameter cùng base path.
  • Verify multi-segment parameter trước single-segment parameter cùng base path.
  • Verify wildcard (nếu có) đăng ký cuối cùng — Shop API decision lock B22 không dùng wildcard cho data endpoint.
  • Integration test cho từng cặp static-parameter conflict (test mỗi handler nhận đúng request).

Suggested commit (no code change cần) — chỉ document convention qua inline comment: B26: document route ordering convention + add inline comment in routes/products.rs.

9

Tổng Kết

  • axum match request URL với route table theo quy tắc first match wins — sequential check theo thứ tự đăng ký, KHÔNG phải "best match" như nginx/Express.
  • 3 loại path segment: static literal match exact, parameter :name match 1 segment không chứa slash, wildcard *rest catch-all đến cuối path bao gồm slash.
  • Priority đúng trong cùng base path: static > parameter > wildcard (lock B21). Nhiều segment specific hơn ít segment — đăng ký trước.
  • Pitfall phổ biến nhất: parameter đặt trước static cùng base path → request match parameter ăn static (vd /products/:slug trước /products/popular/products/popular match :slug = "popular").
  • Multi-level nested router cùng rule — quy tắc first match áp dụng trong mỗi sub-router độc lập, nest KHÔNG tự sửa ordering.
  • Compile-time KHÔNG detect ordering bug — rustc compile pass mọi thứ tự, axum không có lint runtime.
  • Runtime silent bug — handler sai chạy thay handler đúng, không exception, không log error tự sinh.
  • Debug strategy 4 lớp: convention thứ tự khi add route, code review pull request, integration test mandatory cho mỗi cặp static-parameter conflict, log middleware verify handler đúng dispatch.
  • Shop API audit: routes/products.rs đã đúng pattern (/products/popular static trước /products/:slug parameter, /products/:slug/related/:related_slug multi-segment trước /products/:slug).
  • Lock convention G7+ cho mọi resource mới: file routes/<name>.rs LUÔN order static (1) → parameter (2) → wildcard (3). Pattern URL với static keyword: /me, /recent, /popular, /items, /related — đặt trước parameter cùng base path.
10

Bài Tập Củng Cố

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

  1. axum match route theo strategy nào? "First match wins" hay "best match"? Khác nginx location block và Express ra sao?
  2. 3 loại path segment trong axum 0.8 khác nhau ở điểm gì? Cho ví dụ mỗi loại + chỉ rõ phạm vi match.
  3. Code có .route("/products/:slug", ...) đăng ký TRƯỚC .route("/products/popular", ...). Request GET /products/popular match handler nào? Tại sao? Đây là lỗi compile hay runtime?
  4. Multi-level nest có thay đổi rule ordering không? Khi đặt parameter trước static trong sub-router rồi nest, behavior ra sao?
  5. Bug ordering silent runtime — rustc không bắt, axum không log error. Strategy phát hiện sớm là gì? Vì sao integration test bắt buộc?
Đáp án
  1. axum match theo first match wins — quét route table theo thứ tự đăng ký top-down, dispatch handler của route đầu tiên match cả path và method. KHÔNG có concept "best match" như nginx location block (longest prefix wins — quét toàn bộ block, chọn block có prefix dài nhất match URL) hay Express (sequential nhưng với regex precedence — route có ít wildcard hơn xem là specific hơn). axum chọn first match vì 2 lý do thiết kế: (a) predictable — đọc router.rs top-down biết chính xác request được dispatch handler nào, không phải lookup precedence table; (b) performance — match O(n) với route table dưới 100 entry thường nhanh hơn build/query precedence tree O(log n). Trade-off: developer chịu trách nhiệm đăng ký theo đúng priority (static trước parameter). Implication: đảo 2 dòng .route() có thể đổi behavior runtime mà compiler không phát hiện.
  2. Ba loại path segment trong axum 0.8 với độ specific giảm dần:
    • Static (literal): cú pháp /products/popular, match đúng chuỗi exact (case-sensitive, không có trailing slash khác, không có path đầy hơn). Vd /products/popular chỉ match request URL có path đúng /products/popular. Specific cao nhất.
    • Parameter :name: cú pháp /products/:slug, match bất kỳ chuỗi không chứa slash (1 segment), capture giá trị vào Path<T> extractor (lock B22). Vd match /products/phone-x (slug="phone-x"), /products/abc (slug="abc"), /products/popular (slug="popular" — chú ý), /products/123 (slug="123"). KHÔNG match /products/phone/x (slash trong slug → 2 segment) hoặc /products (thiếu segment). Specific trung bình.
    • Wildcard *rest: cú pháp /files/*rest, catch-all đến cuối path bao gồm slash (không giống parameter). Vd match /files/foo (rest="foo"), /files/foo/bar (rest="foo/bar"), /files/foo/bar/baz.jpg (rest="foo/bar/baz.jpg"). KHÔNG match /files (cần ít nhất 1 ký tự sau slash). Specific thấp nhất. Shop API decision lock B22: KHÔNG dùng wildcard cho data endpoint (phá vỡ semantic REST + dễ overlap); chỉ xuất hiện implicit qua tower-http::ServeDir mount cho static file ở B27.
  3. Request GET /products/popular match handler get_product (KHÔNG phải list_popular) — đây là BUG. Phân tích flow: axum quét route table top-down, dòng 1 /products/:slug: parameter :slug match bất kỳ chuỗi không chứa slash, "popular" thỏa → MATCH ngay với slug = "popular", invoke get_product. Dòng 2 list_popular KHÔNG bao giờ được gọi cho bất kỳ request nào. Đây là lỗi runtime chứ không phải compile-time: (a) cú pháp .route(...) chain hợp lệ cả 2 thứ tự, rustc compile pass không warning; (b) axum không có cơ chế báo "route X bị shadow bởi route Y" lúc khởi động; (c) chỉ phát hiện khi request đến — handler get_product chạy với Path("popular"), query DB SELECT * FROM products WHERE slug = 'popular' → trả 404 (sản phẩm slug "popular" không tồn tại) hoặc tệ hơn trả nhầm sản phẩm test trùng slug. Symptom debug: dev nhìn 404 đổ lỗi handler logic sai → debug query DB → bỏ qua gốc rễ routing. Fix: đảo thứ tự đăng ký — static /products/popular TRƯỚC parameter /products/:slug (lock B21).
  4. Multi-level nest KHÔNG thay đổi rule ordering — quy tắc first match wins áp dụng trong mỗi sub-router độc lập, không tự "phẳng hóa" qua nest. Khi đặt parameter trước static trong sub-router rồi nest, bug giống hệt như flat router: let products_router = Router::new().route("/:slug", get(get_product)).route("/popular", get(list_popular)); rồi Router::new().nest("/api/v1/products", products_router); — request GET /api/v1/products/popular flow: axum prefix-match /api/v1/products dispatch vào sub-router, trong sub-router quét top-down, :slug match "popular" → invoke get_product (BUG). Nest chỉ là cơ chế gắn prefix (axum concat prefix + sub_path khi build routing table), KHÔNG can thiệp thứ tự match trong sub-router. Implication design pattern: review ordering rule áp dụng từng sub-router độc lập, mỗi file routes/<name>.rs phải tự đảm bảo ordering đúng cho route khai báo trong scope của mình. Fix cùng quy tắc: static trước parameter ngay trong sub-router — Router::new().route("/popular", ...).route("/:slug", ...).
  5. Bug ordering silent runtime — hai cản trở debug: (a) compile-time KHÔNG detect, route khai báo runtime qua .route() chain trên Router builder, rustc không hiểu semantic ordering chỉ check signature handler, axum 0.8 không có lint tích hợp; (b) runtime silent, chỉ phát hiện khi request đến và handler sai chạy thay handler đúng, không exception không log error tự sinh, frontend báo "endpoint trả sai data" dev phải truy ngược về routing. Strategy phát hiện sớm 4 lớp: (1) Convention khi thêm route — list tất cả route trong sub-router theo priority static → parameter → wildcard trước khi đăng ký, đưa vào checklist code review; (2) Code review pull request — reviewer flag bất kỳ thay đổi nào trong routes/<name>.rs có đảo thứ tự, inline comment trong file nhắc convention; (3) Integration test MANDATORY — đây là biện pháp duy nhất tự động bắt ordering bug, viết test với CẢ static path (/products/popular) VÀ parameter path (/products/phone-x) cho mỗi static endpoint conflict tiềm năng, assert body shape đặc trưng của handler đúng (KHÔNG chỉ assert status 200 vì handler sai cũng có thể trả 200 với data lạc); (4) Log middleware verify runtime — tower-http::TraceLayer (G15) log structured field handler name qua span, chạy server local + curl test + đọc log xác nhận handler đúng dispatch (vd log list_popular: handling chứ không phải get_product: handling). Vì sao integration test bắt buộc: 3 lớp còn lại dựa human review/log đọc thủ công không scale với codebase 60 endpoint Shop API; integration test chạy mỗi CI build (G31 CI/CD), bắt regression khi developer mới refactor route table không biết quy tắc lock. Detail implementation full integration test với axum-test crate ở B253.
11

Bài Tiếp Theo

— chi tiết redirect với Redirect::to (301/302/307/308 phân biệt), serve static files với tower-http ServeDirServeFile, no_cache header cho static, áp dụng Shop API CDN strategy.