Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu layer stack đầy đủ:
hyper→tower→axum→ handler — mỗi layer tách concern rõ ràng. - Biết
tower::Servicetrait là foundation cho mọi request/response service (recap B11 + nối ngầm B141 middleware). - Nắm tower-http middleware chính: CORS, Trace, Compression, Timeout, Body Limit, Request Id, Sensitive Headers.
- Biết
hyperlà HTTP engine low-level (HTTP/1.1, HTTP/2, HTTP/3 quah3crate, WebSocket upgrade). - Hiểu
tonic(gRPC) cùng base tower → dễ integrate axum + tonic trong 1 binary, reuse middleware cross-protocol. - Có ecosystem map tools/library Rust async web 2026: ai maintain crate nào, version lock chéo ra sao.
- Sẵn sàng cho Group 3 Routing Cơ Bản — từ B21 đi vào method router, path/query extract, nested route, state, layer placement.
Layer Stack: hyper → tower → axum
axum không phải framework monolithic — bên dưới là một stack nhiều layer tách concern rõ ràng, mỗi layer chỉ giải quyết một bài toán và đứng trên trait do layer dưới expose. Sơ đồ tổng thể:
┌────────────────────────────────────────┐
│ Application (Handler async fn) │ ← shop-api/src/handlers/*
├────────────────────────────────────────┤
│ axum::Router (thin layer over Service) │ ← Router::new().route(...)
├────────────────────────────────────────┤
│ tower::Service trait │ ← async fn call(Req) → Res
├────────────────────────────────────────┤
│ tower-http middleware │ ← CORS, Trace, Compress, ...
├────────────────────────────────────────┤
│ hyper (HTTP/1.1 + HTTP/2 + WebSocket) │ ← TCP wire protocol
├────────────────────────────────────────┤
│ tokio::net::TcpListener │ ← OS socket
└────────────────────────────────────────┘
Cách đọc stack từ dưới lên: kernel mở socket TCP qua tokio::net::TcpListener, hyper đọc bytes raw rồi parse thành HTTP request structured (HTTP/1.1 hoặc HTTP/2 frame), tower-http middleware wrap quanh service xử lý concern xuyên suốt (log, compress, CORS, ...), axum::Router route request đến đúng handler dựa trên method + path, cuối cùng handler async fn của bạn thực thi business logic.
Mỗi layer tách concern: hyper lo wire protocol HTTP, tower lo abstraction request/response service, tower-http lo middleware HTTP-specific, axum lo routing + extractor, handler lo business logic. Lợi ích đa tầng:
- Reuse cross-framework — tower middleware (TimeoutLayer, RateLimitLayer) viết một lần dùng được cho axum + tonic + warp.
- Test độc lập — handler axum test không cần spin TCP socket, gọi
Router::oneshot()trực tiếp (B253). - Thay thế từng phần — đổi hyper version (1.x → 2.x giả định) không phải sửa handler, chỉ workspace dep bump.
- Composability cao — mọi tầng đều là trait object hoặc generic, không bị lock vào implementation.
hyper — HTTP Engine
hyper là pure HTTP implementation cho Rust — server + client. Author Sean McArthur (core contributor tokio-rs), repo hyperium/hyper, version 1.x rewrite hoàn thiện cuối 2023 sau nhiều năm 0.14. Hỗ trợ HTTP/1.1, HTTP/2 native, HTTP/3 qua crate phụ h3 chuẩn QUIC, WebSocket upgrade qua tokio-tungstenite. Bộ runtime async chạy trên tokio.
hyper là low-level — bạn có thể dùng trực tiếp không cần axum, nhưng phải tự routing + extract + response build:
// Bare hyper — verbose
use hyper::body::Incoming;
use hyper::{Request, Response};
use http_body_util::Full;
async fn handle(req: Request<Incoming>) -> Result<Response<Full<bytes::Bytes>>, hyper::Error> {
match (req.method(), req.uri().path()) {
(&hyper::Method::GET, "/health") => {
Ok(Response::new(Full::from("ok")))
}
_ => {
let mut resp = Response::new(Full::from("not found"));
*resp.status_mut() = hyper::StatusCode::NOT_FOUND;
Ok(resp)
}
}
}
Cùng nội dung qua axum (đã quen từ B12 onward):
// axum — declarative
use axum::{routing::get, Router};
let app = Router::new().route("/health", get(|| async { "ok" }));
axum sử dụng hyper qua adapter axum::serve(listener, app) wrap hyper server loop. Phía client, crate reqwest (HTTP client phổ biến nhất ecosystem Rust) cũng dùng hyper bên dưới — bạn dùng reqwest gọi external service (Stripe, S3) chính là gián tiếp dùng hyper.
Version lock: hyper 1.x đã re-design 2023 sửa rất nhiều rough edge của 0.14 (decouple body trait, generic over executor, không bind tokio cứng). axum 0.8 (đã lock workspace từ B10) dùng hyper 1 — không phải khai báo trực tiếp trong Cargo.toml vì axum re-export type cần thiết. Khi nào cần dùng hyper direct: implement webhook signature verify cần body raw bytes (B37), serve HTTP/3 (chưa standard ecosystem), upgrade WebSocket custom protocol.
tower::Service Trait — Cốt Lõi
Recap B11: tower::Service là universal abstraction cho mọi async request/response — không chỉ HTTP, mà cả gRPC, custom RPC, hay bất kỳ pipeline async Request → Response. Trait đơn giản:
pub trait Service<Request> {
type Response;
type Error;
type Future: Future<Output = Result<Self::Response, Self::Error>>;
fn poll_ready(
&mut self,
cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>>;
fn call(&mut self, req: Request) -> Self::Future;
}
Hai method cốt lõi: poll_ready báo service đã sẵn sàng nhận request hay chưa (use case backpressure, rate-limit), call nhận request trả future của response. Service là async function-like trait generalized — gần giống async fn(Request) -> Result<Response, Error> nhưng cho phép state mutable (&mut self) để giữ counter, pool, ...
axum Router implement Service<Request<Body>, Response = Response<Body>> — toàn bộ router của bạn chính là một Service. hyper server loop chỉ cần một Service để drive accept-loop:
// Minimal Service impl — echo handler
use tower::Service;
use std::task::{Context, Poll};
use std::pin::Pin;
use std::future::Future;
#[derive(Clone)]
struct Echo;
impl Service<String> for Echo {
type Response = String;
type Error = std::convert::Infallible;
type Future = Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: String) -> Self::Future {
Box::pin(async move { Ok(format!("echo: {req}")) })
}
}
Middleware compose qua trait Layer<Service> — wrap một service inner thành service outer:
pub trait Layer<S> {
type Service;
fn layer(&self, inner: S) -> Self::Service;
}
Khi áp dụng .layer(TraceLayer::new_for_http()) vào axum Router, thực chất bạn gọi Layer::layer(&trace_layer, router) để wrap Router thành service mới có thêm logic trace. Mỗi lần wrap thêm một layer là thêm một concentric ring quanh handler core. Deep dive ordering layer ở G15 (Observability) và B29 (route_layer vs Layer).
tower-http — Middleware Battery-Included
tower-http là collection middleware tower chuyên cho HTTP service — viết theo trait Layer<Service> để wrap service HTTP. Version lock: tower-http = "0.6" đã có trong workspace.dependencies từ B10 với feature trace + cors + compression-gzip bật sẵn; các feature còn lại enable dần khi handler cần.
Feature flag chính tower-http 0.6: trace, cors, compression-full (gzip + brotli + deflate + zstd), compression-gzip (chỉ gzip), timeout, limit (body size), request-id, sensitive-headers, set-header, propagate-header, auth, decompression-full, fs (ServeDir/ServeFile cho static), catch-panic, normalize-path.
Shop API sẽ enable dần các layer sau (chi tiết impl ở G15-G16 Observability + Hardening):
TraceLayer— log request method/path/status + latency tự động quatracing; spawn span per request gắn vớiX-Request-Id(B39).CorsLayer— handle CORS preflightOPTIONSrequest, setAccess-Control-Allow-Origin+Allow-Methods+Allow-Headerstheo policy.CompressionLayer— gzip/br response tự động dựaAccept-Encodingclient; B48 deep dive ratio vs CPU.TimeoutLayer— abort handler vượt N giây trả 408 Request Timeout.RequestBodyLimitLayer— cap body size 2MB mặc định, vượt trả 413 Payload Too Large (B47).SetRequestIdLayer+PropagateRequestIdLayer— sinh hoặc tôn trọngX-Request-Idclient gửi, propagate vào response header.SetSensitiveHeadersLayer— markAuthorization/Cookie/Set-Cookielà sensitive →tracingtự redact khỏi log structured (không leak token vào log production).
Pattern wiring layer chain trong build_router (preview, sẽ apply thật ở G15):
use tower_http::{
cors::CorsLayer,
trace::TraceLayer,
compression::CompressionLayer,
timeout::TimeoutLayer,
limit::RequestBodyLimitLayer,
};
use std::time::Duration;
pub fn build_router(state: AppState) -> Router {
Router::new()
.merge(routes::health::routes())
.merge(routes::product::routes())
// layer apply bottom-up: outer cuối chain chạy đầu request
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive())
.layer(CompressionLayer::new())
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.layer(RequestBodyLimitLayer::new(2 * 1024 * 1024))
.with_state(state)
}
Workspace dep recap (đã có từ B10, sẽ extend feature khi G15-G16 cần):
# File: shop/Cargo.toml (workspace root)
[workspace.dependencies]
tower = "0.5"
tower-http = { version = "0.6", features = [
"trace",
"cors",
"compression-gzip",
# G15 thêm: "timeout", "limit", "request-id",
# "sensitive-headers", "set-header", "propagate-header"
] }
tonic — gRPC Cùng Base tower
tonic là gRPC framework cho Rust do tokio-rs maintain — cùng team axum. Version lock (đã preview B7): tonic = "0.13" + prost = "0.14" + tonic-build = "0.13". tonic build trên HTTP/2 (gRPC chuẩn dùng HTTP/2 frame) qua hyper, codegen Rust trait từ file .proto qua tonic-build trong build script.
Điểm vàng: service trait của tonic giống tower — server gRPC cũng là một Service implementor. Hệ quả: middleware viết qua tower::Layer reuse được giữa axum và tonic. Một AuthLayer verify JWT có thể wrap cả axum Router (cho REST endpoint) và tonic service (cho gRPC method) — viết một lần, dùng hai protocol.
// tonic preview (B314) — gRPC service cho Catalog
use tonic::{Request, Response, Status};
// File generated tự động bởi tonic-build từ catalog.proto
use catalog::catalog_service_server::{CatalogService, CatalogServiceServer};
use catalog::{GetProductRequest, GetProductResponse};
#[derive(Default)]
pub struct CatalogSvc {
// share shop-core service và shop-db pool
}
#[tonic::async_trait]
impl CatalogService for CatalogSvc {
async fn get_product(
&self,
request: Request<GetProductRequest>,
) -> Result<Response<GetProductResponse>, Status> {
let slug = request.into_inner().slug;
// gọi cùng shop-core::service::ProductService như axum handler
let product = fetch_product_by_slug(&slug).await?;
Ok(Response::new(product.into()))
}
}
Pattern triển khai trong production: 2 cách phổ biến.
- Hai port riêng biệt — axum nghe port 3000 (REST public), tonic nghe port 50051 (gRPC internal); load balancer hoặc service mesh route khác nhau theo protocol. Đây là pattern Shop API capstone B314 sẽ áp dụng.
- Mux trên cùng port — dùng
hyper::service::make_service_fn+ tower mux phân loại request dựaContent-Type: application/grpcvs JSON, route đến axum hoặc tonic tương ứng. Phức tạp hơn nhưng tiết kiệm port (dev local).
tonic interceptor (middleware gRPC) viết bằng tower::Layer → reuse được auth, tracing, rate-limit từ stack axum của Shop API. Chi tiết deep dive ở B314 Capstone gRPC Service với tonic.
Ecosystem Map — Rust Async Web 2026
Bản đồ ecosystem giúp bạn định vị axum giữa các crate Rust async web Q4 2026:
Layer / Role | Library | Version | Maintained By
--------------------+---------------------+---------+----------------
HTTP engine | hyper | 1.x | tokio-rs
Service abstraction | tower | 0.5 | tokio-rs
HTTP middleware | tower-http | 0.6 | tokio-rs
REST framework | axum | 0.8 | tokio-rs
gRPC framework | tonic | 0.13 | tokio-rs
HTTP client | reqwest | 0.12 | seanmonstar
WebSocket client | tokio-tungstenite | 0.24 | snapview
SSE | axum SSE (built-in) | 0.8 | tokio-rs
GraphQL | async-graphql | 7.x | community
Async runtime | tokio | 1.40+ | tokio-rs
Quan sát quan trọng:
- Tokio-rs maintain 6/10 crate cốt lõi (hyper, tower, tower-http, axum, tonic, tokio) — version compat đảm bảo qua release cadence chung; nâng axum không phải lo break hyper.
reqwestdo Sean McArthur (cũng là author hyper) maintain → integration cực mượt với hyper underlay.axum+tonic+reqwestcùng sharetower+hyperfoundation → middleware viết một lần áp dụng cross-protocol.- Alternatives đáng cân nhắc:
warp(đang giảm dần, filter combinator khó đọc — chính lý do axum sinh ra),rocket(sync history, async muộn, ecosystem nhỏ),actix-web(actor-based, performance gần ngang axum, learning curve cao hơn). Q4 2026 axum chiếm phần lớn project Rust web mới.
Apply Vào Shop API Architecture
Áp dụng ecosystem vào kiến trúc Shop API end-to-end. Diagram binary stack target khi series hoàn thành:
┌──────────────────────────────────────────────────────────┐
│ Shop API Binary Stack (target) │
├──────────────────────────────────────────────────────────┤
│ shop-api (axum + tower-http) shop-grpc (tonic) │
│ port 3000 — REST public port 50051 — gRPC int │
│ handlers/* + dto/* services/* generated │
│ │
│ shop-graphql (axum + async-graphql) │
│ port 3000 /graphql — capstone B313 │
├──────────────────────────────────────────────────────────┤
│ shop-core (domain + service + repo trait) │
│ shop-db (sqlx PostgreSQL adapter) │
│ shop-cache (Redis adapter) │
│ shop-common (config + error + telemetry) │
└──────────────────────────────────────────────────────────┘
Phân tách rõ ràng theo crate:
- REST API service:
shop-api(axum + tower-http) — chính, Group 3-31 từ B21 đi dần qua 60 endpoint thực. - Capstone gRPC:
shop-grpcsubset Catalog (tonic) — B314, reuseshop-core+shop-dbkhông viết lại business logic; chỉ thay HTTP handler bằng gRPC service impl. - Capstone GraphQL:
shop-graphqlsubset Catalog (async-graphql + axum SSE subscription) — B313, exposePOST /graphql+GET /graphqlplayground ở dev. - HTTP client outbound:
shop-apigọi external (Stripe API, S3, SendGrid) quareqwest— cùng base hyper, không phải duplicate runtime async. - Middleware cross-protocol: viết một
AuthLayerquatower::Layertrait → áp dụng cho cảshop-api(axum Router) lẫnshop-grpc(tonic server). Tương tự choTraceLayer+RateLimitLayer+RequestIdLayer.
Cách Shop API setup thực tế trong các bài sau: G15 enable đầy đủ tower-http layer chain (trace + cors + compression + timeout + body limit + request id + sensitive headers), G16 hardening (security header set, rate limit, idempotency), G18 auth middleware (JWT verify + role check), G21 background worker (apalis + Redis), G31 e2e test (axum-test + testcontainers). Capstone B313-B314 đóng vai trò chứng minh kiến trúc clean: thay protocol không phải viết lại domain.
Tổng Kết
- Layer stack:
hyper(HTTP wire) →tower(Service abstraction) →tower-http(HTTP middleware) →axum(Router + extractor) → handler async fn. hyper1.x: HTTP/1.1 + HTTP/2 + HTTP/3 (quah3) + WebSocket upgrade; dotokio-rsmaintain (author Sean McArthur).tower::Servicetrait là universal abstraction cho mọi request/response service: 2 methodpoll_ready+callasync;Layer<Service>trait compose middleware.tower-http0.6 (lock workspace từ B10):TraceLayer,CorsLayer,CompressionLayer,TimeoutLayer,RequestBodyLimitLayer,SetRequestIdLayer,PropagateRequestIdLayer,SetSensitiveHeadersLayer.tonic0.13 gRPC cùng base tower → middleware reuse giữa axum REST và tonic gRPC (1Layerimpl cho 2 protocol).- Ecosystem Rust async web 2026:
tokio-rsmaintain hầu hết (hyper, tower, tower-http, axum, tonic, tokio);reqwestdo seanmonstar;async-graphqldo community. - Shop API architecture:
shop-apiREST core, capstone B313 GraphQL + B314 gRPC dùng cùngshop-core+shop-db; thay protocol không phải viết lại domain. - Middleware viết qua
tower::Layer→ cross-protocol reusable: auth, trace, rate-limit, request-id áp dụng được cho cả axum + tonic + tower mux. - Group 2 Axum Overview hoàn thành — sẵn sàng vào Group 3 Routing Cơ Bản từ B21.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Layer stack
hyper→tower→axum. Mỗi layer trách nhiệm gì? Cho ví dụ cụ thể một concern thuộc tầng nào. tower::Servicetrait core. Hai method chính là gì? Method async hay sync, vì sao thiết kế như vậy?tower-httpcung cấp khoảng 10 layer chính. Liệt kê 5 layer Shop API sẽ dùng ở G15-G16 và vai trò mỗi layer.tonic(gRPC) cùng base tower với axum. Nêu 2 lợi ích cụ thể cho Shop API capstone B314.- Middleware viết qua
tower::Layer<Service>. Cross-protocol reuse như thế nào với axum + tonic? Cho ví dụ mộtAuthLayerdùng được cho cả hai.
Đáp án
- hyper — HTTP wire protocol engine: parse bytes raw từ TCP socket thành
Request/Responsestructured (HTTP/1.1 frame, HTTP/2 frame, HTTP/3 quah3), encode ngược lại khi response. Ví dụ concern thuộc hyper: parse chunked transfer encoding, multiplex stream HTTP/2, upgrade connection sang WebSocket. tower — universal abstractionService<Request>+Layer<Service>: định nghĩa "service" là gì (asynccalltrả future response), compose middleware qua layer wrap. Concern thuộc tower: timeout, rate-limit, retry, load-balance — không phụ thuộc HTTP, áp dụng được cho gRPC hay custom RPC. axum — REST framework layer mỏng trên tower:Router(match method + path), extractor (Json,Path,Query,State), response traitIntoResponse. Concern thuộc axum: routing"/api/v1/products/:slug"đến handler đúng, deserialize JSON body qua serde, map handler return → HTTP response. Handler — business logic của bạn: gọishop-core::service, query DB quashop-db, gọi Redis quashop-cache, return DTO. Concern thuộc handler: lookup product theo slug, kiểm tra inventory, tính total cart. - Hai method chính của
tower::Service:poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>— báo service đã sẵn sàng nhận thêm request hay chưa; trảPoll::Pendingnếu service đang busy (backpressure),Poll::Ready(Ok(()))khi sẵn sàng.call(&mut self, req: Request) -> Self::Future— nhận request, trả future của response. Method không phải async fn trực tiếp (chưa stable async trait method ở thời điểm tower 0.5 spec), mà trảtype Future: Future<Output = Result<Self::Response, Self::Error>>qua associated type → behavior async tương đương. Lý do thiết kế: (a) cho phép Service implementor giữ mutable state qua&mut self(counter, pool, queue) — async fn trait method dùngselfhoặc&self; (b) tách rõ readiness check (poll_readybáo backpressure trước khi call) khỏi execution (callthực thi request) — pattern này cho phép load balancer pick service đã ready, hoặc rate limiter chặn từpoll_readykhông tốn cycle gọicall; (c) universal cho mọi protocol — HTTP, gRPC, custom RPC, queue consumer — không bind cứng vàoasync fnsignature. - 5 layer Shop API sẽ dùng ở G15-G16 (Observability + Hardening): (1)
TraceLayer(featuretrace) — log structured request/response per request: method, path, status, latency; spawn tracing span gắn vớiX-Request-Id(B39) cho distributed tracing. (2)CorsLayer(featurecors) — handle CORS preflightOPTIONSrequest, setAccess-Control-Allow-Origintheo policy whitelist domain frontend (SPA, mobile webview); reject preflight không match → browser block request thật. (3)CompressionLayer(featurecompression-gziphiện tại, có thể bumpcompression-fullbật br nếu CDN không đảm nhận) — gzip response body tự động dựaAccept-Encodingclient; giảm bandwidth catalog list endpoint (B48 ratio/CPU tradeoff). (4)TimeoutLayer(featuretimeout, G15 enable) — abort handler vượt 30s mặc định trả 408 Request Timeout; chặn handler treo do DB slow query hoặc external API hang. (5)RequestBodyLimitLayer(featurelimit, G15 enable) — cap body size 2MB mặc định cho API endpoint, vượt trả 413 Payload Too Large (B47); endpoint upload multipart có override riêng. Ngoài ra G15 còn enable:SetRequestIdLayer+PropagateRequestIdLayer(featurerequest-id) choX-Request-Idpropagation,SetSensitiveHeadersLayer(featuresensitive-headers) markAuthorization/Cookieredact khỏi log. - 2 lợi ích cụ thể tonic cùng base tower với axum cho Shop API capstone B314: (a) Middleware cross-protocol reuse — Shop API viết
AuthLayer(verify JWT từ metadata gRPC giống verify từAuthorizationheader REST),TraceLayer(log request gRPC method + status code gRPC giống log REST),RateLimitLayer(sliding window Redis),RequestIdLayer(sinh + propagate request id) — viết một lần quatower::Layer<Service>, áp dụng cho cảshop-apiaxum Router lẫnshop-grpctonic server bằng cách.layer(AuthLayer::new(state.clone()))ở cả hai. Tiết kiệm code, đảm bảo behavior nhất quán (cùng policy auth/rate-limit cho REST và gRPC). (b) Share runtime + dependency — tonic dùnghyper+tokio+towercùng version với axum, không phải duplicate runtime async hay HTTP engine; Shop API workspace chỉ kéo thêmtonic+prost+tonic-buildmà không gây conflict version cross-crate. Domain layershop-core+shop-dbreuse 100% giữa axum và tonic — gRPC service impl chỉ là handler wrapper gọi cùngProductService::get_by_slug(slug)như axum handler, chỉ khác serialize/deserialize message quaprost(Protocol Buffers) thay vìserde_json. - Cross-protocol reuse qua
tower::Layer<Service>: traitLayer<S>generic over inner serviceS— không bind cứngSphải là axum Router hay tonic server. MộtAuthLayerimplLayer<S> where S: Service<Request>wrap bất kỳ Service nào. Ví dụ pseudocode:struct AuthLayer { state: Arc<AppState> }implLayer<S>vớifn layer(&self, inner: S) -> AuthMiddleware<S> { AuthMiddleware { inner, state: self.state.clone() } };AuthMiddleware<S>implService<Request>vớicallđọcAuthorizationheader → verify JWT quajsonwebtoken→ nếu OK gọiself.inner.call(req), nếu fail trảResponse401 không gọi inner. Áp dụng:let app = axum::Router::new()....layer(AuthLayer::new(state.clone()));cho axum,let svc = tonic::transport::Server::builder().layer(AuthLayer::new(state.clone())).add_service(CatalogServiceServer::new(catalog_svc));cho tonic — cùngAuthLayerinstance, hai protocol khác nhau. Trade-off duy nhất:Request/Responsetype khác (axum dùnghttp::Request<Body>, tonic dùnghttp::Request<tonic::body::BoxBody>) — implementor cần generic đủ rộng hoặc viết 2 impl thin wrapper share core logic. Cách thiết kế đúng: tách verify JWT thành function thuần (input token string, output claims) ngoài Layer; Layer chỉ là adapter cho protocol cụ thể. Pattern này lock cho Shop API: G18 viết mộtAuthMiddlewaretổng quát + thin adapter cho axum (G18) và tonic (B314).
Bài Tiếp Theo
Bài 21: Route Methods: GET, POST, PUT, DELETE, PATCH — bắt đầu Group 3 Routing Cơ Bản: chi tiết method router cho mỗi HTTP method (get, post, put, delete, patch), MethodRouter trait, multi-method cùng path .route("/x", get(h1).post(h2)), any() matcher, route ordering pitfall. Shop API sẽ thiết kế CRUD chuẩn cho /api/v1/products.
