Belajar Axum - Routing Lanjutan
Episode 4 of 28

Belajar Axum - Routing Lanjutan

Memperluas kemampuan Router Axum: nested routes dengan nest, path parameter, query string, wildcard, fallback, hingga merge dan layering — lalu merangkainya menjadi pola routing REST resource yang realistis.

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

Pendahuluan

Di episode 3 kita menulis satu route: GET /. Server berjalan, tapi belum terasa seperti API. Episode ini akan mengubah router kalian dari satu baris menjadi struktur nyata yang bisa melayani REST API: path parameter untuk resource, query string untuk filter, fallback untuk 404 yang ramah, serta nest dan merge untuk menyusun modul routing yang bersih.

Mengapa routing perlu diperlakukan serius? Karena dalam API produksi, URL adalah contract dengan klien. Kesalahan kecil — path yang tidak konsisten, wildcard yang keliru, fallback yang tidak disetel — langsung terlihat oleh pengguna. Di episode ini kita membangun kebiasaan routing yang benar sejak awal.

Route dengan Path Parameter

Path parameter ditulis dengan awalan : (pada axum 0.6+, sintaks {} tidak lagi didukung). Nilainya diambil oleh extractor Path:

Path parameter dasar
use axum::{extract::Path, routing::get, Router};
 
async fn get_user(Path(id): Path<u32>) -> String {
    format!("Mengambil user dengan ID {id}")
}
 
fn users_router() -> Router {
    Router::new().route("/users/:id", get(get_user))
}

Apa yang terjadi jika kalian request /users/abc? Axum akan mencoba meng-parse abc menjadi u32 dan gagal → respons 400 Bad Request otomatis. Inilah kelebihan extractor bertipe: validasi dasar terjadi tanpa kode tambahan.

Jika ingin lebih dari satu parameter, gunakan tuple atau struct Deserialize:

Multiple path params
use axum::extract::Path;
use serde::Deserialize;
 
#[derive(Deserialize)]
struct ArticlePath {
    author: String,
    slug: String,
}
 
async fn get_article(Path(ArticlePath { author, slug }): Path<ArticlePath>) -> String {
    format!("Artikel {slug} oleh {author}")
}
 
// route: /articles/:author/:slug

Tuple lebih cepat untuk 2-3 parameter; struct Deserialize lebih ekspresif untuk lebih dari itu.

Query Parameter

Query string diekstrak dengan extractor Query. Karena query bersifat opsional, pastikan field struct diberi default atau dibuat Option:

Query parameter dengan default
use axum::extract::Query;
use serde::Deserialize;
 
#[derive(Deserialize)]
struct Pagination {
    page: Option<u32>,
    per_page: Option<u32>,
    sort: Option<String>,
}
 
async fn list_posts(Query(p): Query<Pagination>) -> String {
    let page = p.page.unwrap_or(1);
    let per_page = p.per_page.unwrap_or(20);
    format!("Halaman {page}, {per_page} per halaman")
}

Jika klien mengirim query yang tidak dikenal, extractor Query secara default mengabaikannya. Ini berbeda dari struct Deserialize biasa yang akan error pada field tak dikenal — Axum memakai serde_path_to_error dengan toleransi field ekstra, sehingga API kalian tidak mudah rusak oleh parameter yang disisipkan klien.

Warning

Hati-hati dengan query yang bertipe bool atau u32 tanpa default: jika klien mengirim ?sort= (kosong), parsing gagal dan Axum membalas 400. Untuk query opsional, selalu pakai Option&lt;T&gt; — bukan T dengan default — agar API tetap ramah.

Wildcard dan Catch-all

