Menyelami extractor Axum: Json, Path, Query, Header, State, String, Bytes, dan Form, termasuk aturan urutan argumen dan peran trait FromRequestParts vs FromRequest sebelum membuat extractor custom.

Di episode 4 kalian sudah memakai Path dan Query untuk membaca parameter route. Episode ini melengkapinya: extractor adalah mekanisme paling khas Axum, dan menguasainya berarti menguasai cara membaca request dengan aman dan bertipe. Kita akan membedah semua extractor bawaan — Json, Path, Query, Header, State, String, Bytes, Form — lalu memahami dua trait di balik layar: FromRequestParts dan FromRequest.
Mengapa extractor layak mendapat satu episode penuh? Karena semua API nyata akhirnya bermuara pada: baca input dari request, validasi, proses, kembalikan response. Extractor Axum mengambil porsi "baca input" ini dari tangan kalian dan menyerahkannya ke sistem tipe — sehingga error parsing, header yang hilang, dan body yang tidak sesuai bentuk tertangkap sejak compile time atau setidaknya pada lapisan yang terpusat, bukan berserakan di tiap handler.
Semua extractor dibangun di atas dua trait:
FromRequestParts<S> — mengekstrak data dari bagian request yang tidak memakan body: State, Path, Query, Headers, method, URI. Dapat dipanggil berkali-kali dan tidak mengonsumsi request.FromRequest<S> — mengekstrak data yang membutuhkan body: Json, String, Bytes, Form. Hanya bisa dipanggil sekali karena body di-stream.Aturan urutan argumen handler mengikuti fakta ini:
async fn handler(
State(state): State<AppState>, // FromRequestParts - bebas
Path(id): Path<u32>, // FromRequestParts - bebas
Query(q): Query<QueryParams>, // FromRequestParts - bebas
Json(body): Json<CreateBody>, // FromRequest -> WAJIB PALING AKHIR
) {}Jika extractor pemakan body diletakkan sebelum extractor lain, Axum menolak di compile time dengan pesan yang menjelaskan aturan ini. Ini bukan pembatasan sewenang-wenang — body hanyalah stream satu arah, tidak bisa dibaca dua kali.
Json<T> mem-parse body menjadi tipe Deserialize. Jika parsing gagal, otomatis 400:
use axum::{extract::Json, http::StatusCode};
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
age: Option<u8>,
}
async fn create_user(
Json(body): Json<CreateUser>,
) -> (StatusCode, Json<serde_json::Value>) {
let id = 42;
(StatusCode::CREATED, Json(serde_json::json!({ "id": id, "name": body.name })))
}Yang menarik: Axum secara otomatis menolak request dengan Content-Type yang bukan application/json sebelum mencoba parsing — perlindungan gratis terhadap klien yang salah kirim tipe.
Sudah dibahas di episode 4; keduanya FromRequestParts. Poin tambahan: Path menerima satu segmen :id atau tuple/struct untuk banyak parameter; Query mem-parse query string dengan toleransi field ekstra.
HeaderMap mengambil seluruh headers sebagai map, atau TypedHeader (dari axum-extra) untuk header bertipe:
use axum::http::{HeaderMap, HeaderValue};
async fn read_headers(headers: HeaderMap) -> String {
let ua = headers
.get("user-agent")
.and_then(|v: &HeaderValue| v.to_str().ok())
.unwrap_or("unknown");
format!("User-Agent: {ua}")
}Pola get(...).and_then(...).unwrap_or(...) ini sangat umum untuk header opsional. Untuk header yang wajib, lebih baik pakai Result — lihat bagian error handling di bawah.
State mengambil nilai dari app state yang didaftarkan saat router dibangun dengan .with_state(...). Kita akan membahasnya secara mendalam di episode 7 — cukup catat di sini bahwa State adalah FromRequestParts, jadi boleh muncul sebelum extractor body.
Jika body berbentuk teks polos, ambil sebagai String; jika binary, Bytes:
use axum::body::Bytes;
async fn raw_text(body: String) -> String {
format!("Kalian mengirim {} karakter", body.len())
}
async fn raw_bytes(body: Bytes) -> usize {
body.len()
}Sangat berguna untuk endpoint yang menerima data mentah (misal webhook). Perhatikan default batas body Axum adalah 2 MB — kita ubah di episode 23 saat membahas streaming dan multipart.
Form<T> mem-parse application/x-www-form-urlencoded — format submit form HTML klasik:
use axum::{extract::Form, http::StatusCode};
use serde::Deserialize;
#[derive(Deserialize)]
struct LoginForm {
username: String,
password: String,
}
async fn login(Form(form): Form<LoginForm>) -> StatusCode {
if form.username == "admin" && form.password == "rahasia" {
StatusCode::OK
} else {
StatusCode::UNAUTHORIZED
}
}Contoh login di atas sengaja naif — keamanan autentikasi sesungguhnya dibahas di episode 13. Untuk episode ini, fokus pada mekanisme parsing-nya.
Ketika extractor gagal — body tidak valid, header hilang, query salah tipe — Axum menghasilkan error. Ada dua lapisan:
{ "error": ... }) dengan meng-override IntoResponse dari tipe rejection. Ini kita bangun di episode 6 dengan tipe error aplikasi.Praktik terbaik: di fase awal, biarkan rejection default. Jangan dulu over-engineer error handling sebelum sistem error dibangun di episode 6.
Merangkai semuanya — endpoint yang menerima path, query, headers, dan body sekaligus:
use axum::{
extract::{Path, Query, State},
http::HeaderMap,
Json,
};
use serde::Deserialize;
#[derive(Clone)]
struct AppState {
db: String, // placeholder, akan jadi pool di episode 7
}
#[derive(Deserialize)]
struct UpdateQuery {
dry_run: Option<bool>,
}
#[derive(Deserialize)]
struct UpdatePayload {
title: String,
body: Option<String>,
}
async fn update_post(
State(state): State<AppState>,
Path(id): Path<u32>,
Query(q): Query<UpdateQuery>,
headers: HeaderMap,
Json(payload): Json<UpdatePayload>,
) -> String {
let dry = q.dry_run.unwrap_or(false);
let trace_id = headers
.get("x-trace-id")
.and_then(|v| v.to_str().ok())
.unwrap_or("none");
format!(
"update post {id} di state {:?} (dry_run={dry}, trace={trace_id})",
state.db
)
}Handler ini membaca: state (DB pool), ID path, flag query, header tracing, dan body JSON — dalam satu signature yang rapi dan type-safe.
Tip
Urutan argumen handler Axum 0.8 bisa dibaca sebagai dokumentasi: makin ke kanan makin "berat" — state dan path di kiri (murah), body di paling kanan (mahal, sekali pakai). Jaga pola ini agar handler mudah diprediksi.
Extractor bawaan menangani 95% kebutuhan. Extractor custom (FromRequestParts) baru layak dibuat ketika:
Authorization).Kita praktikkan ini di episode 21 dengan extractor AuthUser yang reusable. Untuk sekarang, catat saja syarat utamanya: implement trait FromRequestParts (atau FromRequest untuk yang butuh body), dan handler cukup menulis AuthUser(user): AuthUser seperti extractor biasa.
Pada episode 5 ini kalian telah menguasai extractor:
FromRequestParts (tanpa body) dan FromRequest (pakai body, wajib di argumen terakhir).Json, Path, Query, Header, State, String, Bytes, Form untuk membaca seluruh bentuk request.Di episode 6 selanjutnya kita bangun responses & error handling secara menyeluruh: IntoResponse, status code, headers, redirect, dan tipe error kustom yang mengubah semua rejection menjadi format JSON yang konsisten. Sampai jumpa di episode 6!