Mục lục
- Mục Tiêu Bài Học
- axum-extra Là Gì?
- Cookie Jar — 3 Loại
- TypedHeader — Strongly-Typed Header Extract
- Form Extractor — application/x-www-form-urlencoded
- erased-json — Serialize Trait Object
- Query Với Defaults — Query<T> Extension
- Add axum-extra Vào workspace.dependencies
- 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 vai trò axum-extra crate — chứa extension features không nằm trong axum core để giảm dependency tree mặc định, maintain bởi cùng team tokio-rs.
- Biết 5 feature flag chính:
cookie,cookie-signed,cookie-private,typed-header,form, ngoài raerased-jsonvàqueryhelper. - Nắm CookieJar 3 loại: plain (không bảo vệ),
SignedCookieJar(HMAC SHA256 tamper-proof),PrivateCookieJar(AES-256-GCM encrypted). - Hiểu
TypedHeader<T>với strongly-typedAuthorization<Bearer>,ContentType,UserAgentthay parseHeaderMapraw manual. - Biết
Form<T>extractor choapplication/x-www-form-urlencoded(HTML form login/register, Stripe webhook payload). - Hiểu
erased-jsongiải bài toán serialize trait object quaBox<dyn ErasedSerialize>. - Lock
axum-extra = "0.10"với 6 feature flag opt-in vàoworkspace.dependenciescho future use, enable dần theo handler thật.
axum-extra Là Gì?
axum-extra là crate official maintained bởi tokio-rs cùng team với axum, repo cùng workspace github.com/tokio-rs/axum/tree/main/axum-extra. Mục đích: chứa các extension features mà axum core không bundle để giữ dependency tree mặc định nhỏ gọn. Người dùng axum không cần cookie, không cần typed header, không cần form thì không kéo theo cookie crate, headers crate, serde_urlencoded.
Convention naming Cargo.toml dùng feature flag opt-in — chỉ enable feature thực sự cần:
axum-extra = { version = "0.10", features = [
"cookie",
"cookie-private",
"cookie-signed",
"typed-header",
"form",
] }
Bảng feature flag chính trong axum-extra 0.10 ánh xạ sang module/type tương ứng:
Feature | Module | Type chính
---------------------+---------------------------------+-----------------------
cookie | extract::cookie | CookieJar
cookie-signed | extract::cookie | SignedCookieJar
cookie-private | extract::cookie | PrivateCookieJar
typed-header | typed_header | TypedHeader<T>
form | extract | Form<T>
erased-json | response | ErasedJson
query | extract | OptionalQuery<T>
Version 0.10 align với axum 0.8 (đã lock workspace.dependencies từ B10). Khi axum bump major sang 0.9 thì axum-extra bump tương ứng — luôn pin cùng compatibility window.
Shop API lock axum-extra = "0.10" vào workspace.dependencies ngay ở bài này, nhưng chưa add vào crates/shop-api/Cargo.toml. Feature sẽ enable dần khi handler thật cần dùng — cookie ở B106 (admin login), typed-header ở B112 (JWT extract), form ở B105 (admin login page).
Cookie Jar — 3 Loại
axum-extra cung cấp ba loại jar đại diện ba mức bảo vệ cookie khác nhau, mỗi loại phục vụ một nhóm use case rõ ràng.
Plain CookieJar — cookie không bảo vệ, server lưu raw, client đọc/ghi tự do qua DevTools hoặc document.cookie. Use case: preference UI (dark mode, language, sidebar collapsed state), cookie consent banner — dữ liệu không nhạy cảm, không liên quan auth, client có thể chỉnh tay thoải mái không phá vỡ security.
use axum_extra::extract::cookie::{Cookie, CookieJar};
async fn set_theme(jar: CookieJar) -> CookieJar {
jar.add(Cookie::new("theme", "dark"))
}
async fn get_theme(jar: CookieJar) -> String {
jar.get("theme")
.map(|c| c.value().to_string())
.unwrap_or_else(|| "light".to_string())
}
SignedCookieJar — server ký HMAC SHA256 phía sau giá trị, mỗi cookie thành <value>.<hmac>. Client đọc được giá trị nhưng không thay đổi được — sửa giá trị mà không tính lại HMAC thì server verify fail. Use case: CSRF token (client cần đọc để gửi vào header X-CSRF-Token), feature flag không bí mật nhưng cấm sửa, flash message giữa request.
PrivateCookieJar — server AES-256-GCM encrypt + sign nội dung. Client KHÔNG đọc được giá trị (nhìn DevTools chỉ thấy bytes random base64), không sửa được. Use case: session ID admin dashboard, sensitive user data nhúng vào cookie.
use axum_extra::extract::cookie::{Cookie, Key, PrivateCookieJar};
use axum::extract::State;
#[derive(Clone)]
struct AppState {
cookie_key: Key, // 32+ bytes random
}
async fn login(
State(state): State<AppState>,
jar: PrivateCookieJar,
) -> PrivateCookieJar {
let session_id = "sess_abc123"; // sinh từ Redis (B105)
let cookie = Cookie::build(("session", session_id))
.http_only(true)
.secure(true)
.same_site(axum_extra::extract::cookie::SameSite::Strict)
.path("/")
.build();
jar.add(cookie)
}
Cả SignedCookieJar và PrivateCookieJar đều cần Key instance — đối tượng wrap khóa bí mật server-side. Key tối thiểu 32 bytes random (không phải password user, không phải string ngắn) — sinh qua Key::generate() trong dev hoặc đọc từ cookie_key env variable trong prod. Key phải share cross-instance khi scale horizontal (nhiều pod K8s) — lưu trong secret manager (Vault, AWS Secrets Manager), không hard-code.
Decision lock cho Shop API:
SignedCookieJarcho CSRF token (B107) — token client cần đọc để inject header, nhưng không được sửa.PrivateCookieJarcho session ID admin dashboard (B105) — admin login qua HTML form (không phải API client) → server set session cookie encrypted → request kế server decrypt + lookup Redis session.- API client KHÔNG dùng cookie — giữ pattern Bearer JWT (đã lock B4 + G12). API consumer là mobile app + SPA + partner integration — token-based đơn giản hơn cookie cho cross-origin và mobile.
- Encryption key 32+ bytes random sinh qua
Key::generate()lúc bootstrap dev, prod đọc từ envCOOKIE_KEYbase64. Rotation theo lịch ở B289 (key management).
TypedHeader — Strongly-Typed Header Extract
axum core extract header qua HeaderMap raw — handler tự lookup key bằng string, tự parse value, tự handle lỗi format:
use axum::http::HeaderMap;
async fn handler_raw(headers: HeaderMap) -> String {
let auth = headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.unwrap_or("");
auth.to_string()
}
Vấn đề: lookup string dễ sai chính tả, parse manual mỗi handler, không type-safe, không reuse validation logic cross-handler.
axum-extra cung cấp TypedHeader<T> với T implement trait headers::Header. Compiler check tại signature, parse + validate tự động, reject với 400 Bad Request nếu header missing hoặc sai format. Built-in typed header phổ biến:
Authorization<Bearer>— extractAuthorization: Bearer <token>.Authorization<Basic>— extract Basic auth (legacy, dùng cho admin internal).ContentType— parse MIME typeContent-Type.UserAgent— get UA string typed thay parse manual.Host— get Host header với port tách riêng.Cookie— parse cookie header (alternative choCookieJarkhi chỉ cần đọc).
Handler tận dụng TypedHeader<T>:
use axum_extra::{
headers::{authorization::Bearer, Authorization},
TypedHeader,
};
use shop_common::error::{AppError, AppResult};
async fn protected(
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> AppResult<String> {
let token = auth.token(); // &str đã strip prefix "Bearer "
// verify JWT claims (B112)
let claims = verify_jwt(token)
.map_err(|_| AppError::Unauthenticated)?;
Ok(format!("user_id = {}", claims.sub))
}
So với version raw, handler ngắn hơn nhiều, không quan tâm string lookup, parse, prefix strip — compiler enforce header tồn tại và đúng format. Nếu thiếu header, axum-extra trả 400 với body chuẩn (hoặc bạn map qua AppError ở B41 cho envelope { error, code, request_id } nhất quán).
Custom TypedHeader — impl trait headers::Header cho struct riêng (ví dụ Idempotency-Key theo convention Stripe đã lock B4). Implement 3 method name(), decode(), encode() — chi tiết deep dive ở B33 (Header Extractor Typed).
Apply Shop API:
- B112 JWT extract — middleware/extractor
CurrentUserdùngTypedHeader<Authorization<Bearer>>đọc token → verify claims quajsonwebtokencrate → injectCurrentUser { id, role }vào handler. - B33 custom
Idempotency-Key— implementheaders::Headercho structIdempotencyKey(Uuid), dùng choPOST /api/v1/checkoutvàPOST /api/v1/payments. - Content-Type negotiation ở webhook endpoint — dùng
TypedHeader<ContentType>verifyapplication/x-www-form-urlencodedtrước khi parse Stripe payload (B197).
Form Extractor — application/x-www-form-urlencoded
Form<T> extractor parse request body theo định dạng application/x-www-form-urlencoded — format chuẩn HTML form submit, encoded kiểu email=alice%40shop.com&password=secret123. Khác Json<T> parse JSON body, Form<T> parse URL-encoded body qua serde_urlencoded crate dưới hood.
Use case chính:
- HTML form submit — login page, register page, contact form render server-side (template askama/maud/tera). Browser submit form thì tự gửi
application/x-www-form-urlencodedmặc định. - Webhook nhận form-encoded — Stripe webhook gửi event qua
application/x-www-form-urlencodedtheo convention (không phải JSON), GitHub webhook một số event cũng tương tự legacy.
Handler dùng Form<T>:
use axum_extra::extract::Form;
use serde::Deserialize;
use shop_common::error::{AppError, AppResult};
#[derive(Debug, Deserialize)]
struct LoginForm {
email: String,
password: String,
}
async fn admin_login(
Form(form): Form<LoginForm>,
) -> AppResult<axum::response::Redirect> {
// verify credentials với AuthService (B102+B105)
let user = verify_admin(&form.email, &form.password)
.await
.map_err(|_| AppError::Unauthenticated)?;
// set session cookie + redirect dashboard
Ok(axum::response::Redirect::to("/admin"))
}
Quy tắc cùng Json<T>: Form<T> consume body → phải đặt cuối arg list trong signature (B13 đã lock). Không kết hợp Form<A> + Json<B> cùng handler — body chỉ đọc được một lần.
Decision lock cho Shop API:
- API endpoints (mobile/SPA/partner) dùng
Json<T>theo JSON-only policy (đã lock B5 + B6) — KHÔNG dùngFormcho endpoint/api/v1/*. - Form extractor CHỈ dùng cho hai use case riêng biệt: (a) admin dashboard login page render server-side template (B105) — POST
/admin/loginnhậnapplication/x-www-form-urlencodedtheo HTML form convention; (b) Stripe webhookPOST /api/v1/webhooks/stripe(B197) — Stripe gửiapplication/x-www-form-urlencodedtheo convention provider, không thay đổi được. - Cả hai use case này tách rõ ngoài resource API thường — không lẫn vào CRUD endpoint chính.
erased-json — Serialize Trait Object
Vấn đề Rust thường gặp: derive Serialize không work trực tiếp với Box<dyn Trait> (trait object). Trait serde::Serialize không object-safe — method serialize<S: Serializer>(...) generic, không cho phép virtual dispatch qua vtable.
// Compile error — Serialize không object-safe
let notifications: Vec<Box<dyn Serialize>> = vec![/* ... */];
Crate erased-serde giải bài toán này qua erased_serde::Serialize — phiên bản object-safe của trait. axum-extra cung cấp ErasedJson để response trait object qua JSON:
use axum_extra::response::ErasedJson;
async fn dynamic_response() -> ErasedJson {
let value: Box<dyn erased_serde::Serialize> = make_payload();
ErasedJson::new(&*value)
}
Use case: polymorphic response list chứa nhiều variant struct khác nhau hoàn toàn (Email + Push + SMS notification — mỗi loại field khác nhau hoàn toàn không gộp được qua enum).
Shop API decision: KHÔNG dùng erased-json. Lý do — đã lock pattern tagged enum ở B15 (JSON Serialization Với serde + axum::Json) cho polymorphism:
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Notification {
Email { to: String, subject: String },
Push { device_id: String, body: String },
Sms { phone: String, text: String },
}
Tagged enum serialize sang shape {"type": "email", "to": "...", "subject": "..."} đủ cho mọi domain Shop API. Trade-off: phải list hết variant compile-time (closed set) — không thêm variant động runtime. Domain notification Shop API biết trước 3-5 variant không đổi → enum là lựa chọn đúng. Mention erased-json để bạn biết khi gặp use case plugin system hoặc dynamic variant runtime, không phải để áp dụng vào Shop API.
Query Với Defaults — Query<T> Extension
axum core Query<T> parse query string strict — field missing trong URL nghĩa là deserialize fail nếu type không phải Option<T>. axum-extra mở rộng pattern qua OptionalQuery<T> (entire query có thể missing → None) và một số helper khác.
Pattern dùng phổ biến nhất cho pagination Shop API là kết hợp Query<T> core + serde(default) attribute:
use axum::extract::Query;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Pagination {
#[serde(default = "default_page")]
pub page: u32,
#[serde(default = "default_size")]
pub size: u32,
}
fn default_page() -> u32 { 1 }
fn default_size() -> u32 { 20 }
async fn list_products(
Query(p): Query<Pagination>,
) -> String {
format!("page={} size={}", p.page, p.size)
}
Behavior:
GET /products→page=1 size=20(mặc định).GET /products?page=3→page=3 size=20(size fallback default).GET /products?size=50→page=1 size=50(page fallback default).GET /products?page=3&size=50→ giá trị explicit.
Cần thêm constraint range thì validate ở B41 (JSON Extract + Validation Với validator Crate) — sửa #[derive(Validate)] với #[validate(range(min = 1, max = 1000))] trên field.
Pattern này lock cho mọi list endpoint Shop API ở B64 List Resources Với Pagination — DTO Pagination { page, size } đặt trong crates/shop-common/src/pagination.rs (chưa create, sẽ tạo B64), reuse cross-resource list_products, list_orders, list_reviews, admin_list_*.
Add axum-extra Vào workspace.dependencies
Cập nhật Cargo.toml root để lock version cross-workspace — pattern đã apply từ B10 cho mọi crate share. File shop/Cargo.toml thêm dòng axum-extra vào [workspace.dependencies]:
# File: shop/Cargo.toml (workspace root)
[workspace.dependencies]
# ... existing entries (axum 0.8, tokio, tower, ...)
axum = "0.8"
axum-extra = { version = "0.10", features = [
"cookie",
"cookie-private",
"cookie-signed",
"typed-header",
"form",
"query",
] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
# ... rest
Quan sát:
- 6 feature flag opt-in đặt thẳng ở workspace root để mọi crate consume cùng version + cùng feature set. Member crate (
shop-api,shop-workertương lai) reference quaaxum-extra.workspace = true— feature đã định sẵn ở root, không phải khai báo lại. - KHÔNG add vào
crates/shop-api/Cargo.tomlngay — bài này chỉ lock workspace-level. Feature thực tế enable dần khi handler đầu tiên cần dùng:cookieở B106 (admin login flow),typed-headerở B112 (JWT extract),formở B105 (admin login page POST). - Pin version 0.10 align axum 0.8 đã lock từ B10. Khi axum bump major (0.9 hoặc 1.0), axum-extra bump tương ứng — luôn nâng đôi.
Verify workspace vẫn build sau khi thêm dep (chưa có handler dùng thì chỉ resolve dep, không link vào binary):
cd shop
cargo check --workspace
# Compiling axum-extra v0.10.x
# Finished `dev` profile [unoptimized + debuginfo] target(s)
Khi handler đầu tiên cần axum-extra (B106 admin login), crates/shop-api/Cargo.toml thêm dòng:
[dependencies]
axum-extra = { workspace = true }
Feature flag không phải lặp lại vì đã lock ở workspace root — đây là lý do dùng workspace.dependencies thay vì khai báo riêng từng crate.
Tổng Kết
- axum-extra = extension features cho axum core (cookie, typed-header, form, query helper, erased-json) — official maintained bởi cùng team tokio-rs, repo cùng workspace với axum.
workspace.dependencieslock:axum-extra = "0.10"với 6 feature opt-incookie+cookie-private+cookie-signed+typed-header+form+query.- CookieJar 3 loại:
CookieJarplain (free, preference UI),SignedCookieJarHMAC SHA256 (tamper-proof, client đọc được),PrivateCookieJarAES-256-GCM (encrypted, client không đọc được). - Shop API:
SignedCookieJarcho CSRF token (B107),PrivateCookieJarcho admin session (B105), API client dùng Bearer JWT (đã lock B4). TypedHeader<T>strongly-typed extract:Authorization<Bearer>,Authorization<Basic>,ContentType,UserAgent,Host,Cookie— compiler enforce header tồn tại và format đúng, không lookup string raw quaHeaderMap.Form<T>extractor choapplication/x-www-form-urlencoded— Shop API chỉ dùng cho admin dashboard login page (B105) + Stripe webhook (B197), API endpoint giữJson<T>JSON-only.erased-jsongiải bài toán serialize trait object quaBox<dyn ErasedSerialize>— Shop API KHÔNG dùng vì tagged enum B15 đã đủ polymorphic cho domain.- Pattern pagination:
Query<T>core +serde(default = "fn")+ helper function trả default value (B64). - Khi nào enable feature:
cookie-private+formở B105 (admin login),cookie-signedở B107 (CSRF),typed-headerở B112 (JWT extract),cookiestandalone ở B106 (admin session). - Encryption key 32+ bytes random, sinh qua
Key::generate()dev, prod đọc từ envCOOKIE_KEYbase64 — rotation theo lịch ở B289 (key management).
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- 3 loại CookieJar trong axum-extra khác nhau ở điểm gì (cơ chế bảo vệ, khả năng đọc/sửa từ client)? Khi nào dùng cái nào trong Shop API?
TypedHeader<Authorization<Bearer>>extract gì từ request? Tại sao tốt hơnHeaderMapraw parse manual?Form<T>extractor khácJson<T>ra sao về Content-Type body parse? Shop API dùngFormcho 2 use case nào và tại sao API endpoint chính giữJson<T>?- axum-extra lock version 0.10 trong
workspace.dependencies. Tại sao 0.10 mà không bản mới hơn? Khi nào nâng version? - Shop API có dùng
erased-jsonkhông? Pattern thay thế là gì và tại sao đủ cho domain Shop?
Đáp án
- Plain
CookieJar— cookie không bảo vệ, server lưu raw value, client đọc và sửa tự do qua DevTools hoặcdocument.cookieJavaScript. Dùng cho preference UI không nhạy cảm: dark mode, language, sidebar collapsed state, cookie consent banner. Sửa tay không phá vỡ security vì dữ liệu không liên quan auth.SignedCookieJar— server ký HMAC SHA256 phía sau giá trị, cookie thành<value>.<hmac>. Client đọc được giá trị (vẫn plaintext) nhưng không sửa được — đổi giá trị mà không tính lại HMAC thì server verify fail và reject. Shop API dùng cho CSRF token (B107) — client cần đọc để inject headerX-CSRF-Tokenkhi submit form/AJAX, nhưng không được sửa.PrivateCookieJar— server AES-256-GCM encrypt + sign nội dung. Client KHÔNG đọc được (DevTools chỉ thấy bytes random base64), không sửa được. Shop API dùng cho session ID admin dashboard (B105) — admin login qua HTML form → server set session cookie encrypted → request kế server decrypt + lookup Redis session. Cả Signed và Private đều cầnKeyinstance 32+ bytes random server-side, share cross-instance qua secret manager khi scale horizontal. API client (mobile/SPA/partner) KHÔNG dùng cookie — giữ Bearer JWT (đã lock B4) đơn giản hơn cho cross-origin và mobile. TypedHeader<Authorization<Bearer>>extract headerAuthorization: Bearer <token>từ request, strip prefix "Bearer " tự động, expose.token() -> &strtrả token raw. Compiler check tại signature handler qua trait boundheaders::Header— nếu header missing hoặc sai format (vd "Basic xxx" thay vì "Bearer xxx"), axum-extra reject với 400 Bad Request response trước khi handler chạy. So vớiHeaderMapraw, lợi điểm: (a) type-safe — không lookup string "authorization" dễ sai chính tả (vd "Authorisation" UK spelling); (b) parse + validate tự động — không phải.get().and_then(|v| v.to_str().ok()).and_then(|s| s.strip_prefix("Bearer "))mỗi handler; (c) reuse cross-handler — validation logic một lần, dùng nhiều nơi; (d) document tự động OpenAPI qua utoipa derive (B8) — schema generate biết handler yêu cầu Authorization header; (e) compiler enforce signature đúng — quên header thì compile error tạiget(handler), không phải runtime bug. Pattern lock Shop API: B112 dùng cho JWT verify, B33 cho customIdempotency-Keytheo Stripe convention (đã lock B4).Form<T>parse body Content-Typeapplication/x-www-form-urlencodedqua crateserde_urlencodeddưới hood — formatkey1=value1&key2=value2URL-encoded (space thành %20, ký tự đặc biệt thành %XX).Json<T>parse Content-Typeapplication/jsonquaserde_json— format JSON object{"key1": "value1", "key2": "value2"}nested. Cả hai consume body một lần → cuối arg list signature handler (đã lock B13). Shop API dùngForm<T>CHỈ 2 use case: (a) Admin dashboard login page (B105) — POST/admin/loginnhận form submit từ HTML page render server-side (template askama/maud); browser submit form thì tự gửiapplication/x-www-form-urlencodedmặc định, không phải JSON. (b) Stripe webhook (B197) —POST /api/v1/webhooks/stripenhận event payload từ Stripe theo conventionapplication/x-www-form-urlencodedprovider quy định, không thay đổi được. API endpoint chính (/api/v1/*) giữJson<T>vì: (1) JSON-only policy đã lock B5 + B6 cho consumer mobile/SPA/partner — họ gửi JSON tự nhiên; (2) JSON cho phép nested object, array, typed value (boolean, number) — form chỉ flat key-value string; (3) ecosystem tool (Postman, curl với-H 'Content-Type: application/json') default JSON; (4) OpenAPI schema (B8) document JSON body chính xác hơn URL-encoded.- Lock
axum-extra = "0.10"vì alignaxum = "0.8"đã lock từ B10. axum-extra release theo cùng team tokio-rs với axum, version 0.10 là phiên bản tương thích với axum 0.8 — chéo version (vd axum 0.8 với axum-extra 0.9) gây compile error trait bound không match. Pattern chuẩn: luôn nâng cặp axum + axum-extra cùng nhau. Lý do không lấy bản mới hơn tại thời điểm B19: (a) axum 0.8 là latest stable Q4 2026, axum-extra 0.10 align; (b) workspace.dependencies pattern (lock B10) yêu cầu version stable + reproducible build cross-machine + cross-CI — bump tùy ý gây inconsistency. Khi nào nâng version: (1) CVE bảo mật — patch bắt buộc, audit changelog kỹ, bump kèm test e2e đầy đủ; (2) tokio-rs release major (axum 0.9 hoặc 1.0) — nâng kèm migration code theo upgrade guide, bump axum-extra tương ứng cùng PR; (3) feature mới cần thiết mà bản hiện tại không có — review trade-off với cost migrate. Minor version bump (0.10.x → 0.10.y) trong cùng minor được phép trong PR audit; major bump cần spike riêng đánh giá impact toàn workspace. - Shop API KHÔNG dùng
erased-json. Pattern thay thế đã lock từ B15 (JSON Serialization Với serde + axum::Json) là tagged enum với serde attribute#[serde(tag = "type")]hoặc#[serde(tag = "type", content = "data")]— dùng choNotificationvới 3-5 variant (Email, Push, Sms, In-App, Webhook) khai báo compile-time. Tagged enum serialize sang JSON shape{"type": "email", "to": "...", "subject": "..."}đủ cho polymorphic response. Đủ cho domain Shop API vì: (a) tập variant closed set biết trước compile-time — domain notification cố định 3-5 loại, không thêm runtime; (b) type-safe — pattern matching exhaustive ở mọi nơi consume, miss variant compile error chỉ thẳng vào dòng match; (c) serde derive built-in — không kéo thêm crateerased-serde, dependency tree nhỏ hơn; (d) OpenAPI schema (B8) generate chính xác — utoipa biết list variant + field từng variant, document đẹp; (e) performance tốt hơn — không virtual dispatch qua vtable, serialize trực tiếp. Use case duy nhất cầnerased-jsonlà plugin system runtime hoặc dynamic variant runtime không biết trước compile-time (vd CMS field schema editor, workflow engine custom step) — Shop API không có nhu cầu này. Mentionerased-jsontrong bài để bạn biết khi gặp domain plugin-based ở dự án khác, không phải áp dụng vào Shop.
Bài Tiếp Theo
Bài 20: Hệ Sinh Thái Axum: tower-http, tower, hyper — bài cuối Group 2: layer stack hyper → tower → axum, tower-http middleware (cors, trace, compression), tonic gRPC cùng base, ecosystem map cho hiểu axum nằm đâu trong Rust async web ecosystem.
