Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu
Router::nest(prefix, sub_router)mount sub-router dưới prefix, path bên trong sub-router là relative — axum auto concatprefix + sub_pathkhi dispatch. - Phân biệt
Router::merge(other)(cùng level, KHÔNG thêm prefix — sub-route giữ nguyên path) vsRouter::nest(prefix, other)(mount dưới prefix — sub-route được prepend prefix). - Áp dụng multi-level nest cho admin/public split (ví dụ
/api/v1/admin/productsqua hai lớp nest). - Hiểu tradeoff nested vs flat path: nested tập trung prefix 1 chỗ dễ đổi version, flat rõ ràng từng route nhưng lặp prefix.
- Refactor Shop API:
routes/products.rsdùng path relative/products/...,router.rsnest/api/v1aggregate gọn. - Biết pitfall fallback handler trong nested router — KHÔNG inherit từ parent; Shop API quyết định 1 fallback top-level (B25 deep dive).
Router::nest(prefix, sub_router) Cơ Bản
Router::nest(prefix, sub_router) là phương thức mount (gắn) một sub-router xuống dưới một path prefix cho trước. Mọi route trong sub-router sẽ được dispatch khi URL đầy đủ là prefix + sub_path tương ứng.
Quy tắc cốt lõi cần nhớ: path khai báo trong sub-router là relative, KHÔNG bao gồm prefix. axum tự ghép prefix với path con khi build routing table — handler chỉ cần biết đường đi nội bộ của domain mình, không cần biết version path hay namespace bên ngoài.
use axum::{Router, routing::{get, post}};
async fn list_products() -> &'static str { "list products" }
async fn create_product() -> &'static str { "create product" }
async fn get_product() -> &'static str { "get product" }
// Sub-router — path bên trong là RELATIVE, không có prefix /api/v1
let products_router = Router::new()
.route("/", get(list_products).post(create_product))
.route("/:slug", get(get_product));
// Mount dưới prefix /api/v1/products
let app = Router::new().nest("/api/v1/products", products_router);
// Effective routes (URL client gọi):
// GET /api/v1/products
// POST /api/v1/products
// GET /api/v1/products/:slug
Quan sát ba điểm:
- Trong
products_router, path"/"không có ý nghĩa "root của app" — nó nghĩa là "root của sub-router" (sau khi mount sẽ thành chính prefix/api/v1/products). - Path
"/:slug"mount xuống thành/api/v1/products/:slug— axum nối prefix với sub-path tự động, dynamic segment:sluggiữ nguyên semantic Path extractor (B22). - Handler
list_productstrong sub-router KHÔNG cần biết prefix bên ngoài là/api/v1/products— nó chỉ biết bản thân được mount ở"/". Đổi prefix sang/api/v2/productshay/internal/productskhông cần sửa handler hay sub-router.
Đây là tính chất modular quan trọng nhất của nest: tách bạch route declaration khỏi mount point. Sub-router là một unit độc lập, reusable, testable riêng — có thể mount nhiều lần ở các prefix khác nhau nếu cần.
merge vs nest — Khác Biệt
Trước B24, Shop API dùng Router::merge(other) ở router.rs aggregate sub-router (lock B17, B18, B21). Đến B24 thêm Router::nest(prefix, other). Hai method khác biệt rõ ràng cần phân biệt để dùng đúng tình huống.
Router::merge(other) gộp hai router ở cùng level path, KHÔNG thêm prefix. Mọi route trong other giữ nguyên path khai báo gốc, được dispatch như thể đã khai báo trực tiếp trong app chính.
Router::nest(prefix, other) mount router con dưới prefix, mọi route trong other được prepend prefix lúc dispatch.
Operation | sub_router có route | App effective route
---------------------------+----------------------+----------------------
.merge(sub) | "/api/v1/products" | "/api/v1/products"
.merge(sub) | "/" | "/"
.nest("/api/v1", sub) | "/products" | "/api/v1/products"
.nest("/api/v1", sub) | "/" | "/api/v1"
.nest("/api/v1/products", sub) | "/" | "/api/v1/products"
.nest("/api/v1/products", sub) | "/:slug" | "/api/v1/products/:slug"
Ví dụ minh họa cùng sub-router nhưng hai cách compose khác nhau:
use axum::{Router, routing::get};
async fn list_products() -> &'static str { "list products" }
// Approach A: merge — sub-router khai báo full path
let sub_a = Router::new().route("/api/v1/products", get(list_products));
let app_a = Router::new().merge(sub_a);
// Effective: GET /api/v1/products
// Approach B: nest — sub-router khai báo path relative
let sub_b = Router::new().route("/products", get(list_products));
let app_b = Router::new().nest("/api/v1", sub_b);
// Effective: GET /api/v1/products (cùng kết quả với A)
Hai approach trên cho cùng URL effective /api/v1/products, nhưng nội dung sub-router khác nhau: Approach A lặp prefix trong từng route, Approach B chỉ khai báo path domain — prefix tập trung ở nest().
Khi nào dùng cái nào:
merge: gộp các service top-level KHÔNG liên quan nhau, không chia sẻ prefix chung. Ví dụ Shop API root có 3 service riêng —/health(infra),/version(build metadata),/metrics(Prometheus G15). Mỗi service tự khai báo path đầy đủ.nest: tách module theo prefix organizational. Ví dụ/api/v1/*cho REST API public,/admin/*cho admin dashboard,/webhooks/*cho integration partner. Mỗi prefix gom một bundle route logic liên quan.
Multi-Level Nest
Sub-router được mount qua nest bản thân vẫn là một Router bình thường — có thể chứa nest bên trong nó. Pattern này gọi là multi-level nest, dùng cho cấu trúc URL phân tầng admin/public.
use axum::{Router, routing::get};
async fn admin_list_products() -> &'static str { "admin list" }
async fn public_list_products() -> &'static str { "public list" }
// Sub-router admin — path relative trong namespace admin
let admin_router = Router::new()
.route("/products", get(admin_list_products));
// Sub-router public products
let products_router = Router::new()
.route("/", get(public_list_products));
// Sub-router v1 — gom admin + public lại trong namespace /api/v1
let v1_router = Router::new()
.nest("/admin", admin_router)
.nest("/products", products_router);
// App root — mount v1 dưới /api/v1
let app = Router::new().nest("/api/v1", v1_router);
// Effective routes:
// GET /api/v1/admin/products → admin_list_products
// GET /api/v1/products → public_list_products
Hai lớp nest tách bạch concerns: /api/v1 là versioning boundary (đổi sang v2 chỉ sửa 1 dòng), /admin là role boundary (sau này thêm middleware require_role("admin") qua admin_router.layer(...) chỉ áp cho admin route, không ảnh hưởng public — B29 deep dive route_layer).
Cấu trúc cây route sau multi-level nest:
app (root)
└── /api/v1 ← nest level 1 (versioning)
├── /admin ← nest level 2 (role)
│ └── /products ← route in admin_router
└── /products ← nest level 2 (resource)
└── / ← route in products_router
(effective: /api/v1/products)
Khuyến nghị: tối đa 2-3 level cho readability. Quá nhiều level nest khiến trace URL từ raw code phải lookup chain dài, mất tính rõ ràng. Shop API hiện chỉ cần 1 level (/api/v1) ở B24, sẽ thêm level 2 (/admin) khi đến G14 RBAC (Bài 131+).
Tradeoff Nested vs Flat Path
Hai approach compose routing là flat path (mỗi route khai báo path đầy đủ, dùng merge ghép) và nested (path relative trong sub-router, dùng nest mount với prefix).
Flat path:
- Pros: rõ ràng tuyệt đối — nhìn 1 file biết URL đầy đủ, không cần trace nest chain. IDE jump-to-definition trỏ thẳng handler.
- Cons: lặp prefix
/api/v1/ở mỗi.route()call. Đổi version từv1sangv2phải search-replace hàng chục file resource, dễ sót.
Nested:
- Pros: prefix tập trung 1 chỗ (
router.rs) — đổiv1→v2sửa 1 dòng. Sub-router reusable, mount nhiều prefix nếu cần (vd mount cùng router dưới/api/v1và/api/v2để hỗ trợ song song). - Cons: trace URL từ raw code phải lookup chain — đọc
routes/products.rsthấy/productsnhưng URL effective phụ thuộcrouter.rsnest ở đâu. IDE-friendly (search reference) nhưng grep raw text phức tạp hơn.
Shop API decision lock B24: dùng hybrid — nest cho organizational prefix (/api/v1, /admin, /webhooks tương lai), flat trong sub-router cho resource path (/products, /products/:slug, /products/popular). Lý do:
- Versioning (
v1,v2) là dimension tách bạch độc lập với domain — đặt ở 1 chỗ duy nhất giúp version bump không lan tỏa. - Resource path (
/products,/products/:slug) trong cùng sub-router KHÔNG nest sâu thêm vì khiến đọc handler khó:nest("/products", Router::new().route("/", ...).route("/:slug", ...))2 lớp khá lắt léo cho 2-3 route resource thông thường, flatroute("/products", ...).route("/products/:slug", ...)trực quan hơn.
Quy tắc thực hành: nest cho boundary cross-cutting (version, role, integration); flat cho resource path trong cùng domain.
Fallback Handler Trong Nested Router
Router::fallback(handler) đăng ký handler dự phòng chạy khi không route nào match URL request. Mặc định, axum trả 404 với body plain text "Not found" — fallback cho phép custom envelope chuẩn project.
Pitfall quan trọng: fallback trong sub-router (mount qua nest) hoạt động tách biệt với fallback của parent router — KHÔNG inherit theo cây. Nếu sub-router có fallback riêng, request không match route nào trong sub-router (nhưng vẫn nằm dưới prefix) sẽ rơi vào fallback của sub-router, KHÔNG bubble lên parent.
use axum::{Router, routing::get, http::StatusCode};
async fn list_products() -> &'static str { "list" }
let products_router = Router::new()
.route("/", get(list_products))
.fallback(|| async {
(StatusCode::NOT_FOUND, "no product matches")
});
let app = Router::new()
.nest("/api/v1/products", products_router)
.fallback(|| async {
(StatusCode::NOT_FOUND, "app fallback")
});
// Request /api/v1/products/unknown
// → match prefix /api/v1/products, không match route nào trong sub-router
// → rơi vào sub-router fallback: "no product matches"
//
// Request /random
// → không match prefix nào
// → rơi vào app-level fallback: "app fallback"
Hệ quả của tách biệt này: nếu muốn 404 nhất quán toàn app (cùng envelope, cùng header, cùng log format), cách đơn giản nhất là chỉ đặt fallback ở top-level và KHÔNG đặt fallback trong sub-router. Mọi request không match đều rơi xuống fallback top-level, response thống nhất.
Shop API decision lock B24: chỉ 1 fallback top-level trong router.rs, KHÔNG add fallback trong sub-router (routes/products.rs, routes/health.rs, ...). Fallback trả 404 + AppError::NotFound envelope chuẩn lock B3 với code = "NOT_FOUND". Implementation chi tiết ở B25 (Route Merge & Fallback Handler) cùng với method_not_allowed_fallback trả 405 + Allow header tự sinh.
Lý do quyết định: (a) consistency — client nhận envelope JSON chuẩn dù URL sai ở namespace nào; (b) observability — 1 chỗ log "unmatched URL"/access log, đếm 404 metric dễ; (c) đơn giản maintain — không có sub-router quên fallback rồi fall-through default axum text "Not found" bị lẫn lộn.
Refactor Shop API: Nest /api/v1
Đến phần áp dụng vào Shop API. Trước B24, routes/products.rs đang dùng full path /api/v1/products ở từng .route() call (lock từ B21 — URL pattern tạm thời). B24 refactor: tách prefix /api/v1 ra router.rs qua nest, sub-router routes/products.rs giữ path relative /products/....
Cập nhật crates/shop-api/src/routes/products.rs — đổi mọi "/api/v1/products" sang "/products", mọi thứ khác giữ nguyên:
// File: crates/shop-api/src/routes/products.rs
// Routes are now RELATIVE — prefix "/api/v1" được prepend bởi router.rs nest
use axum::{
extract::{Path, Query},
http::StatusCode,
routing::get,
Json, Router,
};
use serde_json::json;
use shop_common::pagination::{ListResponse, Pagination};
use crate::state::AppState;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/products", get(list_products).post(create_product))
.route("/products/popular", get(list_popular))
.route(
"/products/:slug/related/:related_slug",
get(get_related_product),
)
.route(
"/products/:slug",
get(get_product)
.put(replace_product)
.patch(update_product)
.delete(delete_product),
)
}
// 8 handler skeleton từ B21+B22+B23 GIỮ NGUYÊN nội dung — chỉ path string đổi
async fn list_products(
Query(pagination): Query<Pagination>,
) -> Json<serde_json::Value> {
tracing::info!(
page = pagination.page,
size = pagination.size,
"listing products",
);
let response = ListResponse::<serde_json::Value>::new(vec![], 0, &pagination);
Json(serde_json::to_value(response).unwrap())
}
// create_product, list_popular, get_product, replace_product, update_product,
// delete_product, get_related_product — body GIỮ NGUYÊN từ B21+B22.
Cập nhật crates/shop-api/src/router.rs — wrap sub-router domain trong nest("/api/v1", ...), infrastructure route (/, /health, /version, /error/*, /demo/*) giữ merge ở root level:
// File: crates/shop-api/src/router.rs
use axum::{
http::StatusCode,
routing::get,
Router,
};
use crate::{routes, state::AppState};
pub fn build_router(state: AppState) -> Router {
// Sub-router gom toàn bộ domain route public dưới /api/v1
let api_v1 = Router::new().merge(routes::products::routes());
// Tương lai aggregate thêm:
// .merge(routes::orders::routes())
// .merge(routes::cart::routes())
// .merge(routes::categories::routes())
// .merge(routes::reviews::routes())
Router::new()
.route("/", get(root))
.merge(routes::health::routes()) // infrastructure — root level
.merge(routes::version::routes()) // infrastructure — root level
.merge(routes::demo_error::routes()) // tạm thời, sẽ remove G3+
.merge(routes::demo_async::routes()) // tạm thời, sẽ remove G3+
.nest("/api/v1", api_v1) // ← NEST hết domain route
.with_state(state)
}
async fn root() -> (StatusCode, &'static str) {
(StatusCode::OK, "shop-api v0.1.0")
}
Quan sát hai thay đổi quan trọng:
let api_v1 = Router::new().merge(routes::products::routes());— sub-routerapi_v1aggregate mọi resource domain. Hiện chỉ cóproducts(B21 lock), tương lai thêmorders,cart, ... cùng cách: append.merge(routes::<name>::routes())vào builder chain — KHÔNG cần sửanest("/api/v1", ...)..nest("/api/v1", api_v1)— gắn toàn bộapi_v1dưới prefix/api/v1. Đổi version sang/api/v2chỉ sửa string này — sub-router và mọi handler không cần đổi 1 dòng.
Infrastructure route (/, /health, /version) giữ merge ở root level theo Shop API URL convention (lock từ shop-state.md): /health, /healthz, /readyz, /metrics KHÔNG có version prefix vì là probe infrastructure độc lập domain — Kubernetes liveness/readiness probe + Prometheus scrape gọi đúng path cố định.
Verify Endpoints Sau Refactor
Chạy cargo run -p shop-api rồi curl 4 case verify URL effective KHÔNG đổi từ góc nhìn client. Tính transparency này là chỉ dấu refactor đúng — chỉ thay đổi internal code structure, không break contract với consumer.
# Case 1: domain route qua nest /api/v1
curl http://localhost:3000/api/v1/products
# {"items":[],"total":0,"page":1,"size":20,"hasNext":false}
# Case 2: domain route với path parameter qua nest
curl http://localhost:3000/api/v1/products/phone-x
# {"slug":"phone-x","name":"demo product"}
# Case 3: infrastructure route ở root level (không qua nest)
curl http://localhost:3000/health
# {"status":"ok"}
# Case 4: infrastructure root
curl http://localhost:3000/
# shop-api v0.1.0
Cả 4 endpoint trả response giống y B23 — client KHÔNG biết bên trong router structure thay đổi. URL contract giữ nguyên là điều kiện cần của refactor an toàn cho deploy production: deploy refactor không cần coordinate với consumer (mobile app, frontend, partner integration) cập nhật URL.
Sơ đồ routing table sau B24:
app (root, build_router)
├── GET / → root (router.rs)
├── /health (merge routes::health) → routes/health.rs
│ └── GET /health
├── /version (merge routes::version) → routes/version.rs
│ └── GET /version
├── /error/* (merge routes::demo_error, tạm) → routes/demo_error.rs
│ ├── GET /error/not-found
│ ├── GET /error/unauthenticated
│ └── GET /error/rate-limited
├── /demo/* (merge routes::demo_async, tạm) → routes/demo_async.rs
│ └── POST /demo/background
└── /api/v1 (nest, sub: api_v1) → router.rs (inline)
└── api_v1 (merge routes::products) → routes/products.rs
├── GET, POST /products
├── GET /products/popular
├── GET /products/:slug/related/:related_slug
└── GET, PUT, PATCH, DELETE /products/:slug
Workspace state thay đổi B24: UPDATED crates/shop-api/src/routes/products.rs (path từ full /api/v1/products sang relative /products, 4 entry .route() thay đổi string, 8 handler nội dung giữ nguyên), UPDATED crates/shop-api/src/router.rs (introduce sub-router api_v1, wrap .nest("/api/v1", api_v1)). KHÔNG sửa file khác — Cargo.toml không cần update (không add dependency mới), routes/mod.rs không đổi (sub-router products đã có từ B21).
Tổng Kết
Router::nest(prefix, sub_router)mount sub-router dưới prefix, path bên trong sub-router là relative; axum auto concatprefix + sub_pathkhi dispatch.Router::merge(other)gộp router cùng level, KHÔNG thêm prefix — sub-route giữ nguyên path khai báo gốc.- Multi-level nest cho admin/public split:
nest("/admin", admin_router)trongnest("/api/v1", v1_router)sinh URL/api/v1/admin/products; tối đa 2-3 level readability. - Flat vs nested: nested tập trung prefix dễ đổi version, flat rõ ràng tuyệt đối khi đọc raw code; Shop API dùng hybrid — nest cho organizational prefix (
/api/v1,/admin), flat trong sub-router cho resource path (/products,/products/:slug). - Fallback handler KHÔNG inherit qua nest — sub-router có fallback riêng, request không match route trong sub-router rơi vào fallback của chính sub-router; Shop API quyết định chỉ 1 fallback top-level cho consistency (B25 deep dive).
- Refactor Shop API B24:
routes/products.rsđổi path từ full/api/v1/productssang relative/products;router.rsintroduce sub-routerapi_v1aggregate domain route, wrap.nest("/api/v1", api_v1). - Đổi version
v1→v2chỉ sửa 1 dòng string trongrouter.rs; sub-router và mọi handler không cần đổi. - URL client KHÔNG đổi sau refactor — chỉ thay đổi internal code structure; deploy refactor không cần coordinate với consumer cập nhật URL.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Phân biệt
Router::merge(other)vàRouter::nest(prefix, other). Khi nào dùng cái nào trong Shop API? - Sub-router có
route("/x", get(h)), mount quanest("/api/v1", sub). URL effective client gọi là gì? Trong sub-router, handlerhcó biết prefix/api/v1hay không? - Multi-level nest:
nest("/admin", admin)trongnest("/api/v1", v1). Sub-routeradmincóroute("/users", get(list_users)). URL effective là gì? Vẽ cây route 3 level. - Fallback handler trong nested router có inherit từ parent không? Hệ quả với Shop API là gì? Vì sao Shop API chọn chỉ 1 fallback top-level?
- Shop API muốn add
v2song songv1(giữv1để client cũ dùng,v2cho client mới). Refactorrouter.rsnhư thế nào? Sub-routerroutes/products.rscó cần đổi không?
Đáp án
Router::merge(other)gộp router ở cùng level path, KHÔNG thêm prefix — mọi route trongothergiữ nguyên path khai báo gốc, được dispatch như thể đã khai báo trực tiếp trong app chính.Router::nest(prefix, other)mount router con dướiprefix, mọi route trongotherđược prepend prefix lúc dispatch — path trongotherlà relative. Quyết định Shop API: merge cho service top-level KHÔNG liên quan nhau, không chia sẻ prefix chung — infrastructure như/health,/version,/metricsmỗi cái tự khai báo path đầy đủ ở root level. nest cho tách module theo prefix organizational — domain REST API public dưới/api/v1, admin dashboard tương lai dưới/admin, integration partner dưới/webhooks. Pattern lock B24: hybrid merge+nest, nest cho boundary cross-cutting (version, role, integration), flat trong sub-router cho resource path.- URL effective:
/api/v1/x— axum nối prefix/api/v1với sub-path/xtự động lúc dispatch routing table. Handlerhbên trong sub-router KHÔNG biết prefix/api/v1bên ngoài — nó chỉ biết bản thân được mount ở"/x". Đây là tính chất modular quan trọng của nest: tách bạch route declaration khỏi mount point. Hệ quả: đổi prefix mount sang/api/v2/x,/internal/x, hay/legacy/api/v1/xkhông cần sửa handler hay sub-router — chỉ đổi string ởnest()call trongrouter.rs. Sub-router reusable, có thể mount nhiều lần ở các prefix khác nhau (vd mount cùng router dưới/api/v1và/api/v2để hỗ trợ song song version). - URL effective:
/api/v1/admin/users— axum nối prefix/api/v1(level 1) + prefix/admin(level 2) + sub-path/users(route trongadmin) lúc dispatch. Cây route:
Mỗi level nest tách concern riêng:app (root) └── /api/v1 ← nest level 1 (v1_router) ├── /admin ← nest level 2 (admin_router) │ └── /users ← route("/users", get(list_users)) │ (effective: /api/v1/admin/users) └── ... (route khác trong v1_router)/api/v1là versioning boundary (đổi v1→v2 chỉ sửa 1 dòng),/adminlà role boundary (sau này thêm middlewarerequire_role("admin")quaadmin_router.layer(...)B29 chỉ áp cho admin route, không ảnh hưởng public). Khuyến nghị tối đa 2-3 level cho readability — quá sâu khiến trace URL từ raw code phải lookup chain dài, mất tính rõ ràng. - KHÔNG inherit.
Router::fallback(handler)trong sub-router hoạt động tách biệt với fallback của parent router. Nếu sub-router có fallback riêng, request không match route nào trong sub-router (nhưng vẫn nằm dưới prefix mount) sẽ rơi vào fallback của sub-router, KHÔNG bubble lên parent. Hệ quả với Shop API: nếu mỗi sub-router (products,orders,cart) tự đặt fallback, mỗi cái có thể trả 404 envelope khác nhau (khác message, khác header, khác log format), client trải nghiệm không nhất quán + observability rải rác (nhiều chỗ log "unmatched URL", khó đếm metric 404 tổng). Shop API decision lock B24: chỉ 1 fallback top-level trongrouter.rs, KHÔNG add fallback trong sub-router. Mọi request không match đều rơi xuống fallback top-level, response thống nhấtAppError::NotFoundenvelope chuẩn lock B3 vớicode = "NOT_FOUND". Implementation chi tiết ở B25 cùng vớimethod_not_allowed_fallbacktrả 405 +Allowheader. Lý do: consistency cho client + 1 chỗ log/metric tập trung + đơn giản maintain không lo sub-router quên fallback rồi fall-through default axum text "Not found". - Refactor
router.rsthêm sub-routerapi_v2song songapi_v1, mỗi cái nest dưới prefix tương ứng. Trường hợp v2 dùng route giống hệt v1 (chỉ khác version path, semantic API giống nhau):
Reuse cùngpub fn build_router(state: AppState) -> Router { let api_v1 = Router::new().merge(routes::products::routes()); let api_v2 = Router::new().merge(routes::products::routes()); // reuse sub-router Router::new() .route("/", get(root)) .merge(routes::health::routes()) .merge(routes::version::routes()) .nest("/api/v1", api_v1) .nest("/api/v2", api_v2) // ← song song .with_state(state) }routes::products::routes()ở cả hai version — vìroutes::products::routes()trảRouter<AppState>mới mỗi lần gọi (factory pattern). Sub-routerroutes/products.rsKHÔNG cần đổi 1 dòng — đây chính là pros lớn nhất của nested approach so với flat (đã thấy ở bước 5). Trường hợp v2 có breaking change cần khác handler (vd field renamed, response structure đổi): tạo fileroutes/products_v2.rsriêng vớifn routes() -> Router<AppState>chứa handler v2, sửalet api_v2 = Router::new().merge(routes::products_v2::routes());. Sub-routerroutes/products.rs(v1) vẫn KHÔNG cần đổi. Strategy versioning chi tiết (URL vs header vs query) sẽ deep dive ở B30 (API Versioning).
Bài Tiếp Theo
Bài 25: Route Merge & Fallback Handler — đi sâu vào Router::merge use case cụ thể, fallback handler patterns (404 custom với AppError envelope, method_not_allowed_fallback trả 405 + Allow header tự sinh), test fallback trong Shop API.
