Mục lục
- Mục Tiêu Bài Học
- State Hiện Tại Của Shop API (Recap B10)
- Flow Cốt Lõi: Router → Service → TcpListener → serve
- Handler Signature: async fn → impl IntoResponse
- Multi-Route Trong Router
- Thực Hành: Enhance crates/shop-api/src/main.rs
- Test Với curl Và HTTPie
- Graceful Shutdown — Tại Sao Quan Trọng
- Suggested Commit
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu rõ flow 4 bước của axum server: Router →
tower::Service→ TcpListener →axum::serve. - Biết handler signature chuẩn axum
async fn() -> impl IntoResponsevà 6 type built-in implementIntoResponse. - Biết tạo multi-route trong cùng
Routerqua builder pattern.route()+ chain method. - Hiểu graceful shutdown: vì sao cần,
tokio::signal::ctrl_cbắt SIGINT,signal::unix::SignalKind::terminatebắt SIGTERM,tokio::select!chờ cả hai. - Test endpoint bằng
curl+HTTPie(tool đã lock ở B9) và verify Ctrl+C drain in-flight request. - Có Shop API enhanced với 3 route (
/,/health,/version) + graceful shutdown + log mỗi lần listen/stop, build incremental trên skeleton B10.
State Hiện Tại Của Shop API (Recap B10)
B10 đã init workspace Shop API với 2 crate (shop-api binary + shop-common lib). File crates/shop-api/src/main.rs hiện chạy được trên port 3000 với 2 route tối giản: GET / trả chuỗi "shop-api v0.1.0" và GET /health trả "ok". Module shop_common::config::AppConfig đã load env, shop_common::error::AppError 11 variants đã sẵn (sẽ dùng từ B16), shop_common::telemetry::init_tracing stub đã setup tracing subscriber. Workspace.dependencies đã lock axum = "0.8", tokio = "1" (features macros, rt-multi-thread, signal), serde_json = "1", tracing = "0.1" — bài này không cần thêm dependency mới.
Cây file liên quan trong workspace:
shop/
├── Cargo.toml # workspace.dependencies đã lock
├── crates/
│ ├── shop-api/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── main.rs # ← bài này enhance
│ └── shop-common/
│ └── src/
│ ├── lib.rs
│ ├── config.rs # AppConfig (sẵn từ B10)
│ ├── error.rs # AppError (sẵn từ B10)
│ └── telemetry.rs # init_tracing (sẵn từ B10)
Bài này chỉ chạm vào một file duy nhất crates/shop-api/src/main.rs — extend từ 2 route lên 3 route, thêm graceful shutdown, thêm tracing log mỗi vòng đời server. Module shop-common giữ nguyên không sửa.
Flow Cốt Lõi: Router → Service → TcpListener → serve
Một axum server tối giản trải qua đúng 4 bước, không hơn không kém:
- Build Router —
Router::new().route("/path", get(handler))tạo typeaxum::Router. Bản chấtRouterlà mộttower::Service<Request>— nhậnhttp::Request<Body>, dispatch tới handler khớp path, trảhttp::Response<Body>. - Bind TcpListener —
tokio::net::TcpListener::bind(addr).await?mở socket lắng nghe TCP port. Type này là async wrapper củastd::net::TcpListenerchuẩn POSIX, lấy ownership socket khỏi runtime. - Start server loop —
axum::serve(listener, app).await?drive accept loop: nhận connection mới → spawn task tokio xử lý → gọiService::callcủa router → write response về socket. Function này trảFuturenever-ending cho đến khi shutdown signal đến. - Graceful shutdown — gắn
.with_graceful_shutdown(future)trước.await. Khifutureready (Ctrl+C, SIGTERM), axum stop accept connection mới, đợi in-flight request hoàn tất, rồi mới trả về.
Diagram flow:
┌──────────────────────────────────────────────────────────┐
│ 1. Router::new() │
│ .route("/", get(root)) │
│ .route("/health", get(health)) ← tower::Service│
│ .route("/version", get(version)) │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 2. TcpListener::bind("0.0.0.0:3000").await? │
│ (open socket POSIX, lắng nghe port) │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 3. axum::serve(listener, app) │
│ .with_graceful_shutdown(shutdown_signal()) │
│ .await? │
│ (accept loop + spawn task per request) │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 4. shutdown_signal() future ready │
│ (Ctrl+C hoặc SIGTERM) → drain → exit │
└──────────────────────────────────────────────────────────┘
So với viết server thuần bằng hyper raw (không qua axum), bạn đỡ ~100+ dòng boilerplate cho parse HTTP request, dispatch routing, build response, error handling. axum đóng gói toàn bộ thông qua bốn dòng API trên.
Handler Signature: async fn → impl IntoResponse
Handler axum là một async fn trả về type implement trait IntoResponse trong module axum::response. axum không yêu cầu handler trả thẳng Response<Body> — bất kỳ type nào convert được sang Response đều hợp lệ. Đây là sức mạnh ergonomic giúp handler đọc gần như function tổng quát Rust.
Sáu type built-in implement IntoResponse bạn sẽ dùng nhiều nhất:
Stringvà&'static str→ responsetext/plain; charset=utf-8, status 200.(StatusCode, body)tuple — override status code, body có thể là String hoặc bất kỳ type IntoResponse khác. Ví dụ(StatusCode::CREATED, "ok").Html<T>wrap text →text/html; charset=utf-8. Dùng cho admin dashboard render template.Json<T>wrap struct serialize →application/json; charset=utf-8. Sẽ là response chính của Shop API, deep dive ở B14-B15.Response<Body>— raw response tự build quaResponse::builder(). Dùng khi cần custom header phức tạp.Result<T, E>vớiT: IntoResponsevàE: IntoResponse— error type cũng convert sang response. Đây là pattern Shop API sẽ dùngAppResult<T>từ B16 sau khi implIntoResponsechoAppError.
Ví dụ 4 handler khác signature, cùng hợp lệ với axum:
use axum::{http::StatusCode, response::{Html, IntoResponse, Json}};
use serde_json::json;
// 1. Trả chuỗi tĩnh — text/plain
async fn ping() -> &'static str {
"pong"
}
// 2. Tuple (StatusCode, body) — override status
async fn created() -> impl IntoResponse {
(StatusCode::CREATED, "resource created")
}
// 3. Html — text/html
async fn dashboard() -> impl IntoResponse {
Html("<h1>Admin</h1>")
}
// 4. Json — application/json
async fn version() -> impl IntoResponse {
Json(json!({ "version": "0.1.0" }))
}
Ghi nhớ: AppError (11 variants từ B10 trong shop-common::error) sẽ được impl IntoResponse ở B16 — sau đó mọi handler Shop API return AppResult<Json<T>> sạch, axum tự convert error sang status code + body envelope { error, code, request_id } đã lock ở B3. Tạm thời bài này chỉ dùng String, (StatusCode, ...) và Json<serde_json::Value> đủ minh họa.
Multi-Route Trong Router
Router dùng builder pattern — mọi method khai báo route đều trả về Router mới (consume ownership), cho phép chain method:
.route(path, method_router)— đăng ký một path với mộtMethodRouter. Functionget(handler),post(handler), ... trong moduleaxum::routingtạoMethodRoutersingle method..route(path, get(h1).post(h2))— cùng path nhiều method. Chain method trênMethodRouterđể gắnPOSTsauGET. Đây là pattern Shop API sẽ dùng cho/api/v1/products(GET list + POST create) ở B62..merge(other_router)— gộp hai Router cùng prefix. Use case: tách route module theo resource (fileproducts.rsexportfn router()) rồi merge vào root..nest(prefix, sub_router)— gắn sub-router dưới một prefix..nest("/api/v1", api_v1_router())sẽ làm mọi route trong sub-router có prefix/api/v1. Deep dive ở B24.
Trong bài này, Shop API dùng 3 route đơn giản trong cùng builder chain:
fn build_router() -> Router {
Router::new()
.route("/", get(root))
.route("/health", get(health))
.route("/version", get(version))
}
Path /health sẽ được tách thành /healthz (liveness probe — server có sống không) và /readyz (readiness probe — DB + Redis có sẵn sàng không) ở Group 15. Path /version là build metadata endpoint mới, trả về tên crate + version + edition, dùng cho debug deploy ("phiên bản nào đang chạy ở môi trường này?").
Thực Hành: Enhance crates/shop-api/src/main.rs
Rewrite file crates/shop-api/src/main.rs với 3 route + graceful shutdown + tracing log:
// File: crates/shop-api/src/main.rs
use axum::{
http::StatusCode,
response::IntoResponse,
routing::get,
Json, Router,
};
use serde_json::json;
use shop_common::{config::AppConfig, telemetry};
use tokio::signal;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 1. Tracing subscriber init trước mọi log
telemetry::init_tracing(false);
// 2. Load config từ env (.env + env::var)
let config = AppConfig::from_env()?;
// 3. Build axum Router với 3 route
let app = build_router();
// 4. Bind TCP listener
let addr = format!("0.0.0.0:{}", config.port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
tracing::info!(addr = %addr, "shop-api listening");
// 5. Serve + graceful shutdown
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await?;
tracing::info!("shop-api stopped");
Ok(())
}
fn build_router() -> Router {
Router::new()
.route("/", get(root))
.route("/health", get(health))
.route("/version", get(version))
}
// Root: text/plain identifier
async fn root() -> impl IntoResponse {
(StatusCode::OK, "shop-api v0.1.0")
}
// Health: JSON status, sẽ split thành /healthz + /readyz ở G15
async fn health() -> impl IntoResponse {
Json(json!({ "status": "ok" }))
}
// Version: build metadata cho debug deploy
async fn version() -> impl IntoResponse {
Json(json!({
"name": "shop-api",
"version": env!("CARGO_PKG_VERSION"),
"edition": "2024",
}))
}
// Graceful shutdown: chờ Ctrl+C hoặc SIGTERM, cái nào đến trước
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
tracing::info!("shutdown signal received, draining requests");
}
Phân tích từng phần:
- Macro
#[tokio::main]— khởi tạo multi-thread tokio runtime (featurert-multi-threadđã bật trong workspace.dependencies B10). Functionmaintrở thành async, runtime tự setup worker thread theo số CPU core máy. - Tracing init trước config —
telemetry::init_tracing(false)setup subscriber pretty format cho dev (paramjson=false). Mọitracing::info!sau đó sẽ in ra stdout. Production sẽ passjson=trueđể output JSON cho log aggregator parse. - Macro
env!("CARGO_PKG_VERSION")— compile-time macro của Rust, expand thành chuỗi version trongCargo.tomlcủa crate hiện tại. Khi bạn bump versionshop-apitừ 0.1.0 lên 0.1.1 và rebuild,/versiontrả ngay version mới — không phải hard-code. with_graceful_shutdown— method trênaxum::serve()nhận mộtFuture<Output = ()>. Khi future complete, axum stop accept connection mới, đợi mọi in-flight request hoàn tất (tới khi handler return), rồi resolveserve(...).await.#[cfg(unix)]conditional compilation — chỉ Unix-like OS (Linux, macOS, BSD) mới cóSIGTERMstandard. Windows dùng cơ chế ConsoleControlEvent khác hoàn toàn — đoạnterminatetrên Windows fallbackstd::future::pending::<()>()(future không bao giờ ready), nghĩa là chỉ Ctrl+C hoạt động.tokio::select!— macro race future, branch nào ready trước thì execute branch đó, branch còn lại bị drop. Ở đây chờ cùng lúc Ctrl+C và SIGTERM, signal nào đến trước thì shutdown.
File Cargo.toml của shop-api không cần update — serde_json đã có sẵn từ B10 (đã lock serde_json = "1" trong workspace.dependencies + import qua .workspace = true trong crate Cargo.toml). Verify nhanh bằng cargo build -p shop-api không lỗi.
Test Với curl Và HTTPie
Chạy server từ workspace root:
cargo run -p shop-api
Output kỳ vọng trên terminal:
Compiling shop-api v0.1.0 (/.../crates/shop-api)
Finished `dev` profile in 3.21s
Running `target/debug/shop-api`
2026-06-12T14:00:00.123Z INFO shop_api: shop-api listening addr=0.0.0.0:3000
Mở terminal khác test bằng curl:
# Root: trả text/plain
curl http://localhost:3000/
# shop-api v0.1.0
# Health: trả JSON
curl http://localhost:3000/health
# {"status":"ok"}
# Version: trả JSON với metadata
curl -v http://localhost:3000/version
# > GET /version HTTP/1.1
# > Host: localhost:3000
# < HTTP/1.1 200 OK
# < content-type: application/json
# < content-length: 65
# {"edition":"2024","name":"shop-api","version":"0.1.0"}
Tương đương qua HTTPie (tool đã lock ở B9):
http :3000/
http :3000/health
http :3000/version
HTTPie auto detect application/json Content-Type và pretty-print body có color highlight. Đây là tool nên cài cho dev workflow local — output dễ đọc hơn curl rất nhiều.
Test graceful shutdown — quay về terminal chạy server, ấn Ctrl+C:
^C
2026-06-12T14:01:30.456Z INFO shop_api: shutdown signal received, draining requests
2026-06-12T14:01:30.458Z INFO shop_api: shop-api stopped
Hai dòng log xuất hiện ngay sau khi nhấn Ctrl+C: dòng 1 từ function shutdown_signal (signal đã bắt được), dòng 2 từ main sau khi axum::serve(...).await? resolve (server thực sự dừng). Nếu lúc đó có request đang xử lý, axum sẽ đợi handler hoàn tất rồi mới resolve — đó là essence của graceful shutdown.
Graceful Shutdown — Tại Sao Quan Trọng
Trong môi trường production, server không bao giờ chạy mãi mãi — nó sẽ bị restart, redeploy, hoặc kill bởi orchestrator. Pattern phổ biến nhất là rolling deploy (K8s Deployment, fly.io machine update, Docker Swarm rolling): orchestrator gửi SIGTERM cho process cũ → đợi grace period (mặc định K8s là 30 giây) → nếu process chưa thoát thì gửi SIGKILL force kill. Trong khoảng grace period đó, server phải dọn dẹp sạch sẽ trước khi exit.
Nếu thiếu graceful shutdown:
- Request đang xử lý bị hủy đột ngột — connection TCP bị reset giữa chừng, client nhận lỗi
502 Bad Gatewayhoặcconnection reset by peer. Trong giờ peak traffic, một deploy có thể spike error rate vài phần trăm. - Database connection chưa close — connection pool còn giữ session, transaction đang mở bị abort phía DB, có thể để lại lock hoặc dirty state.
- Background task chưa flush — log buffer, metric buffer, queue message in-flight bị mất.
Axum giải quyết qua method .with_graceful_shutdown(future) nhận một Future<Output = ()>. Khi future ready (signal đến), axum:
- Stop accept connection TCP mới (socket vẫn listen nhưng từ chối nhanh).
- Đợi mọi in-flight request hoàn tất (handler return Response và Response đã ghi xong xuống socket).
- Đóng connection còn lại sạch sẽ, resolve
serve(...).awaittrả vềOk(()).
Hai signal cần bắt cho Shop API binary:
SIGINT— gửi khi dev nhấn Ctrl+C trong terminal.tokio::signal::ctrl_c()trả future ready khi signal đến. Cross-platform (Unix + Windows).SIGTERM— gửi bởi orchestrator (K8s, Docker, systemd) khi cần stop process gracefully. Unix-only — bắt quasignal::unix::signal(SignalKind::terminate())?.recv().await. Trên Windows không có (Windows dùng ConsoleCtrlHandler khác cơ chế hoàn toàn).
Macro tokio::select! race hai future — signal nào đến trước thì shutdown. Pattern này áp dụng nguyên xi cho mọi binary trong workspace Shop API: shop-api (HTTP server, bài này), shop-worker (background job runner sẽ init ở G21), shop-cli (CLI tool ở G29 — CLI ít cần graceful shutdown hơn nhưng vẫn nên có).
Chi tiết deeper về deploy K8s SIGTERM lifecycle, preStop hook, terminationGracePeriodSeconds, và readinessProbe drain pattern sẽ mổ riêng ở B297 (Group 30 Docker & Deploy).
Suggested Commit
Commit message gợi ý cho repo Shop API:
git add crates/shop-api/src/main.rs
git commit -m "B12: enhance shop-api with multi-route, graceful shutdown, tracing log"
File thay đổi trong commit:
crates/shop-api/src/main.rs— rewrite enhanced: thêm route/version, đổi/healthtrả JSON thay text, gắnwith_graceful_shutdown, tracing log lifecycle, helper functionbuild_routervàshutdown_signal.
File giữ nguyên (không touch từ B10):
crates/shop-common/src/*— moduleconfig,error,headers,telemetrykhông đổi.Cargo.tomlworkspace root + crateshop-api/shop-common— dependency lock từ B10 đủ dùng, không thêm gì..env.example,rust-toolchain.tomlkhông đổi.
Sau commit, cargo build --workspace phải pass clean và cargo run -p shop-api phải start được server lắng localhost:3000 như section 7 demo.
Tổng Kết
- Flow axum 4 bước:
Router::new()build router (bản chấttower::Service) →TcpListener::bindmở socket →axum::servedrive accept loop →with_graceful_shutdownchờ shutdown signal. - Handler signature chuẩn axum:
async fn name(extractors...) -> impl IntoResponse. 6 type built-in:String,(StatusCode, body),Html<T>,Json<T>,Response<Body>,Result<T, E>với cả hai vế là IntoResponse. - Router builder:
.route(path, method_router)đăng ký,.merge(other)gộp,.nest(prefix, sub)prefix nested. Multi-method cùng path qua chainget(h1).post(h2)trên MethodRouter. - Graceful shutdown:
with_graceful_shutdown(future)stop accept connection mới + drain in-flight + close clean khi future ready. Cần thiết cho rolling deploy K8s/Docker — tránh request bị hủy nửa chừng spike error rate. - Signal handle:
tokio::signal::ctrl_c()bắt SIGINT cross-platform,signal::unix::SignalKind::terminate()bắt SIGTERM Unix-only (Windows fallbackfuture::pending).tokio::select!race hai future. - Test workflow:
cargo run -p shop-api→curlverbose để xem header + body,HTTPiengắn gọn pretty-print color → Ctrl+C verify log "shutdown signal received" và "shop-api stopped" xuất hiện đúng. - Shop API state sau bài này: 3 route expose —
GET /trả text identifier,GET /healthtrả JSON status,GET /versiontrả JSON build metadata (name + version từenv!("CARGO_PKG_VERSION")+ edition). Skeleton B10 vẫn nguyên, chỉ enhance filemain.rs. - Pattern shutdown chuẩn workspace: function
shutdown_signal()này áp dụng nguyên xi cho mọi binary tương lai —shop-workerở G21,shop-cliở G29 đều cần handle SIGINT + SIGTERM tương tự. Deeper deploy K8s lifecycle ở B297.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Handler axum return
impl IntoResponsenghĩa là gì? Liệt kê 4 type built-in implementIntoResponsevà cho biết Content-Type mặc định của mỗi type. - Phân biệt
.route("/x", get(h1).post(h2))và.merge(other_router). Khi nào dùng cái nào? Cho ví dụ cụ thể trong Shop API. - Tại sao cần
with_graceful_shutdowntrong production? Hậu quả gì xảy ra nếu thiếu khi K8s rolling deploy? - Đoạn code bắt SIGTERM bị wrap trong
#[cfg(unix)]. Tại sao? Có cần handle SIGINT trên Windows không và cơ chế thế nào? - Test endpoint
/versionvớicurl(verbose mode để xem header) vàHTTPie. Viết command tương đương cho cả hai.
Đáp án
- Handler return
impl IntoResponsenghĩa là return một type bất kỳ implement traitaxum::response::IntoResponse— axum tự gọi.into_response()chuyển type đó thànhResponse<Body>. Compiler infer type cụ thể lúc build, không phải dynamic dispatch. 4 type built-in và Content-Type: (1)String/&'static str→text/plain; charset=utf-8; (2)(StatusCode, body)tuple → giữ Content-Type của body, override status code; (3)Html<T>→text/html; charset=utf-8; (4)Json<T>→application/json; charset=utf-8(đã lock từ B5 JSON-only Shop API). Hai type bổ sung:Response<Body>giữ custom header phức tạp vàResult<T, E>với cả hai vế IntoResponse — pattern Shop API dùng choAppResult<T>từ B16. .route("/x", get(h1).post(h2))— cùng path/xnhiều method khác nhau (GETgọih1,POSTgọih2). Chain method trênMethodRouter..merge(other_router)— gộp toàn bộ route củaother_routervào router hiện tại; hai router thường định nghĩa các path khác nhau, merge để tổng hợp. Khi nào dùng: dùngget().post()chain khi cùng resource cùng path nhưng khác method (vd Shop APIGET /api/v1/productslist +POST /api/v1/productscreate — B62). Dùng.merge()khi tách route theo resource sang file riêng để code có cấu trúc — vd fileproducts.rsexportfn router() -> Routercó cảGET+POST+PATCHcho products, rootmain.rsgọi.merge(products::router())+.merge(orders::router())+ ... gom tất cả lại.- Cần
with_graceful_shutdownvì: production server bị restart liên tục (rolling deploy, scale down, redeploy after bug fix) — orchestrator gửi SIGTERM cho process cũ trước khi kill cứng. Trong khoảng grace period (K8s mặc định 30s), server phải dọn dẹp: stop accept connection mới, đợi in-flight request hoàn tất, close DB connection, flush log/metric buffer. Hậu quả thiếu: (a) request đang xử lý bị reset giữa chừng → client nhận502 Bad Gatewayhoặcconnection reset by peer— peak traffic có thể spike error rate vài phần trăm mỗi lần deploy; (b) DB transaction đang mở bị abort, có thể để lock hoặc dirty state; (c) background task chưa flush, log/metric mất. K8s rolling deploy với 10 pod thay phiên restart sẽ tạo ra hiện tượng "fan-out error" rõ rệt trong monitoring nếu thiếu graceful shutdown — đây là lỗi production phổ biến nhất với binary Rust mới deploy. - Đoạn bắt SIGTERM wrap
#[cfg(unix)]vì SIGTERM là khái niệm POSIX — chỉ tồn tại trên Unix-like OS (Linux, macOS, FreeBSD, ...). Windows không có signal mechanism tương đương — Windows dùng ConsoleCtrlHandler với event nhưCTRL_C_EVENT,CTRL_CLOSE_EVENT,CTRL_SHUTDOWN_EVENT, cơ chế khác hoàn toàn. Codesignal::unix::*không compile được trên Windows nên cần conditional compile. SIGINT trên Windows:tokio::signal::ctrl_c()cross-platform — trên Unix bắt SIGINT, trên Windows bắtCTRL_C_EVENTqua ConsoleCtrlHandler dưới hood. Cùng API, behavior tương đương — dev không phải viết riêng. Branch#[cfg(not(unix))]trongshutdown_signalfallbackstd::future::pending::<()>()— future không bao giờ ready, nên trên Windows chỉ Ctrl+C hoạt động (Windows production thường chạy qua Windows Service với cơ chế stop khác, không phải target chính của Shop API). - curl verbose:
curl -v http://localhost:3000/version— flag-vin chi tiết request line + request header (prefix>), response status + response header (prefix<), và body cuối cùng. Bạn sẽ thấy< HTTP/1.1 200 OK,< content-type: application/json,< content-length: ...rồi body JSON. HTTPie:http :3000/version— short form, HTTPie tự suffixlocalhostvà prefixhttp://khi thấy:port/path. HTTPie default in cả header response và body với color highlight, không cần flag verbose. Ngắn hơn rõ rệt, output thân thiện hơn cho dev workflow local — đã lock ở B9 là tool kèmcurltrong README documentation Shop API.
Bài Tiếp Theo
Bài 13: Route Handler — Function Signature — deep dive handler signature axum: argument của handler là extractor (Path, Query, Json, State, ...), return type implement IntoResponse với độ linh hoạt cao, và những wrong type signature gây compile error khó hiểu mà dev mới hay gặp. Bài này sẽ phân loại extractor theo FromRequest vs FromRequestParts, giải thích lý do Json<T> phải đặt cuối argument list, và pattern AppResult<Json<T>> chuẩn cho mọi handler Shop API sau khi AppError impl IntoResponse ở B16.
