Membangun extractor kustom yang reusable dengan FromRequestParts dan FromRequest, menerapkan pola AuthUser yang dipakai banyak handler, serta teknik routing dinamis untuk aplikasi modular.

Di episode 5 kita menyebut bahwa extractor custom layak dibuat saat pola berulang: "baca header, parse token, cek klaim" muncul di banyak handler. Episode ini menepati janji itu: kita membangun AuthUser extractor — satu baris di signature handler menggantikan seluruh boilerplate JWT — plus FromRequestParts dan FromRequest secara menyeluruh, serta teknik routing lanjutan.
Mengapa ini "advanced"? Karena extractor custom adalah titik di mana kalian mulai memperluas Axum, bukan sekadar memakainya. Memahami dua trait itu membuka pintu untuk abstraksi apapun: current user, audit logger, rate limit state, hingga konfigurasi per-request.
Dari episode 5:
FromRequestParts<S> — membaca bagian request tanpa body (State, headers, method, URI). Berjalan berurutan sesuai urutan argumen; extractor ini bisa saling bergantung (yang lebih dulu bisa menyediakan data untuk yang berikutnya).FromRequest<S> — membaca body. Hanya satu extractor body per handler, harus di posisi terakhir.Kedua trait punya associated type Rejection — tipe error yang dikembalikan saat ekstraksi gagal. Kita akan memetakannya ke AppError (episode 6) supaya error konsisten.
Kita ubah pola middleware JWT (episode 13) menjadi extractor. Keuntungannya: handler tidak lagi bergantung pada Extension<Claims> yang implisit — signature handler secara eksplisit menyatakan "saya butuh user terautentikasi".
use axum::{
extract::{FromRequestParts, State},
http::{request::Parts, StatusCode},
};
use crate::{
auth::claims::Claims,
error::{AppError, AppResult},
state::AppState,
};
#[derive(Debug, Clone)]
pub struct AuthUser {
pub id: uuid::Uuid,
pub role: String,
}
impl<S> FromRequestParts<S> for AuthUser
where
S: Send + Sync,
{
type Rejection = AppError;
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> Result<Self, Self::Rejection> {
// 1. Ambil header Authorization
let token = parts
.headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.ok_or(AppError::Unauthorized)?;
// 2. Dapatkan JWT secret dari state
let app_state = State::<AppState>::from_request_parts(parts, state)
.await
.map_err(|_| AppError::Internal("state tidak tersedia".into()))?;
// 3. Verifikasi token
let claims: Claims =
verify_token(token, &app_state.config.jwt_secret)
.map_err(|_| AppError::Unauthorized)?;
Ok(AuthUser {
id: claims.sub,
role: claims.role,
})
}
}Yang terjadi di balik layar:
FromRequestParts memberi akses &mut Parts — termasuk headers — dan state (parameter kedua, di sini &S).AppState dari dalam, kita panggil State::<AppState>::from_request_parts(...) — extractor bisa memakai extractor lain! Inilah "chain" yang memungkinkan dependency antar extractor.AppError::Unauthorized — konsisten dengan sistem error kita.Handler sekarang jauh lebih bersih:
use axum::Json;
use crate::{
auth::user::AuthUser,
error::AppResult,
models::PublicUser,
};
async fn me(user: AuthUser) -> AppResult<Json<PublicUser>> {
// user.id dan user.role sudah tersedia, tanpa boilerplate token
Ok(Json(PublicUser {
id: user.id,
username: "username-nya".into(),
display_name: None,
created_at: chrono::Utc::now(),
}))
}
fn protected() -> Router {
Router::new().route("/me", axum::routing::get(me))
}Perbedaan signifikan dari episode 13: middleware JWT masih perlu di-attach via .layer(...), dan handler membaca Extension<Claims>. Dengan AuthUser, semua informasi ada di signature handler — tidak ada cara memanggil handler ini tanpa user terautentikasi.
Tip
Kapan memakai middleware dan kapan extractor? Middleware untuk keputusan yang harus terjadi sebelum handler dipilih (misal cek rate limit, validasi global). Extractor untuk data yang dibutuhkan handler (user, konfigurasi). AuthUser adalah extractor karena hampir semua handler yang dilindungi akan memakainya — middleware require_auth masih berguna untuk "pagar" sebelum router.
Extractor pemakan body dibangun dengan FromRequest — contoh JsonBody yang menggabungkan deserialize + validasi (rekam jejak ValidatedJson dari episode 14):
use axum::{
async_trait,
extract::{FromRequest, Json, Request},
};
use serde::de::DeserializeOwned;
use validator::Validate;
pub struct ValidatedJson<T>(pub T);
#[async_trait]
impl<S, T> FromRequest<S> for ValidatedJson<T>
where
S: Send + Sync,
T: DeserializeOwned + Validate,
{
type Rejection = AppError;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let Json(value) = Json::<T>::from_request(req, state).await?;
value.validate().map_err(|err| AppError::BadRequest(format!("{err}")))?;
Ok(ValidatedJson(value))
}
}Catatan: FromRequest menerima Request (bukan Parts), karena perlu mengonsumsi body. Handler memakainya dengan:
async fn create_post(
user: AuthUser,
ValidatedJson(body): ValidatedJson<CreatePost>,
) -> AppResult<(StatusCode, Json<Post>)> {
// body sudah terdeserialize DAN tervalidasi
}Perhatikan urutan yang dihasilkan oleh sistem tipe Axum: AuthUser (parts, tidak sentuh body) lalu ValidatedJson (body, terakhir). Compiler menegakkan aturan ini — tidak ada kesempatan salah urutan.
Routing tidak harus statis di main. Teknik yang berguna untuk aplikasi modular:
fn posts_module(admin_only: bool) -> Router<AppState> {
let router = Router::new()
.route("/posts", get(list_posts).post(create_post));
if admin_only {
router.route_layer(middleware::from_fn_with_state(
state.clone(),
require_role,
))
} else {
router
}
}Factory menerima parameter dan mengembalikan router yang berbeda — berguna untuk environment berbeda (misal admin_only di produksi).
fn app(config: &Config) -> Router {
let mut router = Router::new().route("/health", get(|| async { "ok" }));
if config.enable_docs {
router = router.merge(SwaggerUi::new("/docs")
.url("/api-docs/openapi.json", ApiDoc::openapi()));
}
if config.enable_metrics {
router = router.route("/metrics", get(metrics_endpoint));
}
router
}Fitur yang diaktifkan per environment (dokumentasi di produksi? metrics internal?) menjadi keputusan config, bukan kode yang dicomment.
Untuk kasus eksotik (plugin, multi-tenant), route bisa dibangun dari iterator:
let mut router = Router::new();
for tenant in tenants {
router = router.nest(
&format!("/tenants/{}", tenant.slug),
tenant_router(),
);
}Gunakan dengan hati-hati — daftar route yang dibangun runtime lebih sulit di-debug daripada yang statis.
Kombinasi extractor custom + middleware memungkinkan response yang konsisten. Contoh: memastikan tiap response punya X-Request-Id — polanya sudah kita mulai di episode 8:
use axum::{extract::Request, http::HeaderValue, middleware::Next, response::Response};
async fn request_id_middleware(mut request: Request, next: Next) -> Response {
let request_id = request
.headers()
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.map(|v| v.to_string())
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
// Simpan ke extension agar extractor lain (misal logger) bisa membacanya
request.extensions_mut().insert(RequestId(request_id.clone()));
let mut response = next.run(request).await;
if let Ok(value) = HeaderValue::from_str(&request_id) {
response.headers_mut().insert("x-request-id", value);
}
response
}
#[derive(Clone)]
pub struct RequestId(pub String);Efeknya: setiap response punya ID yang bisa dilacak — klien yang melaporkan bug cukup menyebut x-request-id, dan kalian menelusuri log (episode 17) untuk request tersebut. Ini kebiasaan observability yang murah dan bernilai besar.
Note
Jika klien mengirim X-Request-Id, hargai dan pakai ID itu (bukan generate baru) — pola di atas menerima ID eksternal. Ini memungkinkan korelasi lintas layanan ketika request berpindah antar microservice.
Pada episode 21 ini kalian memperluas Axum sendiri:
AuthUser extractor dari FromRequestParts — handler menyatakan kebutuhannya secara eksplisit.State::from_request_parts.FromRequest untuk ValidatedJson — body + validasi dalam satu langkah.X-Request-Id untuk korelasi log lintas layanan.Di episode 22 selanjutnya kita beri aplikasi pekerja latar: background tasks & concurrency — tokio spawn, mpsc channel, job queue dengan deadpool/Redis, dan pola concurrency yang aman. Sampai jumpa di episode 22!