Belajar Axum - Middleware
Episode 8 of 28

Belajar Axum - Middleware

Menyusun middleware stack di Axum: TraceLayer untuk logging, CorsLayer, TimeoutLayer, CompressionLayer dari tower-http, lalu membangun custom middleware dengan from_fn_with_state dan menambah data ke request via extensions.

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

Pendahuluan

Di episode 2 kita melihat middleware secara konseptual — lapisan yang membungkus service. Episode 7 menambah state. Sekarang keduanya bertemu: middleware perlu mengakses state, dan itulah yang membuatnya benar-benar berguna di produksi.

Episode ini membangun middleware stack yang realistis: logging (TraceLayer), CORS, timeout, dan compression dari tower-http — lalu custom middleware dengan from_fn_with_state dan cara menyisipkan data ke request lewat extensions. Setelah episode ini, aplikasi kalian akan punya fondasi "plumbing" yang dipakai semua fitur berikutnya.

Layer: Bahasa Middleware tower

Axum mengadopsi konsep Layer dari tower: sebuah Layer adalah pabrik yang membungkus service menjadi service baru. Sintaks Axum .layer(...) menaruh layer paling luar — sesuai urutan pemasangan.

Menyusun layer
use axum::Router;
use tower_http::{
    compression::CompressionLayer,
    cors::CorsLayer,
    timeout::TimeoutLayer,
    trace::TraceLayer,
};
use std::time::Duration;
 
fn build_app() -> Router {
    Router::new()
        .route("/api", axum::routing::get(|| async { "api" }))
        .layer(TraceLayer::new_for_http())
        .layer(CorsLayer::permissive())
        .layer(TimeoutLayer::new(Duration::from_secs(30)))
        .layer(CompressionLayer::new())
}

Urutan di atas berarti: request masuk → Trace mencatat → CORS memeriksa asal → Timeout menetapkan batas → Compression menyiapkan response terkompres → handler. Saat response kembali, urutan dibalik: kompresi, lalu timeout selesai, lalu CORS menambah header, lalu trace menutup span.

Warning

Urutan layer itu penting dan sering salah. TimeoutLayer sebaiknya di dalam CORS (seperti contoh di atas) supaya request preflight OPTIONS yang butuh header CORS tidak terpotong sebelum waktunya. Uji perilaku stack kalian dengan request nyata — jangan hanya mengandalkan logika.

TraceLayer: Logging Request

TraceLayer::new_for_http() mencatat setiap request beserta status, durasi, dan latency. Ini lapisan pertama yang harus dipasang di aplikasi produksi mana pun:

TraceLayer dengan format default
use tower_http::trace::TraceLayer;
 
Router::new()
    .route("/", axum::routing::get(|| async { "ok" }))
    .layer(TraceLayer::new_for_http())

Dengan tracing_subscriber dari episode 3, output di terminal akan seperti:

Contoh output tracing
INFO request{method=GET uri=/ status=200 latency=2ms}: tower_http::trace::on_response: finished

Kita tidak akan membedah format span di sini — observability penuh (termasuk log terstruktur dan OpenTelemetry) adalah topik episode 17. Yang penting sekarang: pasang TraceLayer lebih awal, karena ia mencatat error yang belum tentu terlihat di handler.

CorsLayer: Mengatur Siapa yang Boleh

CORS (Cross-Origin Resource Sharing) mengatur apakah browser boleh memanggil API dari origin berbeda. Contohnya: frontend di app.kita.id memanggil API di api.kita.id.

Konfigurasi CORS
use tower_http::cors::{Any, CorsLayer};
use axum::http::{header, Method};
 
fn cors() -> CorsLayer {
    CorsLayer::new()
        .allow_origin("https://app.kita.id".parse::<axum::http::HeaderValue>().unwrap())
        .allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
        .allow_headers([
            header::CONTENT_TYPE,
            header::AUTHORIZATION,
            header::HeaderName::from_static("x-trace-id"),
        ])
}

Tiga pilihan allow_origin yang umum:

OpsiDampak
AnySemua origin boleh — hanya untuk pengembangan
Origin spesifikAman untuk produksi satu frontend
Any + allow_credentials(true)Berbahaya — kombinasi ini dilarang browser

Jangan pernah pasang CorsLayer::permissive() di produksi tanpa berpikir dua kali — itu membuka API kalian untuk dipanggil dari situs mana pun, yang menyulitkan CSRF protection (episode 18).

TimeoutLayer: Batas Waktu Response

Handler yang menggantung (misal query DB yang lambat) akan menahan worker thread. TimeoutLayer memutus sirkuit:

Timeout global
use std::time::Duration;
use tower_http::timeout::TimeoutLayer;
 
.layer(TimeoutLayer::new(Duration::from_secs(30)))

Setelah 30 detik, request dibatalkan dan klien mendapat 408 Request Timeout. Pilih nilai berdasarkan SLA: API internal boleh 5-10 detik, endpoint dengan task berat boleh lebih lama. Untuk per-route timeout yang berbeda, pasang TimeoutLayer pada sub-router yang bersangkutan.

CompressionLayer: Response Lebih Kecil

CompressionLayer::new() mengompres response (gzip/br/zstd) jika klien mendukungnya:

Compression default
use tower_http::compression::CompressionLayer;
 
.layer(CompressionLayer::new())

