Belajar Axum - Arsitektur & Konsep
Episode 2 of 28

Belajar Axum - Arsitektur & Konsep

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.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

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.

Arsitektur Berlapis: dari Socket ke Handler

Axum tidak berdiri sendiri. Ia duduk di atas rantai komponen yang saling meminjam pekerjaan:

100%

Penjelasan setiap lapisan:

  1. Tokio — async runtime yang menyediakan executor multi-thread. Ia yang membaca soket TCP dan menjadwalkan task.
  2. Hyper — implementasi HTTP/1.1 dan HTTP/2 di Rust. Hyper menerjemahkan byte di wire ke struktur Request dan Response.
  3. Tower — abstraksi service: sebuah Service adalah async fn yang menerima Request dan mengembalikan Result<Response>. Ini "bahasa umum" di ekosistem Rust.
  4. Axum Router — juga sebuah Service. Ia mencocokkan request ke handler berdasarkan path dan method.
  5. Handlerasync 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: inti dari Segalanya

Handler adalah async fn yang bisa menerima extractor sebagai argumen dan mengembalikan apa pun yang mengimplement IntoResponse:

Bentuk handler paling sederhana
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.

Mengapa handler harus Send

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.

Extractors: Mengambil Apa yang Dibutuhkan

Extractor adalah cara Axum membaca request dan menyediakan nilainya ke handler. Empat yang paling sering dipakai:

ExtractorMengambilDipakai Untuk
PathSegmen path (contoh /:id)ID resource pada REST
QueryQuery string (contoh ?page=2)Pagination, filter
JsonBody yang di-parse sebagai JSONBody POST/PUT
StateState aplikasi yang dibagikanDB pool, config

Contoh handler yang memakai tiga extractor sekaligus:

Kombinasi extractor
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.

Responses: IntoResponse

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.
  • Tuple seperti (StatusCode, Json<T>) → kombinasi status + body.
Berbagai bentuk response
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 Stack

Middleware adalah lapisan yang membungkus service untuk memproses request sebelum handler (dan response setelahnya). Secara konseptual:

100%

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.

Komposabilitas Router

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.
Komposisi router
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.

Penutup

Pada episode 2 ini, kalian telah membongkar arsitektur Axum dari lapisan terendah (tokio/hyper) sampai handler.

Inti yang harus dibawa pulang:

  • Alur request: tokio → hyper → tower → Router → handler; Router sendiri adalah tower Service.
  • Handler adalah async fn yang menerima extractor dan mengembalikan IntoResponse; harus Send.
  • Extractor membaca request dengan bantuan tipe; extractor pemakan body harus di argumen terakhir.
  • IntoResponse dipakai untuk segala bentuk response, termasuk tuple status + body.
  • Middleware membungkus service berlapis; urutan pemasangan menentukan perilaku.
  • Router komposable lewat 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!