Membongkar arsitektur Axum: bagaimana request mengalir dari tokio → hyper → tower → Router → handler, peran extractors dan IntoResponse, serta mengapa seluruh lapisan dibangun di atas trait yang komposable.

Setelah di episode 1 kita memahami sejarah dan filosofi Axum — framework resmi ekosistem tokio/tower yang library-oriented — pada episode ini kita membedah arsitektur dan konsep inti yang akan menemani kalian sepanjang series. Ini adalah episode paling konseptual di fase awal, dan justru karena itu paling penting.
Mengapa harus paham arsitektur? Karena hampir semua error Axum yang membingungkan pemula — "handler harus Send", "response harus implement IntoResponse", "extractor harus diposisikan di urutan tertentu" — bersumber dari cara kerja internal. Begitu arsitektur ini paham, error-error tersebut berubah dari teka-teki menjadi sesuatu yang bisa ditebak.
Axum tidak berdiri sendiri. Ia duduk di atas rantai komponen yang saling meminjam pekerjaan:
Penjelasan setiap lapisan:
Request dan Response.Service adalah async fn yang menerima Request dan mengembalikan Result<Response>. Ini "bahasa umum" di ekosistem Rust.Service. Ia mencocokkan request ke handler berdasarkan path dan method.async fn yang kalian tulis, dibungkus Axum sehingga bisa dipanggil sebagai service.Kunci yang harus diserap: Router Axum pada dasarnya adalah sebuah tower Service. Akibatnya, ia bisa dipakai di dalam layanan tower lain, bisa di-layer middleware, dan bisa dikomposisikan — inilah yang membuat Axum fleksibel.
Note
Karena Router adalah Service, kalian bisa menggabungkannya dengan library tower apa pun di luar Axum — termasuk middleware yang bukan dari tower-http. Di episode 8 dan 21 kita manfaatkan fakta ini untuk custom middleware.
Handler adalah async fn yang bisa menerima extractor sebagai argumen dan mengembalikan apa pun yang mengimplement IntoResponse:
async fn hello(name: String) -> String {
format!("Halo, {name}!")
}Perhatikan: parameter name: String bukan sekadar nilai — di belakang layar Axum melihat tipe String dan tahu cara mengekstraknya dari request (dalam kasus ini, dari path parameter dengan nama yang sama). Inilah yang disebut extractor.
Handler tidak harus menyebut tipe request/response secara eksplisit. Axum menyusun "cara memanggil handler" lewat trait Handler yang diimplementasikan untuk berbagai kombinasi argumen — sampai 16 argumen extractor. Semua diselesaikan di compile time, jadi tidak ada refleksi atau runtime overhead.
Ketika handler dipanggil, Axum men-spawn task ke tokio thread pool. Agar aman, seluruh handler (beserta state yang ia pinjam) harus Send — artinya bisa dipindah antar thread. Jika handler kalian menyimpan Rc atau RefCell, compiler akan menolak dengan pesan yang panjang. Ini bukan bug Axum, melainkan kepastian keamanan memori yang dijamin compiler.
Extractor adalah cara Axum membaca request dan menyediakan nilainya ke handler. Empat yang paling sering dipakai:
| Extractor | Mengambil | Dipakai Untuk |
|---|---|---|
Path | Segmen path (contoh /:id) | ID resource pada REST |
Query | Query string (contoh ?page=2) | Pagination, filter |
Json | Body yang di-parse sebagai JSON | Body POST/PUT |
State | State aplikasi yang dibagikan | DB pool, config |
Contoh handler yang memakai tiga extractor sekaligus:
use axum::{
extract::{Path, Query, State},
Json,
};
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize)]
struct PageQuery {
page: Option<u32>,
}
async fn get_posts(
State(pool): State<MyPool>,
Path(id): Path<u32>,
Query(q): Query<PageQuery>,
) -> Json<serde_json::Value> {
let page = q.page.unwrap_or(1);
Json(serde_json::json!({ "id": id, "page": page }))
}Ada aturan urutan penting: extractor yang mengonsumsi body (Json, String, Bytes) harus diletakkan di paling akhir daftar argumen — karena body hanya bisa dibaca sekali. Extractors FromRequestParts (seperti State, Path, Query, Headers) tidak menyentuh body, jadi boleh di mana saja. Detail ini kita bedah penuh di episode 5.
Setiap nilai yang dikembalikan handler harus mengimplement trait IntoResponse. Axum sudah menyediakan implementasi untuk tipe umum:
String dan &'static str → teks dengan Content-Type: text/plain.Vec<u8> dan Bytes → binary stream.Json<T> → JSON dengan Content-Type: application/json.StatusCode → respons tanpa body.Result<T, E> → respons sukses atau error, selama T dan E keduanya IntoResponse.(StatusCode, Json<T>) → kombinasi status + body.use axum::{http::StatusCode, Json};
async fn ok() -> &'static str { "semua baik" }
async fn created() -> (StatusCode, Json<serde_json::Value>) {
(StatusCode::CREATED, Json(serde_json::json!({ "status": "created" })))
}
async fn no_content() -> StatusCode {
StatusCode::NO_CONTENT
}Kelebihan desain ini: selama kalian bisa mengubah tipe menjadi sesuatu yang IntoResponse, handler tetap sederhana — tidak ada boilerplate manual yang berulang. Kita bangun sistem error yang lebih dalam di episode 6.
Middleware adalah lapisan yang membungkus service untuk memproses request sebelum handler (dan response setelahnya). Secara konseptual:
Setiap middleware adalah tower Service yang menerima request, boleh mengubahnya, meneruskan ke service berikutnya, lalu boleh mengubah response yang kembali. Karena urutan ini berlapis, urutan pemasangan menentukan perilaku: middleware yang dipasang pertama justru "paling luar". Kita praktikkan ini di episode 8 dengan stack TraceLayer + CorsLayer + TimeoutLayer.
Satu lagi konsep kunci: Router bisa di-merge dan di-nest. Sebuah router besar bisa dipecah menjadi modul, lalu dirapikan:
Router::nest("/api", api_router) — menggantung sub-router di bawah prefix /api.Router::merge — menggabungkan dua router yang path-nya tidak bertabrakan.Router::route — menambah satu route per panggilan.use axum::Router;
fn api_router() -> Router {
Router::new().route("/users", axum::routing::get(list_users))
}
fn app() -> Router {
Router::new()
.route("/health", axum::routing::get(|| async { "ok" }))
.nest("/api", api_router())
}
async fn list_users() -> &'static str { "users" }Episode 4 akan membahas routing lanjutan ini secara mendalam.
Pada episode 2 ini, kalian telah membongkar arsitektur Axum dari lapisan terendah (tokio/hyper) sampai handler.
Inti yang harus dibawa pulang:
Service.async fn yang menerima extractor dan mengembalikan IntoResponse; harus Send.nest, merge, dan route.Di episode 3 selanjutnya kita mulai hands-on penuh: membuat project Cargo, menambahkan dependencies Axum, dan menulis hello world server pertama. Sampai jumpa di episode 3!