Mục lục
- Mục Tiêu Bài Học
- String / &'static str — text/plain
- Html<T> — text/html
- Json<T> — Cốt Lõi REST API
- StatusCode Standalone — Status Only, No Body
- Tuple (StatusCode, body) Và (StatusCode, HeaderMap, body)
- Custom IntoResponse Implementation
- Decision Matrix: Khi Nào Dùng Cái Nào
- Apply Vào shop-api: Mini Preview Handler Set
- 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ẽ:
- Biết khi nào dùng
Stringvà&'static str(text/plain) — chỉ phù hợp cho infrastructure handler đơn giản, không phù hợp cho data endpoint của REST API. - Biết khi nào dùng
Html<T>(text/html) — dành riêng cho render server-side template hoặc Swagger UI page, kèm cảnh báo XSS khi embed user input. - Nắm
Json<T>đầy đủ — response chính của REST API, vai trò kép vừa response (T: Serialize) vừa request extractor (T: DeserializeOwned), Content-Type tự setapplication/json; charset=utf-8. - Hiểu
StatusCodestandalone trả body rỗng (204/202) và tuple(StatusCode, body),(StatusCode, HeaderMap, body)cho custom status + header với thứ tự cố định. - Implement custom
IntoResponsecho struct domain riêng — chuẩn bị nền tảng choAppErrorimpl ở B16. - Apply vào Shop API: response envelope chuẩn theo endpoint, error format envelope, decision matrix per endpoint pattern cho cả series.
String / &'static str — text/plain
Hai type String và &'static str là response type đơn giản nhất — axum impl IntoResponse sẵn với behavior mặc định:
- Content-Type:
text/plain; charset=utf-8(axum set tự động, charset bắt buộc vì JSON/text spec yêu cầu UTF-8). - Status code: 200 OK mặc định, không override được trừ khi wrap qua tuple.
- Body: bytes UTF-8 của chuỗi.
Use case hợp lệ cho String/&str trong handler:
- Health check minimal — endpoint cũ kỹ trả
"ok"cho load balancer (đã thay bằng JSON envelope ở B12 theo policy B5). - Version banner dạng plain text — debug endpoint.
- Debug echo — endpoint print path/header cho dev.
- Robots.txt, sitemap dạng đơn giản — file static phục vụ qua handler.
Code snippet handler trả String và &'static str:
use axum::response::IntoResponse;
// &'static str — chuỗi literal compile-time
async fn ping() -> &'static str {
"pong"
}
// String — chuỗi build runtime
async fn echo_path(path: String) -> impl IntoResponse {
format!("path: {}", path)
}
// Multi-line, vẫn text/plain
async fn banner() -> &'static str {
"shop-api v0.1.0\nbuild: 2026-06-12\nedition: 2024"
}
Shop API quyết định: KHÔNG dùng raw String hay &str cho bất kỳ data endpoint nào — đã lock policy JSON-only từ B5 và confirm lại ở B12 khi đổi /health từ text "ok" sang Json(json!({"status":"ok"})). Lý do: client tooling expect parse JSON cross-endpoint, monitoring agent (Prometheus exporter, Datadog) cần body structured, browser tab show plain text trông không production-quality. Plain string chỉ giữ kiến thức để bạn hiểu type built-in của axum — không xuất hiện trong codebase Shop API tương lai trừ một trường hợp duy nhất là endpoint GET / banner (vốn không phải data endpoint).
Html<T> — text/html
axum cung cấp wrapper Html<T> trong module axum::response, type parameter T phải implement Into<Body> (vd String, &'static str, Vec<u8>). Behavior mặc định:
- Content-Type:
text/html; charset=utf-8— axum tự set khi bạn wrap value quaHtml(...). - Status code: 200 OK mặc định.
- Body: bytes của HTML string.
Use case hợp lệ cho Html<T> trong REST API:
- Render server-side template — askama (compile-time template), maud (Rust DSL), tera (Jinja2-like). Endpoint trả HTML page hoàn chỉnh cho email confirmation, error page user-friendly, admin dashboard nội bộ.
- Swagger UI / ReDoc / Scalar page — render UI document API từ spec OpenAPI (đã lock B8 stack utoipa + utoipa-swagger-ui).
- Email verification landing page — user click link trong email, server render trang xác nhận thành công kèm CTA back to app.
Code snippet handler render mini template:
use axum::response::Html;
// HTML string literal — debug/dev
async fn admin_banner() -> Html<&'static str> {
Html("<h1>Shop Admin</h1><p>Welcome back.</p>")
}
// HTML build runtime từ template
async fn verify_success(email: String) -> Html<String> {
let html = format!(
"<!DOCTYPE html><html><body>\
<h1>Email Verified</h1>\
<p>Account <strong>{}</strong> đã active.</p>\
<a href=\"/app\">Vào Shop</a>\
</body></html>",
// CẢNH BÁO: chỗ này phải escape user input trước khi inject
html_escape::encode_text(&email)
);
Html(html)
}
Lưu ý bảo mật XSS: KHÔNG bao giờ embed user input chưa escape vào HTML output. Tấn công XSS (Cross-Site Scripting) cho phép attacker inject <script> chạy trên trình duyệt nạn nhân, đánh cắp cookie session. Template engine như askama auto-escape mặc định; nếu build HTML thủ công bằng format!, phải gọi escape helper (html_escape::encode_text) cho mọi giá trị động — KHÔNG có ngoại lệ.
Shop API quyết định: Html<T> chỉ dùng cho hai trường hợp — (1) Swagger UI page /swagger-ui mount qua utoipa-swagger-ui ở dev/staging (đã lock B8), (2) email verification landing page /auth/verify-email/landing. Toàn bộ API data thuần JSON. Không có handler resource nào trả Html<T> — REST API thuần backend, frontend SPA tự render trên client.
Json<T> — Cốt Lõi REST API
Wrapper axum::Json<T> là response type quan trọng nhất của Shop API — mọi resource handler đều trả qua type này. Một điểm thiết kế đáng chú ý: cùng tên struct Json<T> đóng hai vai trò đối xứng nhau:
- Response:
Json<T>vớiT: Serialize— wrap struct DTO, axum serialize quaserde_json::to_vecrồi build response với Content-Type tự set. - Request extractor:
Json<T>vớiT: DeserializeOwned— đặt làm argument handler, axum đọc body request, parse quaserde_json::from_slicevào struct.
Behavior mặc định khi dùng làm response:
- Content-Type:
application/json; charset=utf-8— axum set tự động, charset bắt buộc theo RFC 8259 (JSON UTF-8 default). - Status code: 200 OK mặc định, override qua tuple
(StatusCode, Json<T>). - Body: bytes JSON sau khi
serde_jsonserialize.
Code snippet handler trả Json<T> đơn giản:
use axum::Json;
use serde::Serialize;
use serde_json::json;
#[derive(Serialize)]
struct ProductDto {
id: u64,
name: String,
price: String,
}
// Handler đơn giản — trả Json wrap struct DTO
async fn get_product() -> Json<ProductDto> {
Json(ProductDto {
id: 42,
name: "Demo Laptop".into(),
price: "1499.00".into(),
})
}
// Shortcut ad-hoc qua macro serde_json::json! — không cần define struct
async fn get_status() -> Json<serde_json::Value> {
Json(json!({
"status": "ok",
"timestamp": "2026-06-12T14:30:00Z"
}))
}
Macro serde_json::json! tiện cho ad-hoc response không define struct trước — phù hợp cho infrastructure endpoint (/health, /version ở B12 đã dùng pattern này). Tuy nhiên cho resource handler, luôn define struct DTO rõ ràng vì utoipa cần ToSchema derive để sinh OpenAPI spec (lock B8).
Pattern Shop API: handler resource từ B16 onward dùng signature AppResult<Json<T>> — đã lock từ B13. Đây là pattern xuyên suốt: Result wrap quanh Json, vế Ok(Json(dto)) trả 200 + body, vế Err(AppError) trả status mapping tương ứng (404/401/422/...) qua AppError impl IntoResponse ở B16.
StatusCode Standalone — Status Only, No Body
Type http::StatusCode tự implement IntoResponse — response trả về có status code đúng giá trị, body rỗng, không có Content-Type. Phù hợp cho endpoint mà semantic là "thành công không cần trả gì thêm":
StatusCode::NO_CONTENT(204) — DELETE thành công, RFC 9110 mục 15.3.5 cấm body trong 204. Reset password OK, mark notification read OK.StatusCode::ACCEPTED(202) — request đã nhận, xử lý async; thường kèm body chứajob_idqua tuple (xem section 6) chứ ít khi standalone.StatusCode::CREATED(201) — về lý có thể standalone, nhưng convention REST là kèm body entity mới +Locationheader, nên cũng dùng tuple.
Code snippet handler DELETE trả 204:
use axum::{extract::Path, http::StatusCode};
// Handler DELETE — trả 204 standalone
async fn delete_cart_item(Path(_item_id): Path<u64>) -> StatusCode {
// Logic xóa item khỏi cart
// Thành công → trả 204 No Content
StatusCode::NO_CONTENT
}
// Handler reset password OK — không cần trả gì thêm
async fn confirm_password_reset() -> StatusCode {
StatusCode::NO_CONTENT
}
Pattern Shop API: handler DELETE dùng signature AppResult<StatusCode> — đã lock từ B13. Vế Ok(StatusCode::NO_CONTENT) trả 204; vế Err(AppError::NotFound) trả 404 nếu resource không tồn tại. Mọi handler DELETE trong Shop API (vd DELETE /api/v1/cart/items/:item_id, DELETE /api/v1/me/addresses/:id) follow pattern này.
Pitfall: KHÔNG gửi body kèm 204 — một số HTTP client (curl với verbose) sẽ cảnh báo "no Content-Length but body present" hoặc cache layer (Varnish) drop body âm thầm. axum tự enforce điều này khi bạn return StatusCode standalone — không có cách trộn body vào trong cùng response chuẩn.
Tuple (StatusCode, body) Và (StatusCode, HeaderMap, body)
Khi cần custom status code và/hoặc custom header kèm body, axum cho phép return tuple với hai dạng phổ biến:
- 2-element tuple
(StatusCode, body)— override status, body giữ Content-Type của chính nó (vdJson<T>giữapplication/json). - 3-element tuple
(StatusCode, HeaderMap, body)— thêm nhiều header tùy ý (Location, Cache-Control, X-Request-Id, ...).
Use case chính: POST create cần trả 201 + Location: /api/v1/<resource>/<id> + body entity mới (đã lock B3 status code policy). Code snippet đầy đủ:
use axum::{
http::{header::LOCATION, HeaderMap, HeaderValue, StatusCode},
Json,
};
use serde::Serialize;
use shop_common::error::AppResult;
#[derive(Serialize)]
struct ProductDto {
id: u64,
name: String,
price: String,
}
// Handler POST create — trả 201 + Location + Json body
async fn create_product() -> AppResult<(StatusCode, HeaderMap, Json<ProductDto>)> {
// Logic INSERT vào DB, lấy id mới
let product = ProductDto {
id: 42,
name: "Demo Laptop".into(),
price: "1499.00".into(),
};
let mut headers = HeaderMap::new();
let location = format!("/api/v1/products/{}", product.id);
headers.insert(LOCATION, HeaderValue::from_str(&location).unwrap());
Ok((StatusCode::CREATED, headers, Json(product)))
}
// Variant 2-element — chỉ override status, không thêm header
async fn create_simple() -> (StatusCode, Json<ProductDto>) {
let product = ProductDto {
id: 1,
name: "Demo".into(),
price: "10.00".into(),
};
(StatusCode::CREATED, Json(product))
}
Thứ tự tuple cố định: (StatusCode, HeaderMap, body) — không đảo được. axum impl IntoResponse cho tuple với type position cụ thể; nếu bạn viết (HeaderMap, StatusCode, body) hoặc (body, StatusCode), compile error "the trait IntoResponse is not implemented for this tuple". Đây là compile-time check chứ không phải runtime — rất an toàn. Memorize thứ tự: status → headers → body, từ "outer wrapping" vào "inner content".
Pattern Shop API: handler POST create dùng signature AppResult<(StatusCode, HeaderMap, Json<T>)> — đã lock từ B13. Handler async action (vd POST /api/v1/checkout trả 202 + job_id) dùng signature AppResult<(StatusCode, Json<T>)> với 2-element tuple vì không cần custom header.
Custom IntoResponse Implementation
Khi struct domain riêng (vd error type, custom response wrapper) cần trả về từ handler, bạn impl IntoResponse cho struct đó. axum sẽ gọi method .into_response() tự động sau khi handler return — đúng pattern như mọi built-in type.
Pattern cơ bản cho wrapper struct envelope chung:
use axum::{
response::{IntoResponse, Response},
Json,
};
use serde::Serialize;
// Wrapper response envelope chung — gắn metadata (request_id) vào mọi response
pub struct ApiResponse<T> {
pub data: T,
pub request_id: String,
}
impl<T: Serialize> IntoResponse for ApiResponse<T> {
fn into_response(self) -> Response {
// Delegate qua Json wrapper — tận dụng IntoResponse có sẵn
Json(serde_json::json!({
"data": serde_json::to_value(self.data).unwrap_or(serde_json::Value::Null),
"request_id": self.request_id,
}))
.into_response()
}
}
Pattern quan trọng hơn cho Shop API là AppError impl IntoResponse — chuẩn bị nền tảng cho B16. Code preview để bạn hình dung shape, chi tiết đầy đủ sẽ ở B16:
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use shop_common::error::AppError;
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, code) = match &self {
AppError::BadRequest(_) => (StatusCode::BAD_REQUEST, "BAD_REQUEST"),
AppError::Unauthenticated => (StatusCode::UNAUTHORIZED, "UNAUTHENTICATED"),
AppError::Forbidden => (StatusCode::FORBIDDEN, "FORBIDDEN"),
AppError::NotFound => (StatusCode::NOT_FOUND, "NOT_FOUND"),
AppError::Conflict(_) => (StatusCode::CONFLICT, "CONFLICT"),
AppError::Validation(_) => (StatusCode::UNPROCESSABLE_ENTITY, "VALIDATION"),
AppError::RateLimited => (StatusCode::TOO_MANY_REQUESTS, "RATE_LIMITED"),
// ... 4 variants khác (MethodNotAllowed, Internal, Upstream, Unavailable)
_ => (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL"),
};
let body = Json(serde_json::json!({
"error": self.to_string(),
"code": code,
"request_id": "TODO", // sẽ inject từ Extension<RequestId> ở B39
}));
(status, body).into_response()
}
}
Hai điểm cần làm rõ về code preview này:
- Mapping
AppError→ HTTP status đã lock từ B3 (11 variants → status code tương ứng). Code preview ở section này chỉ minh họa pattern; đầy đủ 11 variants với header phụ trợ (WWW-Authenticate,Retry-After,Allow) sẽ chi tiết ở B16. - Field
request_idhiện đang là placeholder"TODO"— sẽ inject từExtension<RequestId>middleware ở B39 (Extension extractor). Body envelope chuẩn{ "error", "code", "request_id" }đã lock từ B3.
Shop API quyết định: AppError impl IntoResponse bật ở B16 (file crates/shop-api/src/responses/error.rs) — sau bài đó mọi handler return AppResult<Json<T>> mới compile được. Wrapper ApiResponse<T> optional cho metadata envelope — Shop API ban đầu sẽ dùng Json<T> trực tiếp cho đơn giản, evaluate sau xem có cần wrap qua envelope ở G14 (Response Builder Pattern B40) không.
Decision Matrix: Khi Nào Dùng Cái Nào
Bảng quyết định response type per endpoint pattern — lock cho mọi handler Shop API:
Tình huống │ Response Type
────────────────────────────────────────┼──────────────────────────────────────────────
GET /products → list collection │ Json<ListResponse<ProductDto>>
GET /products/:slug → single entity │ Json<ProductDto>
POST /products → 201 + Location │ (StatusCode::CREATED, HeaderMap, Json<ProductDto>)
PUT /products/:id → updated entity │ Json<ProductDto>
PATCH /products/:id → updated entity │ Json<ProductDto>
DELETE /products/:id → 204 │ StatusCode::NO_CONTENT
POST /checkout → 202 + job_id │ (StatusCode::ACCEPTED, Json<CheckoutResponse>)
POST /orders/:id/cancel → state mới │ Json<OrderDto>
GET /health → infrastructure │ Json(json!({"status":"ok"})) per policy B5
GET /swagger-ui → document UI │ Html<String> (qua utoipa-swagger-ui mount)
Error path bất kỳ │ AppError → IntoResponse (B16)
Bảng trên áp dụng cho mọi handler Shop API từ G7 (B61 CRUD Cơ Bản) onward. Mỗi entity (Product, Category, Cart, Order, ...) follow đúng matrix này — không sáng tạo riêng từng resource.
Ba quyết định nguyên tắc đi kèm matrix:
- JSON-only policy (lock B5): KHÔNG dùng raw
Stringhay&strcho data endpoint. Mọi response data envelope quaJson<T>. Plain text chỉ giữ kiến thức axum, không có handler resource nào của Shop API trả raw string. Html<T>giới hạn: chỉ dùng cho Swagger UI mount (/swagger-uiở dev/staging) và email verification landing page. Mọi UI khác là responsibility của frontend SPA, không phải REST API.- Tuple thứ tự cố định:
(StatusCode, HeaderMap, body)— compile-time enforce. Memorize: status → headers → body, không đảo, không skip.
Đây là decision matrix lock vĩnh viễn — cập nhật _plans/shop-state.md note "Response Type Decision Matrix" để mọi bài sau reference đồng nhất.
Apply Vào shop-api: Mini Preview Handler Set
Code preview các handler Shop API sẽ có khi G7 implement, mỗi handler theo signature lock B13 + response type B14:
use axum::{
extract::{Path, Query, State},
http::{header::LOCATION, HeaderMap, HeaderValue, StatusCode},
Json,
};
use serde::{Deserialize, Serialize};
use shop_common::error::AppResult;
#[derive(Clone)]
struct AppState; // define đầy đủ ở B28
#[derive(Deserialize)]
struct ProductFilter { page: u32, size: u32 }
#[derive(Deserialize)]
struct CreateProductDto { name: String, price: String }
#[derive(Deserialize)]
struct CheckoutDto { cart_id: u64 }
#[derive(Serialize)]
struct ProductDto { id: u64, name: String, price: String }
#[derive(Serialize)]
struct ListResponse<T> { items: Vec<T>, total: u64 }
#[derive(Serialize)]
struct CheckoutResponse { job_id: String, poll_url: String }
// GET /api/v1/products → list collection
async fn list_products(
State(_state): State<AppState>,
Query(_filter): Query<ProductFilter>,
) -> AppResult<Json<ListResponse<ProductDto>>> {
Ok(Json(ListResponse { items: vec![], total: 0 }))
}
// POST /api/v1/products → 201 + Location + entity mới
async fn create_product(
State(_state): State<AppState>,
Json(_dto): Json<CreateProductDto>,
) -> AppResult<(StatusCode, HeaderMap, Json<ProductDto>)> {
let product = ProductDto {
id: 42,
name: "demo".into(),
price: "100".into(),
};
let mut headers = HeaderMap::new();
let location = format!("/api/v1/products/{}", product.id);
headers.insert(LOCATION, HeaderValue::from_str(&location).unwrap());
Ok((StatusCode::CREATED, headers, Json(product)))
}
// DELETE /api/v1/cart/items/:item_id → 204 No Content
async fn delete_cart_item(
State(_state): State<AppState>,
Path(_item_id): Path<u64>,
) -> AppResult<StatusCode> {
Ok(StatusCode::NO_CONTENT)
}
// POST /api/v1/checkout → 202 Accepted + job_id polling URL
async fn checkout(
State(_state): State<AppState>,
Json(_dto): Json<CheckoutDto>,
) -> AppResult<(StatusCode, Json<CheckoutResponse>)> {
let job_id = "job_01J5K6XYZ".to_string();
let response = CheckoutResponse {
poll_url: format!("/api/v1/checkout/jobs/{}", job_id),
job_id,
};
Ok((StatusCode::ACCEPTED, Json(response)))
}
Bốn handler trên đại diện 4 dạng response type lock cho Shop API: Json<ListResponse<T>> cho list, tuple 3-element cho create, StatusCode cho delete, tuple 2-element cho action async. Mọi resource entity sau này (Category, Cart, Order, Review, Payment, ...) follow đúng matrix B14.
Code preview ở section này tạm chưa compile được vì AppError impl IntoResponse sẽ bật ở B16. Sau B16, các signature AppResult<Json<T>>, AppResult<StatusCode>, AppResult<(StatusCode, HeaderMap, Json<T>)> chạy thật được trong codebase. Suggested commit khi B16 done: "B16: implement AppError IntoResponse cho mapping 11 variants → HTTP status".
Tổng Kết
- axum cung cấp 8 built-in response type implementor phổ biến:
&str,String,Html<T>,Json<T>,StatusCode, tuple(StatusCode, body)+(StatusCode, HeaderMap, body),Response<Body>,Result<T, E>. - REST API ưu tiên
Json<T>cho data response (Content-Type tự setapplication/json; charset=utf-8),StatusCodecho 204/202 không body, tuple cho 201 +Locationheader. Html<T>chỉ dùng cho Swagger UI / email verification landing page — KHÔNG cho data endpoint; cảnh báo XSS khi embed user input chưa escape.- Custom
impl IntoResponsecho struct domain riêng — patternAppErrorsẽ ở B16, wrapperApiResponse<T>optional cho envelope chung evaluate ở B40. - Shop API JSON-only confirm: KHÔNG dùng raw
Stringcho data endpoint,Json(json!({"status":"ok"}))thay"ok"text — align policy B5 đã lock từ B12. - Tuple thứ tự cố định:
(StatusCode, HeaderMap, body)— compile-time check, không đảo được, memorize status → headers → body. - Decision matrix per endpoint lock vĩnh viễn: list/read →
Json, create → tuple 3-element +Location, update →Jsonhoặc 204, delete → 204, action async → tuple 2-element 202 + Jsonjob_id, infrastructure →Jsonenvelope, swagger →Html, error →AppErrorIntoResponse.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Phân biệt
Json<T>(response) vàJson<T>(extractor request body). Cùng tên struct nhưng vai trò khác nhau ra sao? Trait bound trênTở hai vai trò là gì? - Handler POST tạo product cần return code 201 +
Locationheader. Viết signature đầy đủ kèm import cần thiết. - DELETE thành công không trả body. Return type nào phù hợp? Status code nào? Tại sao RFC 9110 cấm body trong status đó?
- Custom struct
ApiResponse<T>muốn dùng làm response type của handler. Phải implement trait gì? Method nào? Liệt kê đầy đủ signature của method. - Tại sao Shop API không dùng raw
Stringcho data endpoint? Policy nào quyết định? Đưa ví dụ cụ thể về handler đã chuyển từ raw text sang JSON envelope.
Đáp án
- Cùng tên struct
axum::Json<T>đóng hai vai trò đối xứng: (a) Response:Json<T>vớiT: Serialize— wrap struct DTO, axum serialize quaserde_json::to_vec, set Content-Typeapplication/json; charset=utf-8và status 200, trả response. Handler returnJson(ProductDto { ... }). (b) Request extractor:Json<T>vớiT: DeserializeOwned— đặt làm argument handler, axum đọc body request bytes, parse quaserde_json::from_slicevào struct, reject 422 nếu parse fail. Handler nhậnJson(dto): Json<CreateProductDto>. Trait bound khác nhau vì hướng dòng dữ liệu khác nhau: response cần serialize (struct → JSON bytes), extract cần deserialize (JSON bytes → struct). Cùng struct vì axum thiết kế đối xứng cho DX gọn — bạn import một type dùng cả hai phía. - Signature đầy đủ cho POST create product:
Pattern lock B13. Tuple thứ tự cố địnhuse axum::{ http::{header::LOCATION, HeaderMap, HeaderValue, StatusCode}, Json, }; use shop_common::error::AppResult; async fn create_product( State(state): State<AppState>, Json(dto): Json<CreateProductDto>, ) -> AppResult<(StatusCode, HeaderMap, Json<ProductDto>)> { let product = service::create(&state, dto).await?; let mut headers = HeaderMap::new(); let location = format!("/api/v1/products/{}", product.id); headers.insert(LOCATION, HeaderValue::from_str(&location)?); Ok((StatusCode::CREATED, headers, Json(product))) }(StatusCode, HeaderMap, Json<T>)— compile-time enforce. - Return type:
StatusCodestandalone — tự implementIntoResponsetrả status only, body rỗng, không Content-Type. Status code:StatusCode::NO_CONTENT(204). Lý do RFC 9110 mục 15.3.5 cấm body trong 204: "204 No Content" semantic là "request thành công, không có representation để trả" — nếu kèm body, contradict semantic, một số HTTP client (curl verbose) cảnh báo "no Content-Length but body present", cache layer (Varnish, browser) có thể drop body âm thầm gây inconsistency. axum tự enforce: bạn returnStatusCode::NO_CONTENTthì không có cách trộn body vào cùng response chuẩn. Pattern Shop API lock: handler DELETE dùngAppResult<StatusCode>, vếOk(StatusCode::NO_CONTENT)trả 204, vếErr(AppError::NotFound)trả 404. - Phải implement trait
axum::response::IntoResponse. Method:fn into_response(self) -> Response— consumeselftrả vềaxum::response::Response(alias củahttp::Response<axum::body::Body>). Signature đầy đủ:
Lưu ý: methoduse axum::response::{IntoResponse, Response}; impl<T: serde::Serialize> IntoResponse for ApiResponse<T> { fn into_response(self) -> Response { // Delegate qua type built-in có sẵn impl Json(serde_json::json!({ "data": serde_json::to_value(self.data).unwrap_or_default(), "request_id": self.request_id, })).into_response() } }into_responseconsumeself(move ownership), không phải&self. axum gọi method này tự động sau khi handler return — bạn không bao giờ gọi tay. Pattern delegate qua type built-in (Json) là cách clean nhất, tận dụng impl có sẵn thay vì buildResponse::builder()raw từ đầu. - Lý do Shop API không dùng raw
Stringcho data endpoint: (a) Client tooling expect parse JSON cross-endpoint — frontend SPA, mobile client, third-party integration đều assume mọi response data là JSON. Trả text mixed làm pipeline parse complex (phải detect Content-Type từng response). (b) Monitoring agent (Prometheus exporter, Datadog APM) cần body structured để extract metric/log field — text không cấu trúc bỏ qua. (c) Browser tab show plain text trông không production-quality cho debug. (d) Test harness viết generic dễ hơn khi mọi response cùng dạng. Policy quyết định: Content Negotiation Policy lock từ B5 — "JSON-only cho mọi data endpoint Shop API, response set Content-Typeapplication/json; charset=utf-8". Ví dụ cụ thể: handlerGET /healthở B10 ban đầu trả&'static str "ok", **B12 đã đổi sang**Json(serde_json::json!({"status":"ok"}))để align policy B5; B14 confirm lại quyết định này lock vĩnh viễn — mọi infrastructure handler tương lai (/healthz,/readyz,/metricsở G15) follow pattern JSON envelope, không trả raw text. Ngoại lệ duy nhất:GET /banner trả tuple(StatusCode::OK, "shop-api v0.1.0")— không phải data endpoint, chỉ là banner identification.
Bài Tiếp Theo
Bài 15: JSON Serialization Với serde + axum::Json — chi tiết serde derive Serialize/Deserialize, field rename #[serde(rename = "camelCase")] và rename_all cấp struct, skip_serializing_if = "Option::is_none" bỏ field None khỏi output, tagged/untagged/adjacent enum cho polymorphism, default value qua #[serde(default)], alias cho input đa dạng, áp dụng vào DTO Shop API (ProductDto, CreateProductDto, UpdateProductDto) với convention naming + JSON Schema sinh qua schemars.