Response JSON dengan banyak string bisa mengecil 5-10x — penghematan bandwidth yang besar untuk API publik. Perhatikan: jangan pasang compression di belakang proxy yang sudah mengompres (nginx/caddy), karena duplikasi kerja. Topik proxy kita bahas di episode 20.

Custom Middleware: from_fn dan from_fn_with_state

Ketika layer siap pakai tidak mencukupi, Axum menyediakan middleware::from_fn — cara termudah menulis middleware kustom sebagai async fn biasa:

from_fn middleware
use axum::{
    body::Body,
    extract::{Request, State},
    http::StatusCode,
    middleware::{self, Next},
    response::Response,
};
 
async fn require_api_key(
    headers: axum::http::HeaderMap,
    request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    match headers.get("x-api-key") {
        Some(key) if key == "secret-dev-key" => Ok(next.run(request).await),
        _ => Err(StatusCode::UNAUTHORIZED),
    }
}
 
fn app() -> Router {
    Router::new()
        .route("/private", axum::routing::get(|| async { "rahasia" }))
        .route_layer(middleware::from_fn(require_api_key))
}

Perhatikan dua hal:

  1. Signature middleware: extractor (di sini HeaderMap) + Request + NextResult<Response, _>. Axum mengekstrak nilai dari request sebelum meneruskan, sama seperti handler.
  2. .route_layer vs .layer: .route_layer hanya membungkus route yang ada di router tersebut — route baru yang ditambahkan nanti tidak otomatis terkena middleware. .layer membungkus router dan semua anaknya. Untuk middleware yang harus menangani semua route, gunakan .layer.

Pola require_api_key di atas sengaja sederhana; autentikasi sungguhan (JWT) kita bangun di episode 13.

Tip

Middleware dari from_fn bisa membungkus request baru dan response sebelum/ sesudah next.run(request).await. Contoh: tambahkan header ke request (misal timestamp), panggil next.run(), lalu tambahkan header ke response (misal X-Served-By). Inilah mekanisme "ekstensi request" Axum.

from_fn_with_state dan Extensions

Middleware kadang butuh state aplikasi — misal config. Itu kerja from_fn_with_state:

Middleware dengan state
use axum::{
    extract::{Request, State},
    http::HeaderValue,
    middleware::Next,
    response::Response,
};
 
#[derive(Clone)]
struct AppState {
    region: String,
}
 
async fn add_region_header(
    State(state): State<AppState>,
    request: Request,
    next: Next,
) -> Response {
    let mut response = next.run(request).await;
    if let Ok(value) = HeaderValue::from_str(&state.region) {
        response.headers_mut().insert("x-region", value);
    }
    response
}
 
fn app() -> Router {
    Router::new()
        .route("/", axum::routing::get(|| async { "ok" }))
        .layer(middleware::from_fn_with_state(
            AppState { region: "ap-southeast-1".into() },
            add_region_header,
        ))
}

Jenis data lain yang bisa disisipkan ke request adalah extensions — peta data per-request yang bisa ditulis middleware dan dibaca handler/extractor berikutnya:

Menulis extension di middleware
use axum::extract::Request;
 
async fn inject_request_id(mut request: Request, next: Next) -> Response {
    let id = uuid::Uuid::new_v4().to_string();
    request.extensions_mut().insert(RequestId(id));
    next.run(request).await
}
 
#[derive(Clone)]
struct RequestId(String);

Extension RequestId ini nantinya bisa dibaca di handler maupun extractor custom (episode 21) — pola penting untuk tracing per-request yang menyambung ke episode 17.

Stack Lengkap yang Siap Produksi

Gabungkan semua lapisan menjadi satu pola yang akan menjadi titik awal project kalian:

Stack middleware lengkap
use std::time::Duration;
use axum::Router;
use tower_http::{
    compression::CompressionLayer,
    cors::CorsLayer,
    timeout::TimeoutLayer,
    trace::TraceLayer,
};
 
fn build_app(state: AppState) -> Router {
    Router::new()
        .route("/api/health", axum::routing::get(|| async { "ok" }))
        .layer(middleware::from_fn_with_state(state.clone(), add_region_header))
        .layer(TraceLayer::new_for_http())
        .layer(cors())
        .layer(TimeoutLayer::new(Duration::from_secs(30)))
        .layer(CompressionLayer::new())
        .with_state(state)
}

Urutan dari luar ke dalam: region header → trace → cors → timeout → compression → handler. Setiap lapisan punya satu tanggung jawab, dan semuanya terpasang di satu titik — jauh lebih mudah dikelola daripada menyebar middleware di tiap handler.

Penutup

Pada episode 8 ini kalian telah menyusun middleware stack:

  • TraceLayer untuk logging, CorsLayer, TimeoutLayer, CompressionLayer dari tower-http.
  • Urutan layer menentukan perilaku; timeout sebaiknya di dalam CORS.
  • from_fn/from_fn_with_state untuk custom middleware dengan akses state.
  • Extensions untuk menyisipkan data per-request (misal request ID).
  • .route_layer vs .layer untuk mengontrol cakupan middleware.

Di episode 9 selanjutnya kita masuk fase yang paling "sungguhan": CRUD dengan sqlx — koneksi PostgreSQL, migration, pool, dan REST CRUD penuh yang memakai semua yang sudah dipelajari: extractor, state, error handling, dan routing. Sampai jumpa di episode 9!