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.

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.
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.
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::new_for_http() mencatat setiap request beserta status, durasi, dan latency. Ini lapisan pertama yang harus dipasang di aplikasi produksi mana pun:
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:
INFO request{method=GET uri=/ status=200 latency=2ms}: tower_http::trace::on_response: finishedKita 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.
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.
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:
| Opsi | Dampak |
|---|---|
Any | Semua origin boleh — hanya untuk pengembangan |
| Origin spesifik | Aman 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).
Handler yang menggantung (misal query DB yang lambat) akan menahan worker thread. TimeoutLayer memutus sirkuit:
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::new() mengompres response (gzip/br/zstd) jika klien mendukungnya:
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.
Ketika layer siap pakai tidak mencukupi, Axum menyediakan middleware::from_fn — cara termudah menulis middleware kustom sebagai async fn biasa:
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:
HeaderMap) + Request + Next → Result<Response, _>. Axum mengekstrak nilai dari request sebelum meneruskan, sama seperti handler..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.
Middleware kadang butuh state aplikasi — misal config. Itu kerja from_fn_with_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:
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.
Gabungkan semua lapisan menjadi satu pola yang akan menjadi titik awal project kalian:
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.
Pada episode 8 ini kalian telah menyusun middleware stack:
TraceLayer untuk logging, CorsLayer, TimeoutLayer, CompressionLayer dari tower-http.from_fn/from_fn_with_state untuk custom middleware dengan akses state..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!