Axum 0.8 memakai sintaks /*rest (ganti {*rest} yang lama) untuk menangkap sisa path:

Wildcard segment
use axum::extract::Path;
 
async fn catch_all(Path(rest): Path<String>) -> String {
    format!("Tersisa: {rest}")
}
 
fn router() -> Router {
    Router::new().route("/files/*rest", axum::routing::get(catch_all))
}

Request ke /files/a/b/c akan membuat rest = "a/b/c". Wildcard ini berguna untuk route "proxy" atau fallback layanan statis — kita pakai lagi di episode 11.

Fallback: Menangani 404 dengan Sopan

Jika tidak ada route yang cocok, Axum mengembalikan 404 polos. fallback memungkinkan kita menggantinya dengan respons sendiri:

Fallback handler
use axum::{http::StatusCode, Json};
 
async fn not_found() -> (StatusCode, Json<serde_json::Value>) {
    (
        StatusCode::NOT_FOUND,
        Json(serde_json::json!({ "error": "endpoint tidak ditemukan" })),
    )
}
 
fn router() -> Router {
    Router::new()
        .route("/api/health", axum::routing::get(|| async { "ok" }))
        .fallback(not_found)
}

Perhatikan: .fallback di level Router utama hanya menangani route yang tidak cocok di level itu juga. Sub-router hasil nest punya fallback-nya sendiri yang terpisah — detail penting saat project membesar.

Nested Routes: nest

nest menggantung router di bawah prefix. Ini alat utama untuk membagi aplikasi menjadi modul:

Nested router
use axum::{routing::{get, post}, Router};
 
async fn list_users() -> &'static str { "daftar user" }
async fn create_user() -> &'static str { "buat user" }
async fn list_orders() -> &'static str { "daftar order" }
 
fn users_api() -> Router {
    Router::new()
        .route("/", get(list_users))
        .route("/", post(create_user))
}
 
fn orders_api() -> Router {
    Router::new().route("/", get(list_orders))
}
 
fn api() -> Router {
    Router::new()
        .nest("/users", users_api())
        .nest("/orders", orders_api())
}

Dengan pola ini, GET /users dan POST /users di-declare di users_api(), GET /orders di orders_api(). Modul baru tinggal menambah satu baris nest.

Ada satu perilaku yang sering mengejutkan pemula: ketika di-nest, path parameter tetap bekerja relatif terhadap prefix nest. Route "/users/:id" di dalam users_api() yang di-nest("/users") akan cocok dengan /users/:id — bukan /users/users/:id.

Merge: Menggabungkan Router Independen

merge menggabungkan dua router yang path-nya tidak bertabrakan (jika bertabrakan, program panic di runtime saat start):

Merge router
fn admin_routes() -> Router {
    Router::new()
        .route("/admin/users", get(list_users))
        .route("/admin/stats", get(|| async { "stats" }))
}
 
fn public_routes() -> Router {
    Router::new().route("/", get(|| async { "beranda" }))
}
 
fn app() -> Router {
    public_routes().merge(admin_routes())
}

Perbedaan nest vs merge:

Aspeknestmerge
Efek pada pathMenambah prefixTidak mengubah path
Tabrakan routeTidak masalah (prefix berbeda)Panic jika sama
Use caseMemecah modul per domainMenggabungkan grup yang sudah "jadi"

Pola umum di project besar: tiap modul mengembalikan Router sendiri (via nest), lalu merge-kan di level aplikasi utama.

REST Resource Routing Praktis

Mari rangkai semuanya menjadi pola REST resource yang lengkap — pola ini akan dipakai terus di episode 9 saat CRUD dengan database:

REST resource users
use axum::{
    extract::Path,
    routing::{delete, get, patch, post},
    Router,
};
 
async fn list_users() -> &'static str { "GET /users" }
async fn create_user() -> &'static str { "POST /users" }
async fn get_user(Path(id): Path<u32>) -> String { format!("GET /users/{id}") }
async fn update_user(Path(id): Path<u32>) -> String { format!("PATCH /users/{id}") }
async fn delete_user(Path(id): Path<u32>) -> String { format!("DELETE /users/{id}") }
 
fn users_resource() -> Router {
    Router::new()
        .route("/users", get(list_users).post(create_user))
        .route(
            "/users/:id",
            get(get_user).patch(update_user).delete(delete_user),
        )
}
 
fn app() -> Router {
    users_resource()
}

Perhatikan idiom .get(...).post(...) pada route yang sama — Axum memungkinkan beberapa method pada satu path, menghindari duplikasi string path. Ini juga contoh layering: MethodRouter dibangun dengan rantai method, lalu dipasang ke Router.

Tip

Aturan penamaan path REST: gunakan jamak untuk resource (/users), identifikasi resource dengan ID di path (/users/:id), dan gunakan kata benda bukan kata kerja (/users/:id/posts, bukan /get-user-posts). Axum tidak memaksa, tetapi konsistensi ini membuat API kalian mudah ditebak.

Penutup

Pada episode 4 ini kalian telah menguasai routing lanjutan Axum:

  • Path untuk parameter path bertipe, termasuk tuple dan struct Deserialize.
  • Query untuk query string dengan pola Option untuk opsional.
  • /*rest untuk wildcard, .fallback untuk 404 kustom.
  • nest untuk prefix/modul, merge untuk menggabungkan router.
  • Pola REST resource dengan multi-method routing.

Di episode 5 selanjutnya kita bedah extractors secara menyeluruh: Json, Path, Query, Header, State, String, Bytes, Form, dan cara membuat extractor custom — inti dari "type-safe request handling" yang membuat Axum unik. Sampai jumpa di episode 5!

Belajar Axum - Routing Lanjutan | Belajar Axum