Danh sách bài viết

Bài 29: route_layer vs Layer — Khác Biệt

Bài 29 của series Rust RESTful API — phân biệt hai method Router::layer(layer) và Router::route_layer(layer) trong axum: .layer(layer) wrap toàn router qua tower::Service chain (lock pattern B20), áp dụng cho mọi route đã đăng ký cùng cả route thêm SAU method call — dùng cho global middleware như TraceLayer log structured request/response, CorsLayer handle preflight, CompressionLayer gzip body, RequestIdLayer sinh X-Request-Id (B39), TimeoutLayer abort handler quá 30s, RequestBodyLimitLayer cap body 2MB (B47); .route_layer(layer) ngược lại CHỈ áp dụng cho routes đã khai báo TRƯỚC method call — routes thêm SAU KHÔNG được wrap, đây là pitfall lớn nhất khi mixed với .route() mới — dùng cho selective middleware như RequireAuth scope subset endpoint (B112), RequireRole("admin") cho admin namespace (B135), RateLimitPerUser cho endpoint sensitive; pattern sub-router auth cleaner hơn .route_layer() mixed — tách public_router() / protected_router() / admin_router() rồi merge ở build_router() tránh nhầm lẫn route nào trong scope nào; ordering bottom-up theo Tower lock B20 — outer layer (last .layer() call trong chain) chạy ĐẦU request cuối response, recommended Shop API order G15+: Trace (outer) → RequestId → Cors → Timeout → BodyLimit → Compression (inner gần handler) — lý do Trace outer để log mọi request kể cả bị timeout hay body-limit reject; Shop API strategy G14+ vĩnh viễn: public catalog (/products, /categories) KHÔNG auth, protected (/cart, /checkout, /me, /orders) có RequireAuth::new() với JWT B112, admin (/admin/*) có RequireRole::new("admin") với Casbin B135; B29 conceptual + document layer strategy qua TODO comments trong router.rs cho G7 merge sub-router, G14 tách public/protected/admin, G15 add layer stack — KHÔNG add layer thực tế ở B29.

14/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ẽ:

  • Phân biệt .layer(layer).route_layer(layer) trong Router — hai method tưởng giống nhưng scope khác hoàn toàn.
  • Hiểu .layer apply cho mọi route đã đăng ký cùng cả routes thêm SAU method call — wrap toàn router qua tower service chain.
  • Hiểu .route_layer apply CHỈ cho routes đã khai báo TRƯỚC method call — routes thêm sau KHÔNG được wrap.
  • Áp dụng pattern: global TraceLayer qua .layer() + selective RequireAuth qua .route_layer() hoặc sub-router.
  • Biết ordering layer bottom-up — outer layer (last .layer() call) chạy đầu request, cuối response.
  • Tránh pitfall lớn nhất: .route_layer() KHÔNG áp dụng cho route thêm sau — quên dễ leak endpoint qua auth.
  • Lock Shop API strategy G14+: public catalog không auth, protected (cart/checkout/me/orders) RequireAuth, admin RequireRole("admin").
  • Document layer strategy qua TODO comments trong router.rs — chuẩn bị cho G7 merge sub-router, G14 tách scope, G15 add layer stack thực tế.
2

Layer Trong Tower Recap

Recap B20 (Hệ Sinh Thái Axum) đã lock: axum Router implement tower::Service<Request<Body>, Response = Response<Body>>; trait tower::Layer<S> compose middleware qua method fn layer(&self, inner: S) -> Self::Service wrap service. Mỗi middleware (lớp trung gian xử lý request/response) là một Layer wrap router phía dưới.

Use case middleware phổ biến: log structured request/response (TraceLayer), CORS preflight (CorsLayer), authentication (RequireAuth custom), rate-limit (RateLimitLayer), compression (CompressionLayer), request ID propagation (SetRequestIdLayer), timeout (TimeoutLayer), body size limit (RequestBodyLimitLayer).

Pattern apply tổng quát của tower: outer-in cho request (request đi qua từng layer từ ngoài vào trong rồi tới handler), inner-out cho response (response đi từ handler ra ngoài qua từng layer ngược chiều). Cụ thể với TraceLayer wrap handler:

// Conceptual — chưa apply Shop API ở B29
use axum::{Router, routing::get};
use tower_http::trace::TraceLayer;

let app = Router::new()
    .route("/products", get(list_products))
    .layer(TraceLayer::new_for_http());  // wrap toàn handler

// Request lifecycle:
//   Request → TraceLayer (log "started") → handler → TraceLayer (log "done", latency) → Response

Router trong axum cung cấp HAI method khác nhau để apply layer: .layer(layer).route_layer(layer). Tên gần giống nhưng scope hoàn toàn khác — đây là pitfall lớn cho dev mới và là chủ đề chính của bài này.

3

.layer(layer) — Áp Dụng Cho TẤT CẢ Routes

Method Router::layer(layer) wrap toàn router qua một tower::Layer. Apply cho:

  • Mọi route đã đăng ký TRƯỚC .layer() call (route đã có trong router instance lúc gọi).
  • Cả route thêm SAU .layer() call qua .route(), .merge(), .nest() — đây là điểm phân biệt then chốt với .route_layer.
  • Fallback handler (404, 405) tự động được wrap luôn — TraceLayer log cả request 404.

Use case điển hình: global middleware cần áp dụng cho mọi endpoint không trừ ai — log truy cập, CORS preflight, request ID propagation, body size limit, compression response.

// Conceptual — Shop API sẽ apply ở G15+
use axum::{Router, routing::get};
use tower_http::{
    trace::TraceLayer,
    cors::CorsLayer,
    compression::CompressionLayer,
};

let app = Router::new()
    .merge(routes::products::routes())
    .merge(routes::health::routes())
    .layer(TraceLayer::new_for_http())   // ← apply mọi route + fallback
    .layer(CorsLayer::permissive())      // ← apply mọi route + fallback
    .layer(CompressionLayer::new());     // ← apply mọi route + fallback

Thêm route SAU .layer() vẫn được wrap automatic:

let app = Router::new()
    .route("/products", get(list_products))
    .layer(TraceLayer::new_for_http())   // ← apply /products
    .route("/health", get(health));      // ← /health VẪN có TraceLayer

// Cả /products và /health đều được TraceLayer wrap

Pattern lock: dùng .layer() cho mọi middleware mang tính cross-cutting concern không phân biệt resource. Shop API G15 sẽ wire stack .layer(TraceLayer).layer(SetRequestIdLayer).layer(CorsLayer).layer(TimeoutLayer).layer(RequestBodyLimitLayer).layer(CompressionLayer)build_router() sau khi tất cả route đã merge xong.

4

.route_layer(layer) — Áp Dụng CHỈ Routes Đã Khai Báo

Method Router::route_layer(layer) ngược lại với .layer(): CHỈ apply cho routes đã được khai báo TRƯỚC thời điểm gọi .route_layer(). Routes thêm SAU method call KHÔNG được wrap.

Use case chính: áp dụng auth/role middleware cho một subset endpoint cụ thể, để lại các endpoint public bên ngoài scope đó.

// Conceptual — preview G14 (B135 Casbin authorization)
use axum::{Router, routing::{get, post}};

let app = Router::new()
    // Public routes — không cần auth
    .route("/products", get(list_products))
    .route("/products/:slug", get(get_product))
    // Protected routes — cần auth
    .route("/cart", get(get_cart))
    .route("/checkout", post(checkout))
    .route_layer(RequireAuth::new())     // ← CHỈ wrap 4 routes phía trên
    // Public routes thêm SAU — KHÔNG bị wrap auth
    .route("/health", get(health));      // ← KHÔNG có RequireAuth

Điểm cốt lõi cần ghi nhớ: .route_layer() "đóng băng" scope tại thời điểm method được gọi. Nếu sau đó bạn thêm .route("/admin/users", ...) và quên đặt nó vào sub-router protected riêng, route mới đó sẽ rò khỏi RequireAuth — security hole nguy hiểm.

┌─────────────────────────────────────────────────────────────┐
│  Pitfall: route thêm SAU .route_layer KHÔNG bị wrap         │
│  ─────────────────────────────────────────────────          │
│  Router::new()                                              │
│      .route("/cart", get(get_cart))         ┐               │
│      .route("/checkout", post(checkout))    ├ trong scope   │
│      .route_layer(RequireAuth::new())       ┘ auth wrap     │
│      .route("/admin/secret", get(secret))   ← LEAK auth     │
│                                                             │
│  → curl /admin/secret thành công KHÔNG cần token            │
└─────────────────────────────────────────────────────────────┘

Pattern lock B29: KHÔNG mixed .route().route_layer() trong cùng Router nếu có thể tránh — luôn tách thành sub-router riêng (xem Bước 7) để compiler kiểm tra rõ ràng route nào trong scope nào, code review tránh sót.

5

Decision: .layer vs .route_layer Khi Nào Dùng?

Decision matrix lock cho Shop API:

Loại middleware       │ Method dùng        │ Use case Shop API
──────────────────────┼────────────────────┼──────────────────────────
TraceLayer (log)      │ .layer() global    │ G15 (B148) — log mọi req
CorsLayer (CORS)      │ .layer() global    │ G15 (B158) — preflight
CompressionLayer      │ .layer() global    │ G15 (B48) — gzip body
SetRequestIdLayer     │ .layer() global    │ G15 (B39) — X-Request-Id
TimeoutLayer          │ .layer() global    │ G15 — abort handler 30s+
RequestBodyLimitLayer │ .layer() global    │ G15 (B47) — cap 2MB
RequireAuth           │ .route_layer()     │ G14 (B112) — JWT verify
                      │ hoặc sub-router    │  scope protected/admin
RequireRole("admin")  │ .route_layer()     │ G14 (B135) — Casbin
                      │ hoặc sub-router    │  scope admin/*
RateLimitPerUser      │ .route_layer()     │ G17 — endpoint sensitive
                      │ hoặc sub-router    │  (login, password reset)

Global middleware (.layer()): áp dụng cross-cutting concern không phân biệt resource — observability, security headers, body limit, compression. Wire một lần ở build_router() cuối chain, mọi route mới tự inherit.

Selective middleware (.route_layer() hoặc sub-router): áp dụng cho scope cụ thể — auth, authorization, rate limit cho endpoint sensitive. Quyết định scope ở cấp router structure để compiler enforce.

Alternative cleaner: tạo sub-router với layer riêng rồi nest/merge vào root router — pattern lock Shop API B29 (xem Bước 7). Cleaner hơn mixed .route_layer() vì:

  • Compiler enforce scope qua signature — sub-router protected_router() trả Router<AppState> đã wrap, code review thấy ngay.
  • Test sub-router riêng dễ — mount mock state, test endpoint trong scope độc lập.
  • Tránh pitfall route thêm sau .route_layer() bị leak — route mới phải thuộc về một sub-router rõ ràng.

Shop API strategy lock G14+ (vĩnh viễn cho mọi resource tương lai):

  • Public: /products, /products/:slug, /categories, /products/:slug/reviews (read-only) — KHÔNG auth, ai cũng gọi được.
  • Protected: /cart, /cart/items, /checkout, /me, /me/addresses, /ordersRequireAuth::new() verify JWT (B112), inject Extension<CurrentUser> cho handler.
  • Admin: /admin/products, /admin/categories, /admin/orders, /admin/inventory/:id/restockRequireRole::new("admin") qua Casbin enforcer (B135), reject 403 nếu role không khớp.
6

Layer Ordering Bottom-Up

Lock pattern Tower middleware từ B20: layer apply bottom-up trong code, tương ứng outer-first lúc runtime. Layer ở dòng .layer() CUỐI cùng trong builder chain là layer OUTER nhất — chạy đầu tiên khi request đến, cuối cùng khi response đi ra.

// Conceptual — pattern lock G15+
let app = Router::new()
    .merge(routes::products::routes())
    .layer(InnerLayer)    // ← gần handler nhất (chạy sau cùng request)
    .layer(MiddleLayer)
    .layer(OuterLayer);   // ← ngoài cùng (chạy đầu tiên request)

// Request flow:
//   incoming → OuterLayer → MiddleLayer → InnerLayer → handler
// Response flow:
//   handler → InnerLayer → MiddleLayer → OuterLayer → outgoing

Diagram cụ thể với 3 layer:

Request                                          Response
   │                                                  ▲
   ▼                                                  │
┌──────────────────────────────────────────────────────┐
│ OuterLayer (Trace — log "started"/"done" + latency) │
│ ┌───────────────────────────────────────────────┐   │
│ │ MiddleLayer (Cors — preflight check)          │   │
│ │ ┌───────────────────────────────────────────┐ │   │
│ │ │ InnerLayer (Compression — gzip response)  │ │   │
│ │ │ ┌─────────────────────────────────────┐   │ │   │
│ │ │ │ handler async fn business logic     │   │ │   │
│ │ │ └─────────────────────────────────────┘   │ │   │
│ │ └───────────────────────────────────────────┘ │   │
│ └───────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────┘

Shop API recommended order G15+ (từ outer xuống inner — tức từ .layer() cuối chain ngược lên):

// File: crates/shop-api/src/router.rs (G15 sẽ apply)
.layer(CompressionLayer::new())              // inner — gần handler nhất
.layer(RequestBodyLimitLayer::new(2 * 1024 * 1024))
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.layer(CorsLayer::permissive())
.layer(SetRequestIdLayer::x_request_id(MakeRequestUuid))
.layer(TraceLayer::new_for_http())           // outer — chạy đầu request

Lý do Trace outer nhất: log mọi request kể cả request bị reject sớm bởi TimeoutLayer (408), RequestBodyLimitLayer (413), hay CorsLayer (403 cross-origin reject). Nếu đặt Trace inner thì request bị reject ở outer sẽ KHÔNG log → observability gap, không debug được "tại sao client kêu 413 mà server không thấy?".

Lý do Compression inner nhất: nén response ngay sau khi handler tạo body, trước khi đi qua các layer khác — minimize work cho layer outer. Đặt Compression outer thì response phải đi qua TraceLayer/CorsLayer ở dạng plain (uncompressed) trước khi nén, không sai logic nhưng waste CPU log byte plain.

Lý do RequestId sớm (gần outer): các layer sau (Trace, Cors, Timeout) cần X-Request-Id trong tracing span — sinh ID càng sớm càng tốt để mọi log line cùng request gắn cùng ID. Đặt sau Trace thì span tracing tạo trước khi có ID → ID không vào được log.

7

Pattern Sub-Router Auth — Cleaner Hơn route_layer

Thay vì mixed .route_layer() với .route() public trong cùng Router (dễ leak auth khi thêm route mới), pattern lock Shop API B29 tách rõ 3 sub-router theo scope auth, mỗi sub-router tự .route_layer() middleware tương ứng, rồi merge vào build_router():

// File: crates/shop-api/src/router.rs (preview G14)
use axum::{Router, routing::get};
use crate::{routes, state::AppState};

fn public_router() -> Router<AppState> {
    Router::new()
        .merge(routes::products::routes())    // catalog read
        .merge(routes::categories::routes())  // category tree
    // KHÔNG có .route_layer — public mở
}

fn protected_router() -> Router<AppState> {
    Router::new()
        .merge(routes::cart::routes())
        .merge(routes::orders::routes())
        .merge(routes::me::routes())
        .route_layer(middleware::RequireAuth::new())  // ← scope auth
}

fn admin_router() -> Router<AppState> {
    Router::new()
        .merge(routes::admin_products::routes())
        .merge(routes::admin_orders::routes())
        .route_layer(middleware::RequireRole::new("admin"))  // ← scope role
}

pub fn build_router(state: AppState) -> Router {
    let api_v1 = Router::new()
        .merge(public_router())
        .merge(protected_router())
        .merge(admin_router());

    Router::new()
        .merge(routes::health::routes())   // infrastructure không auth
        .merge(routes::version::routes())
        .nest("/api/v1", api_v1)
        .fallback(handlers::fallback::not_found)
        .method_not_allowed_fallback(handlers::fallback::method_not_allowed)
        // G15+: layer stack global
        // .layer(CompressionLayer::new())
        // .layer(TimeoutLayer::new(Duration::from_secs(30)))
        // .layer(CorsLayer::permissive())
        // .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid))
        // .layer(TraceLayer::new_for_http())
        .with_state(state)
}

Ưu điểm so với mixed .route_layer() trong một router phẳng:

  • Scope rõ ràng qua signature — function name protected_router() đã nói rõ scope; reviewer thấy ngay route nào trong scope nào không cần đọc kỹ vị trí .route_layer().
  • Tránh pitfall route mới leak — thêm endpoint mới phải chọn rõ public_router(), protected_router(), hay admin_router(), không thể "vô tình" thêm sau .route_layer() mà thoát wrap.
  • Test sub-router riêng dễ — unit test protected_router() mount với mock state + mock auth, verify auth được apply mà không phải build cả app.
  • Refactor scope dễ — chuyển endpoint từ public → protected chỉ là di chuyển 1 dòng .merge() giữa hai function, không tracking thứ tự .route_layer() trong builder chain dài.
  • Match với code organization theo resource — file routes/cart.rs, routes/orders.rs, routes/me.rs tự nó là protected (chỉ user logged-in mới truy cập); group ở cấp protected_router() phản ánh đúng domain.

Pattern lock Shop API G14 vĩnh viễn: 3 sub-router factory function public_router() / protected_router() / admin_router() đặt trong router.rs, mỗi function merge các sub-router resource tương ứng + route_layer middleware đúng scope.

8

Apply Vào shop-api — Document Layer Strategy

Trạng thái Shop API hiện tại (sau B28): build_router() trong crates/shop-api/src/router.rs đã có nest("/api/v1", api_v1) (B24) + fallback/method_not_allowed_fallback (B25) + with_state(state) (B17) — CHƯA có layer nào. Folder crates/shop-api/src/middleware/ mới chỉ có mod.rs placeholder từ B17.

B29 KHÔNG add layer thực tế. Lý do: TraceLayer cần tracing span config đầy đủ (G15 Observability), RequireAuth cần JWT verify (B112), RequireRole cần Casbin enforcer (B135) — chưa có dependencies này. Add layer giả lập ở B29 sẽ phải refactor lại ở G14/G15.

Action B29: document layer strategy qua TODO comments trong router.rs để khi đến G7/G14/G15 dev biết chính xác điểm cần extend, không phải đọc lại spec series. Cụ thể update file:

// File: crates/shop-api/src/router.rs
use axum::{routing::get, Router, http::StatusCode};
use crate::{handlers, routes, state::AppState};

pub fn build_router(state: AppState) -> Router {
    let api_v1 = Router::new()
        .merge(routes::products::routes());
        // TODO G7 (B62-B70): merge thêm sub-router resource
        //   .merge(routes::categories::routes())
        //   .merge(routes::cart::routes())
        //   .merge(routes::orders::routes())
        //   .merge(routes::me::routes())
        //
        // TODO G14 (B135): tách thành 3 sub-router theo auth scope
        //   public_router()    — catalog, categories (KHÔNG auth)
        //   protected_router() — cart, checkout, me, orders
        //     route_layer(RequireAuth::new())  // JWT verify B112
        //   admin_router()     — /admin/*
        //     route_layer(RequireRole::new("admin"))  // Casbin B135

    Router::new()
        .route("/", get(root))
        .merge(routes::health::routes())
        .merge(routes::version::routes())
        .merge(routes::demo_error::routes())
        .merge(routes::demo_async::routes())
        .nest("/api/v1", api_v1)
        .fallback(handlers::fallback::not_found)
        .method_not_allowed_fallback(handlers::fallback::method_not_allowed)
        // TODO G15 (B148+): global middleware stack — apply bottom-up,
        // outer last call chạy đầu request:
        //   .layer(CompressionLayer::new())                          // inner
        //   .layer(RequestBodyLimitLayer::new(2 * 1024 * 1024))      // B47
        //   .layer(TimeoutLayer::new(Duration::from_secs(30)))
        //   .layer(CorsLayer::permissive())                          // B158
        //   .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) // B39
        //   .layer(TraceLayer::new_for_http())                       // outer
        .with_state(state)
}

async fn root() -> (StatusCode, &'static str) {
    (StatusCode::OK, "shop-api v0.1.0")
}

3 cluster TODO marker cho 3 milestone tương lai rõ ràng — dev không phải nhớ spec, đọc router.rs là biết:

  • TODO G7: extend sub-router resource (orders, cart, me, categories) khi CRUD đầu tiên implement (B62-B70).
  • TODO G14: refactor thành 3 sub-router scope (public/protected/admin) khi Casbin authorization apply (B135).
  • TODO G15: wire layer stack global khi Observability + Hardening implement (B148+).

Verify behavior KHÔNG đổi sau B29 — chỉ comment thêm:

cargo run -p shop-api

# Terminal khác:
curl http://localhost:3000/health
# {"status":"ok"}

curl http://localhost:3000/api/v1/products
# {"items":[],"total":0,"page":1,"size":20,"hasNext":false}

curl -i http://localhost:3000/error/not-found
# HTTP/1.1 404 Not Found
# content-type: application/json; charset=utf-8
# {"error":"not found: ...","code":"NOT_FOUND","request_id":null}

Suggested commit: B29: document layer strategy qua TODO comments trong router.rs (G7/G14/G15). KHÔNG sửa file khác — middleware/mod.rs placeholder vẫn empty đến G15.

9

Tổng Kết

  • Router::layer(layer) apply cho mọi route — cả route đã đăng ký trước method call lẫn route thêm SAU qua .route()/.merge()/.nest(), fallback handler cũng được wrap.
  • Router::route_layer(layer) CHỈ apply cho routes khai báo TRƯỚC method call — routes thêm SAU KHÔNG được wrap, đây là pitfall lớn nhất khi mixed với .route() mới.
  • Global layer qua .layer(): TraceLayer, CorsLayer, SetRequestIdLayer, TimeoutLayer, RequestBodyLimitLayer, CompressionLayer — wire ở build_router() cuối chain.
  • Selective layer qua .route_layer() hoặc sub-router: RequireAuth, RequireRole("admin"), RateLimitPerUser — chọn scope ở cấp router structure.
  • Ordering bottom-up (lock B20): layer ở dòng .layer() CUỐI chain là outer nhất — chạy đầu request, cuối response. Shop API order G15+: Compression (inner) → BodyLimit → Timeout → Cors → RequestId → Trace (outer).
  • Lý do Trace outer: log mọi request kể cả bị reject sớm bởi Timeout/BodyLimit/Cors — observability gap nếu Trace inner.
  • Pattern sub-router auth cleaner hơn mixed .route_layer(): tách public_router() / protected_router() / admin_router() rồi mergebuild_router() — compiler enforce scope, tránh leak route mới.
  • Shop API strategy G14+: public catalog (/products, /categories) KHÔNG auth; protected (/cart, /checkout, /me, /orders) có RequireAuth với JWT B112; admin (/admin/*) có RequireRole("admin") với Casbin B135.
  • B29 conceptual + document strategy qua TODO comments trong router.rs cho G7 (merge sub-router resource), G14 (tách public/protected/admin), G15 (wire layer stack thực tế); KHÔNG add layer ở B29 vì dependencies (tracing config, JWT, Casbin) chưa có.
  • Folder crates/shop-api/src/middleware/ placeholder từ B17 vẫn empty đến G15 — sẽ populate RequireAuth, RequireRole, RequestIdLayer custom theo Tower Service + Layer pattern.
10

Bài Tập Củng Cố

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

  1. Phân biệt .layer(layer).route_layer(layer). Mỗi method apply cho routes nào? Route thêm SAU method call có được wrap không?
  2. .layer(L1).layer(L2) — layer L1 hay L2 chạy đầu request? Vì sao theo Tower convention?
  3. Sub-router protected_router().route_layer(RequireAuth). Nếu sau khi merge protected_router() vào root router, root router thêm .route("/secret", ...), route /secret có bị RequireAuth wrap không?
  4. Khi nào nên dùng pattern sub-router (public_router() / protected_router() / admin_router()) thay vì mixed .route_layer() trong cùng một Router?
  5. Shop API global layer ordering G15: Trace là outer nhất, Compression là inner nhất. Vì sao Trace outer chứ không phải Compression outer?
Đáp án
  1. .layer(layer) wrap toàn router qua tower service chain — apply cho mọi route đã đăng ký TRƯỚC method call lẫn route thêm SAU qua .route()/.merge()/.nest() + fallback handler. Vd: Router::new().route("/a", ...).layer(TraceLayer).route("/b", ...) — cả /a/b đều được TraceLayer wrap. .route_layer(layer) ngược lại CHỈ apply cho routes đã khai báo TRƯỚC method call — routes thêm SAU KHÔNG được wrap. Vd: Router::new().route("/a", ...).route_layer(RequireAuth).route("/b", ...) — CHỈ /a bị RequireAuth wrap, /b public. Đây là pitfall lớn nhất khi mixed .route() với .route_layer(): thêm route mới sau .route_layer() dễ leak khỏi scope auth, security hole. Cách tránh: tách thành sub-router riêng (pattern sub-router auth lock B29).
  2. L2 chạy đầu request (outer nhất), L1 chạy gần handler nhất. Tower convention: .layer() apply bottom-up trong code, tương ứng outer-first lúc runtime — layer ở dòng cuối chain là outer nhất, wrap mọi layer phía trên cộng router phía dưới. Lý do: method Router::layer(layer) nhận layer làm tham số rồi return router mới wrap layer đó OUTER hơn router hiện tại, mỗi lần gọi tiếp tục wrap thêm 1 lớp ngoài. Vd: router.layer(L1).layer(L2) nghĩa là first wrap router với L1 (L1 outer hơn router), rồi wrap kết quả với L2 (L2 outer hơn L1+router). Request flow: incoming → L2 → L1 → router → handler. Response flow: handler → router → L1 → L2 → outgoing. Hệ quả: muốn middleware chạy SỚM nhất khi request đến (vd log structured trước mọi thứ khác) thì đặt nó ở dòng .layer() CUỐI chain.
  3. Route /secret KHÔNG bị RequireAuth wrap. Lý do: .route_layer() "đóng băng" scope tại thời điểm gọi trên protected_router() — wrap chỉ áp dụng cho các route đã merge vào protected_router() trước khi gọi .route_layer(). Khi protected_router() được merge vào root router, axum compose service tree giữ nguyên scope wrap đó — sub-tree protected_router() vẫn có RequireAuth, nhưng phần root router bên ngoài (bao gồm /secret mới thêm) KHÔNG bị wrap. Đây chính là điểm mạnh của pattern sub-router auth: scope wrap được giữ độc lập per sub-router, root router không "lây" wrap sang phần khác. Pitfall ngược cần tránh: nếu bạn gọi .route_layer(RequireAuth) Ở ROOT ROUTER sau khi merge protected_router(), thì RequireAuth apply cho mọi route đã có lúc đó (cả public lẫn protected lúc đó — protected bị double-wrap dư thừa). Conclusion: .route_layer() nên gọi bên trong sub-router factory function (như pattern lock B29), KHÔNG gọi ở root router sau merge.
  4. Pattern sub-router preferred trong 4 case: (a) Có nhiều scope auth khác nhau — Shop API có 3 scope (public, protected, admin), mỗi scope cần middleware khác (none, RequireAuth, RequireRole) — tách sub-router clearer hơn mixed 2-3 .route_layer() trong cùng router phẳng. (b) Route count lớn (>5-10 route mỗi scope) — mixed .route_layer() với nhiều route khó scan vị trí .route_layer() đặt ở đâu, dễ bị lẫn; sub-router function tách rõ scope qua signature fn protected_router() -> Router<AppState>. (c) Cần test scope riêng — sub-router function có thể test độc lập: mount với mock state + mock auth, verify scope hoạt động đúng mà không cần build full app + tất cả layer global. (d) Team multi-dev contribute — dev thêm endpoint mới phải chọn rõ public_router()/protected_router()/admin_router() qua function call .merge(), compiler không cho thêm route "vô tình" lệch scope; code review nhanh hơn nhiều so với check vị trí .route_layer() trong builder chain dài. Mixed .route_layer() chỉ dùng cho case đơn giản 2-3 route cùng scope, không có resource khác (vd CLI tool internal). Shop API là production e-commerce → pattern sub-router lock vĩnh viễn G14+.
  5. Trace outer vì 3 lý do critical observability: (a) Log mọi request, kể cả request bị reject sớm — nếu request bị TimeoutLayer abort (408), RequestBodyLimitLayer reject (413), hay CorsLayer block (403 cross-origin), Trace outer vẫn log đầy đủ method/path/status/latency. Đặt Trace inner thì request bị reject ở layer outer KHÔNG đi qua Trace → server "không thấy" request đó, không debug được "tại sao client kêu 413 mà metrics không có?". (b) Tracing span phải bao trùm toàn lifecycle request — span tạo ở TraceLayer wrap mọi inner layer; metric latency đo từ lúc request đến đến lúc response đi ra, bao gồm cả thời gian Compression nén body. Trace inner thì span miss phần outer → latency report thiếu chính xác. (c) X-Request-Id propagate vào tracing spanSetRequestIdLayer đặt ngay dưới Trace (outer-1) sinh ID sớm, Trace span tự inject ID vào field %request_id qua tracing-opentelemetry integration; mọi log line trong cùng request gắn cùng ID → grep log dễ. Đặt RequestId ngoài Trace thì span tạo trước khi có ID → ID không vào field span được, log line trong span không có request_id. Compression inner nhất vì nén response ngay sau handler tạo body, minimize work cho layer outer (Trace không phải log byte plain dài, Cors không phải xử lý body lớn). Đặt Compression outer cũng work nhưng waste CPU + memory (response phải qua mọi layer ở dạng uncompressed trước khi nén).
11

Bài Tiếp Theo

— bài cuối Group 3 Routing Cơ Bản: chi tiết URL versioning /api/v1 vs /api/v2, header versioning qua Accept: application/vnd.shop.api+json;version=1, query versioning ?v=1, decision matrix cho production API, áp dụng Shop API URL versioning (đã apply B24 qua nest("/api/v1", api_v1)).