Danh sách bài viết

Bài 13: Route Handler — Function Signature

Bài 13 của series Rust RESTful API — deep dive vào signature handler axum: rule cốt lõi async fn(extractors...) -> impl IntoResponse được compiler enforce qua trait bound, trait IntoResponse với 8 type built-in implement sẵn (&str, String, Html<T>, Json<T>, StatusCode, tuple (StatusCode, body), Response<Body>, Result<T, E>), khái niệm extractor là arg handler implement FromRequest/FromRequestParts với rule body extractor luôn đặt cuối, 3 pattern return type (impl IntoResponse đơn giản, Response<Body> full control, Result<T, E> recommended), 5 common pitfall compile error dev mới hay gặp kèm rustc message cụ thể, và pattern AppResult<Json<T>> được lock làm signature chuẩn cho mọi handler Shop API từ B16 onward sau khi AppError impl IntoResponse — bài này conceptual deep dive, code preview chưa chạy được nhưng lock pattern cho mọi resource handler tương lai.

12/06/2026
10 phút đọc
0 lượt xem
1

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 IntoResponse và lý do compiler enforce qua trait bound.
  • Nắm trait IntoResponse cố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.
2

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:

  1. async fn — handler bắt buộc là async function. axum không support sync handler vì trait Handler đòi return Future, dùng fn thường sẽ ra type không match trait bound.
  2. Arg là extractor — mỗi argument phải có type implement trait FromRequest hoặc FromRequestParts củ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".
  3. 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.

3

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).

4

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 strtext/plain; charset=utf-8, status 200. Dùng cho chuỗi literal compile-time như "pong".
  • Stringtext/plain; charset=utf-8, status 200. Dùng khi chuỗi build runtime qua format!.
  • Html<T> với T: Into<Body>text/html; charset=utf-8, status 200. Dùng cho admin dashboard render template (askama/maud).
  • Json<T> với T: Serializeapplication/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 cho DELETE trả 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 + Location header (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ới T: IntoResponseE: IntoResponse → recursive: axum gọi .into_response() trên branch tương ứng. Đây là pattern AppResult<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.

5

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, file crates/shop-common/src/error.rs. Mọi handler Shop API import qua use shop_common::error::{AppError, AppResult};.
  • AppError impl IntoResponse sẽ làm ở B16 (groupOrder 2, ordering 16). Sau B16, handler return AppResult<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 crate shop-api thự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.

6

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ầu T: DeserializeOwned). Deep dive B41.
  • State<T> — shared app state (DB pool, Redis client, config) inject qua Router::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.

7

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.

8

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 asyncasync 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.

9

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ứa items + total (chi tiết pagination contract B64).
  • Create handler (POST): AppResult<(StatusCode, HeaderMap, Json<T>)> trả 201 + header Location + body entity mới (lock từ B3 status code policy).
  • Update handler (PUT/PATCH): AppResult<Json<T>> trả 200 + entity sau update; hoặc AppResult<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.

10

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() consume self trả 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 lock AppResult<Json<T>> xuyên suốt từ B16 onward; AppError 11 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 &str từ 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.
11

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. 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?
  2. 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.
  3. Handler có Json<UpdateDto>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?
  4. 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.
  5. Compile error "the trait Handler is 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
  1. 3 yêu cầu signature handler axum: (a) async fn — bắt buộc async vì trait Handler đòi return Future; (b) mỗi argument có type implement FromRequest hoặc FromRequestParts — không nhận arbitrary type; (c) return type implement IntoResponse — convert được sang HTTP response. Compiler enforce thông qua trait bound trong signature của Router::route, get(handler), post(handler): các function này nhận impl Handler<T, S> với Handler trait 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 trait Handler is not implemented" — không phải runtime error.
  2. Lý do Vec<u8> không implement IntoResponse trự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 qua axum::body::Bytes::from(vec) — type này có impl IntoResponse với Content-Type application/octet-stream default; (2) trả tuple (StatusCode, HeaderMap, Bytes) với HeaderMap chứa Content-Type rõ ràng — pattern Shop API dùng cho download file (B36, B37); (3) build Response<Body> raw qua Response::builder()...body(Body::from(vec)) khi cần full control.
  3. Thứ tự đúng: Path<u64> phải đặt trước Json<UpdateDto>. Lý do: Path implement FromRequestParts (chỉ đọc URI parts, không touch body), Json implement FromRequest (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 (Json trước Path): compile error error[E0277]: the trait bound `Path<u64>: FromRequestParts<_>` is not satisfied kèm note "must come before body extractor". Compile-time check chứ không phải runtime — rất an toàn.
  4. Pattern Shop API chọn: Result<T, E> với AppResult<T> alias (= Result<T, AppError>) cho mọi resource handler từ B16 onward sau khi AppError impl IntoResponse. Signature đầy đủ: (a) List: AppResult<Json<ListResponse<T>>> trả 200 + envelope { items, total }; (b) Create (POST): AppResult<(StatusCode, HeaderMap, Json<T>)> trả 201 + header Location: /api/v1/.../id + entity mới; (c) Update (PUT/PATCH): AppResult<Json<T>> trả 200 + entity sau update, hoặc AppResult<StatusCode> trả 204 nếu không cần body; (d) Delete: AppResult<StatusCode> trả 204 No Content; (e) Infrastructure (/, /health, /version từ B12): impl IntoResponse đơn giản vì không bao giờ fail.
  5. 3 nguyên nhân phổ biến của "the trait Handler is not implemented": (1) Sync handler thiếu async — handler là fn thường thay vì async fn; fix: thêm keyword async trước fn. (2) Arg không phải extractor hợp lệ — type của argument không implement FromRequest/FromRequestParts, vd nhận thẳng u64 hoặc String không wrap qua Path/Query/Json; fix: wrap qua extractor đúng (Path<u64>, Json<String>, hoặc đặt body raw qua String/Bytes tự có impl FromRequest). (3) Return type không implement IntoResponse — trả type như Vec<u8> trần, struct chưa derive serialize không wrap Json; fix: wrap return value qua Json(x), Bytes::from(x), hoặc impl IntoResponse cho 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.
12

Bài Tiếp Theo

— 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.