Mục lục
- Mục Tiêu Bài Học
- Rule Cốt Lõi: async fn(extractors...) → impl IntoResponse
- IntoResponse Trait — Bản Chất
- Built-In Implementor — 8 Type Phổ Biến
- Result<T, E> Pattern — Error Mapping
- Extractor — Arg Handler
- fn Return Type Freedom — 3 Pattern
- Common Pitfall + Compile Error Đọc
- Apply Vào shop-api: Pattern Xuyên Suốt
- 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 rule signature handler axum:
async fn(extractors...) -> impl IntoResponsevà lý do compiler enforce qua trait bound. - Nắm trait
IntoResponsecốt lõi — bản chất, 8 type built-in implementor phổ biến, và cách axum gọi.into_response()tự động. - Biết extractor là gì (arg của handler), preview built-in extractor sẽ deep dive ở B22-B40, rule body extractor phải đặt cuối và lý do.
- Đọc được compile error khi handler signature sai — 5 pitfall thường gặp với rustc message cụ thể và cách fix.
- Hiểu fn return type freedom của axum: 3 pattern
impl IntoResponse(simple),Response<Body>(raw full control),Result<T, E>(recommended cho real handler). - Apply vào Shop API: pattern
AppResult<Json<T>>chuẩn lock cho mọi handler resource từ B16 onward, cùng signature riêng cho list/create/update/delete và infrastructure handler.
Rule Cốt Lõi: async fn(extractors...) → impl IntoResponse
Mọi handler axum hợp lệ phải thỏa đúng 3 yêu cầu signature, không hơn không kém:
async fn— handler bắt buộc là async function. axum không support sync handler vì traitHandlerđòi returnFuture, dùngfnthường sẽ ra type không match trait bound.- Arg là extractor — mỗi argument phải có type implement trait
FromRequesthoặcFromRequestPartscủa axum. Handler không nhận arbitrary type — tất cả input đều phải "biết cách trích từ HTTP request". - Return type implement
IntoResponse— return value phải convert được sang HTTP response. Type return tự build status + header + body từ method.into_response().
Compiler enforce 3 yêu cầu trên qua trait bound trong signature của Router::route (lấy MethodRouter) và get/post/... functions (lấy type implement trait Handler). Nếu signature handler sai bất kỳ chỗ nào, lỗi compile xuất hiện ngay tại .route("/x", get(my_handler)) với message dạng "the trait Handler is not implemented" — kèm note dài về requirement (xem section 8 phân tích cụ thể).
Ví dụ 4 handler valid khác nhau cùng thỏa rule trên:
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use serde::{Deserialize, Serialize};
// Handler 0-arg, return primitive
async fn ping() -> &'static str {
"pong"
}
// Handler 1 path extractor, return tuple status + body
async fn show_product(Path(id): Path<u64>) -> impl IntoResponse {
(StatusCode::OK, format!("product {}", id))
}
// Handler 2 extractor (query + state), return Json
#[derive(Deserialize)]
struct ListFilter { page: u32 }
#[derive(Serialize)]
struct ProductDto { id: u64, name: String }
async fn list_products(
State(_state): State<AppState>,
Query(_filter): Query<ListFilter>,
) -> impl IntoResponse {
Json(vec![ProductDto { id: 1, name: "demo".into() }])
}
// Handler return Result — axum convert cả Ok và Err
async fn get_health() -> Result<Json<serde_json::Value>, StatusCode> {
Ok(Json(serde_json::json!({ "status": "ok" })))
}
#[derive(Clone)]
struct AppState;
Bốn handler trên đại diện 4 dạng signature phổ biến nhất. Phần còn lại của bài sẽ mổ xẻ IntoResponse, extractor, và 3 pattern return type chi tiết.
IntoResponse Trait — Bản Chất
Trait IntoResponse trong module axum::response có định nghĩa cốt lõi đơn giản đến mức bất ngờ:
// File: axum::response::into_response (đã giản hóa)
pub trait IntoResponse {
fn into_response(self) -> Response;
}
Đây là contract giữa handler và axum runtime: return value của handler có method .into_response() consume self và trả về axum::response::Response (alias của http::Response<axum::body::Body>). axum gọi method này tự động sau khi handler return — bạn không bao giờ phải gọi tay.
Vòng đời một request hoàn chỉnh nhìn từ góc trait:
┌──────────────────────────────────────────────────────────────┐
│ HTTP Request │
│ │ │
│ ▼ │
│ axum::serve nhận TCP connection │
│ │ │
│ ▼ │
│ Router::call(req) match path → MethodRouter │
│ │ │
│ ▼ │
│ Run từng extractor: FromRequestParts::from_request_parts(),│
│ cuối cùng FromRequest::from_request() (consume body) │
│ │ │
│ ▼ │
│ handler(extractors...).await → return value T │
│ │ │
│ ▼ │
│ T::into_response() → axum::Response │
│ │ │
│ ▼ │
│ tower service ghi response xuống TCP socket │
└──────────────────────────────────────────────────────────────┘
Lý do thiết kế: handler không cần biết về Response<Body> raw. Bạn return String, axum tự build response với Content-Type: text/plain; charset=utf-8, status 200, body là bytes UTF-8 của chuỗi đó. Bạn return Json(my_struct), axum tự serialize struct qua serde_json, set Content-Type: application/json; charset=utf-8, status 200. Mỗi type return mang theo "khả năng tự convert" — đây là ergonomic core của axum.
Tower service layer phía dưới xử lý ghi bytes ra socket, áp dụng middleware (compression, logging, tracing) — handler chỉ cần produce ra Response, không phải lo phần network. Khi bạn cần custom hành vi response, có 2 cách: (1) chọn type built-in phù hợp, (2) impl IntoResponse cho struct của bạn (deep dive ở B40 Response Builder Pattern).
Built-In Implementor — 8 Type Phổ Biến
axum cung cấp sẵn impl IntoResponse cho hàng chục type. Tám type sau bao phủ 95% use case thực tế Shop API:
&'static str→text/plain; charset=utf-8, status 200. Dùng cho chuỗi literal compile-time như"pong".String→text/plain; charset=utf-8, status 200. Dùng khi chuỗi build runtime quaformat!.Html<T>vớiT: Into<Body>→text/html; charset=utf-8, status 200. Dùng cho admin dashboard render template (askama/maud).Json<T>vớiT: Serialize→application/json; charset=utf-8, status 200. Response chính của Shop API (lock từ B5 JSON-only).StatusCode→ status only, body rỗng. Dùng choDELETEtrả 204 hoặc redirect không kèm body.- Tuple
(StatusCode, body)→ override status code, body giữ Content-Type gốc. Ví dụ(StatusCode::CREATED, Json(dto)). - Tuple
(StatusCode, HeaderMap, body)→ custom status + nhiều header + body. Dùng cho POST 201 +Locationheader (lock B3). Response<Body>→ raw response full control. Dùng khi cần manipulate trực tiếp builder pattern:Response::builder().status(...).header(...).body(...).unwrap().Result<T, E>vớiT: IntoResponsevàE: IntoResponse→ recursive: axum gọi.into_response()trên branch tương ứng. Đây là patternAppResult<T>của Shop API sau B16.
Code snippet ngắn cho từng dạng:
use axum::{
http::{HeaderMap, HeaderValue, StatusCode},
response::{Html, IntoResponse, Response},
Json,
};
use serde_json::json;
async fn plain_str() -> &'static str { "pong" }
async fn plain_string() -> String { format!("hello {}", "world") }
async fn html() -> Html<&'static str> { Html("<h1>Admin</h1>") }
async fn json() -> Json<serde_json::Value> { Json(json!({ "ok": true })) }
async fn no_body() -> StatusCode { StatusCode::NO_CONTENT }
async fn created() -> (StatusCode, Json<serde_json::Value>) {
(StatusCode::CREATED, Json(json!({ "id": 42 })))
}
async fn with_headers() -> (StatusCode, HeaderMap, Json<serde_json::Value>) {
let mut headers = HeaderMap::new();
headers.insert("location", HeaderValue::from_static("/api/v1/products/42"));
(StatusCode::CREATED, headers, Json(json!({ "id": 42 })))
}
async fn raw() -> Response {
Response::builder()
.status(StatusCode::OK)
.header("x-custom", "value")
.body("raw body".into())
.unwrap()
}
Danh sách đầy đủ hơn (vài chục type bao gồm Bytes, Vec<u8> wrapped, Form<T>, Sse, WebSocketUpgrade, ...) ở trait docs. Eight type trên đã đủ cho mọi resource handler cơ bản của Shop API.
Result<T, E> Pattern — Error Mapping
Pattern quan trọng nhất cho real handler là return Result<T, E> với cả hai vế implement IntoResponse. axum thấy Result, gọi .into_response() trên branch Ok(t) hoặc Err(e) tương ứng. Cơ chế này mở ra workflow error handling thuần idiomatic Rust dùng toán tử ?:
use axum::{extract::Path, Json};
use shop_common::error::{AppError, AppResult};
#[derive(serde::Serialize)]
struct ProductDto { id: u64, name: String, price: String }
async fn get_product(Path(id): Path<u64>) -> AppResult<Json<ProductDto>> {
// Mỗi step có thể fail và auto convert sang AppError qua trait From
let product = fetch_product_from_db(id).await?; // ? unwrap hoặc return Err
let dto = ProductDto::from(product);
Ok(Json(dto))
}
async fn fetch_product_from_db(_id: u64) -> AppResult<Product> {
Err(AppError::NotFound)
}
struct Product;
impl From<Product> for ProductDto {
fn from(_: Product) -> Self {
Self { id: 0, name: String::new(), price: String::new() }
}
}
Hai điểm cần làm rõ ở pattern này, cùng status hiện tại trong codebase Shop API:
AppResult<T>alias =Result<T, AppError>đã define ở B10, filecrates/shop-common/src/error.rs. Mọi handler Shop API import quause shop_common::error::{AppError, AppResult};.AppErrorimplIntoResponsesẽ làm ở B16 (groupOrder 2, ordering 16). Sau B16, handler returnAppResult<Json<T>>mới compile được — code preview ở section này tạm chưa chạy được nếu bạn copy vào crateshop-apithực tế.
Mapping AppError → HTTP status đã lock từ B3:
AppError variant → HTTP status → response body envelope
─────────────────────────────────────────────────────────────────────
BadRequest(msg) → 400 → { error, code: "BAD_REQUEST", request_id }
Unauthenticated → 401 → kèm WWW-Authenticate: Bearer
Forbidden → 403 → { error, code: "FORBIDDEN", request_id }
NotFound → 404 → { error, code: "NOT_FOUND", request_id }
MethodNotAllowed → 405 → kèm Allow header
Conflict(msg) → 409 → { error, code: "CONFLICT", request_id }
Validation(msg) → 422 → { error, code: "VALIDATION", request_id }
RateLimited → 429 → kèm Retry-After header
Internal(anyhow::Error) → 500 → log internal, client chỉ thấy code
Upstream(msg) → 502/504 → { error, code: "UPSTREAM", request_id }
Unavailable → 503 → kèm Retry-After header
Lợi ích của pattern: handler body sạch — mọi ? tự bubble error lên, không phải viết match dài dòng hoặc map_err ở mỗi step. Service layer trả AppResult<Product> → handler nhận ? → axum tự convert AppError sang HTTP response đúng status + body envelope. Đây là pattern Shop API sẽ dùng xuyên suốt từ B16 onward cho mọi resource handler.
Extractor — Arg Handler
Extractor trong axum là một type implement trait FromRequest hoặc FromRequestParts. Bản chất là "biết cách trích thông tin từ HTTP request và build ra type Rust tương ứng". Handler nhận extractor làm argument — compiler infer trait bound từ type của argument và enforce tự động.
Hai trait phân chia rõ ràng theo việc consume body hay không:
FromRequestParts— chỉ đọc parts (method, URI, headers, extensions), không consume body. Có thể chạy nhiều lần trong cùng handler. Path param, query, header extractor đều dùng trait này.FromRequest— consume toàn bộ request bao gồm body. Mỗi handler chỉ có tối đa 1 extractor loại này, và phải đặt ở vị trí cuối cùng trong arg list. Body extractor:Json,Form,Bytes,String,Multipart.
Built-in extractor sẽ deep dive ở Group 3-4 (B22-B40), preview ngắn để bạn nhận diện:
Path<T>— path parameter (:id,:slug), parse thành type Rust. Deep dive B22.Query<T>— query string (?page=1&size=20) parse vào struct. Deep dive B23.Json<T>— request body JSON parse vào struct (yêu cầuT: DeserializeOwned). Deep dive B41.State<T>— shared app state (DB pool, Redis client, config) inject quaRouter::with_state. Deep dive B28.Extension<T>— request-scoped data inject qua middleware (current user, request ID). Deep dive B39.HeaderMap— toàn bộ header dạng map, untyped.TypedHeader<T>— typed header có validation sẵn (Authorization Bearer, Content-Type). Deep dive B33.
Rule quan trọng nhất: extractor được parse tuần tự theo thứ tự argument, fail nhanh ở extractor đầu tiên không thỏa. Body extractor (Json, Form, Bytes, Multipart) PHẢI ĐẶT CUỐI arg list, vì consume body là one-shot operation — sau khi đọc body bytes ra, không type nào khác đọc lại được.
Ví dụ handler với nhiều extractor đúng thứ tự:
use axum::{
extract::{Path, Query, State},
http::HeaderMap,
Json,
};
use serde::{Deserialize, Serialize};
#[derive(Clone)]
struct AppState;
#[derive(Deserialize)]
struct ListFilter { page: u32, size: u32 }
#[derive(Deserialize)]
struct UpdateProductDto { name: String, price: String }
#[derive(Serialize)]
struct ProductDto { id: u64 }
// Handler PATCH /api/v1/products/:id — order đúng
async fn update_product(
State(_state): State<AppState>, // FromRequestParts (không consume body)
Path(_id): Path<u64>, // FromRequestParts
Query(_filter): Query<ListFilter>, // FromRequestParts
_headers: HeaderMap, // FromRequestParts
Json(_dto): Json<UpdateProductDto>, // FromRequest — PHẢI CUỐI
) -> Json<ProductDto> {
Json(ProductDto { id: 0 })
}
Nếu bạn đảo Json lên đầu hoặc đặt 2 body extractor trong cùng handler, compile error xuất hiện ngay — phân tích cụ thể ở section 8.
fn Return Type Freedom — 3 Pattern
axum cho phép 3 dạng return type, mỗi dạng phù hợp với mức kiểm soát khác nhau:
Pattern 1: impl IntoResponse — đơn giản nhất. Compiler infer concrete type lúc build, bạn không cần khai báo type cụ thể. Dùng khi handler trả type rõ ràng và không phân nhánh return type khác nhau:
async fn health() -> impl IntoResponse {
Json(serde_json::json!({ "status": "ok" }))
}
Pattern 2: Concrete Response<Body> — full control. Build response thủ công qua builder pattern, set từng header, body bytes. Dùng khi cần manipulate response phức tạp mà type built-in không cover:
use axum::{
body::Body,
http::{Response, StatusCode},
};
async fn raw() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.header("x-trace-id", "abc-123")
.header("cache-control", "no-store")
.body(Body::from("custom body bytes"))
.unwrap()
}
Pattern 3: Result<T, E> — recommended cho real handler. Cho phép dùng toán tử ? bubble error sạch, type vế Ok và Err đều implement IntoResponse. Đây là dạng Shop API dùng xuyên suốt:
use shop_common::error::AppResult;
async fn get_product_handler() -> AppResult<Json<ProductDto>> {
let product = fetch_user().await?;
Ok(Json(product))
}
async fn fetch_user() -> AppResult<ProductDto> {
Ok(ProductDto { id: 1 })
}
#[derive(serde::Serialize)]
struct ProductDto { id: u64 }
Quyết định Shop API: dùng Pattern 3 (AppResult<Json<T>>) cho mọi handler resource từ B16 onward. Pattern 1 (impl IntoResponse) chỉ giữ cho infrastructure handler đơn giản đã có sẵn từ B12: /, /health, /version — những endpoint không cần error mapping vì luôn trả OK. Pattern 2 (Response<Body> raw) hiếm khi xuất hiện trong Shop API — chỉ dùng cho webhook signature verify (B37 Raw Body) hoặc streaming response (B38, B50 SSE).
Lý do chọn Pattern 3 làm default: (1) handler body sạch với toán tử ? không phải viết match; (2) error mapping centralize ở AppError::into_response, đổi format một chỗ áp dụng toàn API; (3) signature self-documenting — đọc AppResult<Json<ProductDto>> biết ngay handler có thể fail và trả JSON; (4) test dễ hơn — assert Result direct mà không phải dispatch qua HTTP.
Common Pitfall + Compile Error Đọc
Năm pitfall compile error dev mới hay gặp khi viết handler axum. Đọc message rustc đúng tiết kiệm hàng giờ debug.
Pitfall 1: Sync handler thay vì async. Quên async trước fn:
// SAI — fn thay vì async fn
fn root() -> &'static str { "shop" }
let app = Router::new().route("/", get(root));
error[E0277]: the trait bound `fn() -> &'static str {root}: Handler<_, _>` is not satisfied
--> src/main.rs:8:36
|
8 | let app = Router::new().route("/", get(root));
| --- ^^^^ the trait `Handler<_, _>` is not implemented
| |
| required by a bound introduced by this call
= help: Handler is implemented for async functions
Fix: thêm async — async fn root() -> &'static str.
Pitfall 2: Multi body extractor trong cùng handler. Dùng 2 Json<T>:
// SAI — 2 Json extractor cùng handler
async fn merge_two(
Json(_a): Json<DtoA>,
Json(_b): Json<DtoB>,
) -> impl IntoResponse { "ok" }
error[E0277]: the trait bound `fn(Json<DtoA>, Json<DtoB>) -> ...: Handler<_, _>` is not satisfied
= note: only the last argument may consume the request body
Fix: gộp 2 DTO thành 1 struct MergeDto { a: DtoA, b: DtoB }, hoặc tách 2 endpoint riêng.
Pitfall 3: Extractor order sai — body trước path/query. Đặt Json trước Path:
// SAI — Json trước Path
async fn update_product(
Json(_dto): Json<UpdateDto>, // body extractor
Path(_id): Path<u64>, // sau body — KHÔNG được
) -> impl IntoResponse { "ok" }
error[E0277]: the trait bound `Path<u64>: FromRequestParts<_>` is not satisfied
= help: `Path<u64>` implements `FromRequestParts`, must come before body extractor
Fix: đặt Path, Query, State, HeaderMap trước, Json hoặc body extractor đặt cuối cùng.
Pitfall 4: Return type không implement IntoResponse. Return Vec<u8> trần:
// SAI — Vec<u8> không impl IntoResponse trực tiếp
async fn raw_bytes() -> Vec<u8> {
vec![1, 2, 3]
}
error[E0277]: the trait bound `Vec<u8>: IntoResponse` is not satisfied
= help: the following types implement `IntoResponse`:
Bytes, Box<[u8]>, (StatusCode, Vec<u8>), Response<Body>
Fix: wrap vào axum::body::Bytes (Bytes::from(vec![1,2,3])) hoặc tuple với Content-Type rõ ràng:
use axum::body::Bytes;
use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
async fn raw_bytes() -> (StatusCode, HeaderMap, Bytes) {
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("application/octet-stream"));
(StatusCode::OK, headers, Bytes::from(vec![1, 2, 3]))
}
Pitfall 5: Lifetime — return &str từ local String. Borrow chuỗi local:
// SAI — return &str trỏ vào local
async fn local_ref(name: String) -> &str {
let s = format!("hello {}", name);
&s[..]
}
error[E0106]: missing lifetime specifier
= help: function return type cannot reference local variable `s`
Fix: return String owned thay vì &str borrow. Handler axum gần như không bao giờ cần return &str non-static — luôn String hoặc &'static str cho literal.
Apply Vào shop-api: Pattern Xuyên Suốt
Lock pattern handler chuẩn cho mọi resource Shop API từ B16 onward (sau khi AppError impl IntoResponse bật được code này chạy):
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; // sẽ define đầy đủ ở B28
#[derive(Deserialize)]
struct ProductFilter { page: u32, size: u32 }
#[derive(Deserialize)]
struct CreateProductDto { name: String, price: String }
#[derive(Serialize)]
struct ProductDto { id: u64, name: String, price: String }
#[derive(Serialize)]
struct ListResponse<T> { items: Vec<T>, total: u64 }
// List handler — AppResult<Json<ListResponse<T>>>
async fn list_products(
State(_state): State<AppState>,
Query(_filter): Query<ProductFilter>,
) -> AppResult<Json<ListResponse<ProductDto>>> {
let items = vec![];
Ok(Json(ListResponse { items, total: 0 }))
}
// Create handler — AppResult<(StatusCode, HeaderMap, Json<T>)> cho 201 + Location
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)))
}
// Update handler — AppResult<Json<T>> cho 200
async fn update_product(
State(_state): State<AppState>,
Path(_id): Path<u64>,
Json(_dto): Json<CreateProductDto>,
) -> AppResult<Json<ProductDto>> {
Ok(Json(ProductDto { id: 0, name: String::new(), price: String::new() }))
}
// Delete handler — AppResult<StatusCode> trả 204
async fn delete_product(
State(_state): State<AppState>,
Path(_id): Path<u64>,
) -> AppResult<StatusCode> {
Ok(StatusCode::NO_CONTENT)
}
Bốn signature lock vĩnh viễn cho Shop API:
- List handler:
AppResult<Json<ListResponse<T>>>trả 200 + envelope chứaitems+total(chi tiết pagination contract B64). - Create handler (POST):
AppResult<(StatusCode, HeaderMap, Json<T>)>trả 201 + headerLocation+ body entity mới (lock từ B3 status code policy). - Update handler (PUT/PATCH):
AppResult<Json<T>>trả 200 + entity sau update; hoặcAppResult<StatusCode>trả 204 nếu không cần body. - Delete handler:
AppResult<StatusCode>trả 204 No Content (RFC 9110 cấm body trong 204).
Infrastructure handler (/, /health, /version từ B12) giữ pattern impl IntoResponse đơn giản vì không bao giờ fail — không cần error mapping. Chi tiết DTO design (CreateProductDto, ProductDto) ở B41, AppState full skeleton ở B28, AppError impl IntoResponse sản sinh body envelope ở B16.
Tổng Kết
- Rule signature handler axum:
async fn(extractors...) -> impl IntoResponse— 3 yêu cầu (async, arg là extractor, return implement IntoResponse) compiler enforce qua trait bound. - Trait
IntoResponse: contract giữa handler và axum runtime, method.into_response()consumeselftrảResponse; axum gọi tự động sau khi handler return. - 8 type built-in implementor phổ biến:
&str,String,Html<T>,Json<T>,StatusCode, tuple(StatusCode, body)+(StatusCode, HeaderMap, body),Response<Body>,Result<T, E>. Result<T, E>pattern cho error mapping: Shop API lockAppResult<Json<T>>xuyên suốt từ B16 onward;AppError11 variants → HTTP status đã lock từ B3 (404/401/403/422/...).- Extractor là arg handler — type implement
FromRequest/FromRequestParts; built-in extractor:Path,Query,Json,State,Extension,HeaderMap,TypedHeader. Body extractor PHẢI đặt cuối arg list (Json, Form, Bytes, Multipart). - 3 return type pattern:
impl IntoResponse(simple, infrastructure handler),Response<Body>(raw full control, webhook/streaming),Result<T, E>(recommended, mọi resource handler). - 5 pitfall compile error: sync handler thiếu
async, multi body extractor, order sai (body trước parts), return type không implement IntoResponse, lifetime issue return&strtừ local. - Shop API pattern lock: list →
AppResult<Json<ListResponse<T>>>, create →AppResult<(StatusCode, HeaderMap, Json<T>)>, update →AppResult<Json<T>>, delete →AppResult<StatusCode>, infrastructure →impl IntoResponse.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Ba yêu cầu signature handler axum là gì? Compiler enforce 3 yêu cầu này thông qua cơ chế nào?
- Tại sao
Vec<u8>không trực tiếp được dùng làm return type của handler? Liệt kê 2 workaround. - Handler có
Json<UpdateDto>vàPath<u64>trong arg list — thứ tự nào đúng và tại sao? Nếu đảo, compile error message gì xuất hiện? - Shop API chọn pattern return type nào cho mọi handler resource? Liệt kê đầy đủ signature cho list/create/update/delete và infrastructure handler.
- Compile error "the trait
Handleris not implemented" thường do nguyên nhân gì? Liệt kê 3 nguyên nhân phổ biến và cách fix mỗi cái.
Đáp án
- 3 yêu cầu signature handler axum: (a)
async fn— bắt buộc async vì traitHandlerđòi returnFuture; (b) mỗi argument có type implementFromRequesthoặcFromRequestParts— không nhận arbitrary type; (c) return type implementIntoResponse— convert được sang HTTP response. Compiler enforce thông qua trait bound trong signature củaRouter::route,get(handler),post(handler): các function này nhậnimpl Handler<T, S>vớiHandlertrait có blanket impl cho mọi async function thỏa 3 điều kiện trên. Khi handler sai signature, compile error xuất hiện ngay tại.route("/x", get(my_handler))với message "the traitHandleris not implemented" — không phải runtime error. - Lý do
Vec<u8>không implementIntoResponsetrực tiếp: axum không tự đoán Content-Type cho raw bytes — có thể làapplication/octet-stream,image/png,application/pdf, ... Không có default safe. Trả "bytes mơ hồ" sẽ phá vỡ contract HTTP. Workaround: (1) wrap quaaxum::body::Bytes::from(vec)— type này có impl IntoResponse với Content-Typeapplication/octet-streamdefault; (2) trả tuple(StatusCode, HeaderMap, Bytes)vớiHeaderMapchứaContent-Typerõ ràng — pattern Shop API dùng cho download file (B36, B37); (3) buildResponse<Body>raw quaResponse::builder()...body(Body::from(vec))khi cần full control. - Thứ tự đúng:
Path<u64>phải đặt trướcJson<UpdateDto>. Lý do:PathimplementFromRequestParts(chỉ đọc URI parts, không touch body),JsonimplementFromRequest(consume toàn bộ request body). axum chạy extractor tuần tự theo thứ tự argument — body extractor phải cuối cùng vì sau khi đọc body bytes ra, không type nào khác đọc lại được (consume là one-shot operation). Nếu đảo (JsontrướcPath): compile errorerror[E0277]: the trait bound `Path<u64>: FromRequestParts<_>` is not satisfiedkèm note "must come before body extractor". Compile-time check chứ không phải runtime — rất an toàn. - Pattern Shop API chọn:
Result<T, E>vớiAppResult<T>alias (=Result<T, AppError>) cho mọi resource handler từ B16 onward sau khiAppErrorimplIntoResponse. Signature đầy đủ: (a) List:AppResult<Json<ListResponse<T>>>trả 200 + envelope{ items, total }; (b) Create (POST):AppResult<(StatusCode, HeaderMap, Json<T>)>trả 201 + headerLocation: /api/v1/.../id+ entity mới; (c) Update (PUT/PATCH):AppResult<Json<T>>trả 200 + entity sau update, hoặcAppResult<StatusCode>trả 204 nếu không cần body; (d) Delete:AppResult<StatusCode>trả 204 No Content; (e) Infrastructure (/,/health,/versiontừ B12):impl IntoResponseđơn giản vì không bao giờ fail. - 3 nguyên nhân phổ biến của "the trait
Handleris not implemented": (1) Sync handler thiếuasync— handler làfnthường thay vìasync fn; fix: thêm keywordasynctrướcfn. (2) Arg không phải extractor hợp lệ — type của argument không implementFromRequest/FromRequestParts, vd nhận thẳngu64hoặcStringkhông wrap quaPath/Query/Json; fix: wrap qua extractor đúng (Path<u64>,Json<String>, hoặc đặt body raw quaString/Bytestự có impl FromRequest). (3) Return type không implementIntoResponse— trả type nhưVec<u8>trần, struct chưa derive serialize không wrapJson; fix: wrap return value quaJson(x),Bytes::from(x), hoặcimpl IntoResponsecho struct custom (B40). Nguyên nhân ít gặp hơn: (4) multi body extractor trong cùng handler, (5) body extractor không đặt cuối arg list, (6) handler dùng&self(method) thay vì free function — axum chỉ support free function async.
Bài Tiếp Theo
Bài 14: Response Types: String, Html, Json, StatusCode — chi tiết từng response type built-in của axum: String trả text/plain; charset=utf-8 với rule UTF-8, Html<T> trả text/html; charset=utf-8 cho template render, Json<T> trả application/json; charset=utf-8 serialize qua serde, StatusCode trả status only cho 204 No Content, tuple (StatusCode, body) + (StatusCode, HeaderMap, body) cho custom status + header, và pattern impl IntoResponse cho custom struct response envelope chuẩn Shop API.
