Web Server

Web Server #

Ekosistem web Rust telah berkembang pesat dan kini punya beberapa framework matang yang siap produksi. Tiga yang paling populer adalah axum (dibangun oleh tim tokio, ergonomis dan modular), actix-web (performa tertinggi di benchmark, berbasis aktor), dan rocket (paling ramah pemula, banyak fitur bawaan). Artikel ini membahas ketiganya dengan contoh yang cukup dalam untuk memulai — routing, JSON, shared state, middleware, dan error handling — lalu diakhiri dengan panduan deployment dengan Docker.

Perbandingan Framework #

flowchart TD
    Q{Prioritas utama?}
    Q --> P["Performa\nabsolut tertinggi"]
    Q --> E["Ergonomis\ndan modular"]
    Q --> B["Ramah pemula\nfitur built-in"]

    P --> AW["actix-web\nBerbasis aktor\nPerforma benchmark tertinggi\nEkosistem sangat kaya"]
    E --> AX["axum\nDibuat tim tokio\nExtractor pattern elegan\nMiddleware via tower"]
    B --> RK["rocket\nMakro yang ekspresif\nValidasi request otomatis\nAsync native di v0.5"]
Aspekaxumactix-webrocket
Versi stabil0.74.x0.5
Async runtimetokiotokio (custom)tokio
PerformaSangat tinggiTertinggiTinggi
Kurva belajarSedangSedangRendah
MiddlewareTower ecosystemActix middlewareFairing
Cocok untukAPI, microserviceHigh-performance APIFullstack web

Axum — Ergonomis dan Modular #

Instalasi #

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace"] }

Server Dasar dengan Routing #

use axum::{
    extract::{Path, Query},
    http::StatusCode,
    response::Json,
    routing::{delete, get, post, put},
    Router,
};
use serde::{Deserialize, Serialize};

#[derive(Serialize)]
struct Respons {
    pesan: String,
    status: u16,
}

// Handler sederhana
async fn root() -> &'static str {
    "Selamat datang di API Rust!"
}

// Path parameter
async fn salam(Path(nama): Path<String>) -> String {
    format!("Halo, {}!", nama)
}

// Query string: GET /cari?kata=rust&halaman=2
#[derive(Deserialize)]
struct ParameterCari {
    kata: String,
    halaman: Option<u32>,
}

async fn cari(Query(params): Query<ParameterCari>) -> Json<Respons> {
    let pesan = format!(
        "Mencari '{}' di halaman {}",
        params.kata,
        params.halaman.unwrap_or(1)
    );
    Json(Respons { pesan, status: 200 })
}

// Multiple path parameters
async fn artikel_berdasarkan_kategori(
    Path((kategori, id)): Path<(String, u64)>,
) -> String {
    format!("Kategori: {}, ID artikel: {}", kategori, id)
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(root))
        .route("/salam/:nama", get(salam))
        .route("/cari", get(cari))
        .route("/artikel/:kategori/:id", get(artikel_berdasarkan_kategori));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("Server di http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}

JSON Request dan Response #

use axum::{extract::Json, http::StatusCode, response::IntoResponse, routing::post, Router};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct RequestBuatPengguna {
    nama: String,
    email: String,
    usia: u8,
}

#[derive(Serialize)]
struct ResponsPengguna {
    id: u64,
    nama: String,
    email: String,
}

async fn buat_pengguna(
    Json(body): Json<RequestBuatPengguna>,
) -> (StatusCode, Json<ResponsPengguna>) {
    // Validasi sederhana
    if body.nama.is_empty() || body.email.is_empty() {
        return (
            StatusCode::BAD_REQUEST,
            Json(ResponsPengguna {
                id: 0,
                nama: String::from("error"),
                email: String::from("nama dan email wajib diisi"),
            }),
        );
    }

    // Simulasi simpan ke database
    let pengguna = ResponsPengguna {
        id: 1001,
        nama: body.nama,
        email: body.email,
    };

    (StatusCode::CREATED, Json(pengguna))
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/pengguna", post(buat_pengguna));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Shared State di Axum #

use axum::{extract::State, routing::get, Router};
use std::sync::{Arc, Mutex};

// State aplikasi yang dibagi ke semua handler
#[derive(Clone)]
struct AppState {
    counter: Arc<Mutex<u64>>,
    nama_app: String,
}

async fn ambil_counter(State(state): State<AppState>) -> String {
    let c = state.counter.lock().unwrap();
    format!("{}: counter = {}", state.nama_app, *c)
}

async fn increment(State(state): State<AppState>) -> String {
    let mut c = state.counter.lock().unwrap();
    *c += 1;
    format!("Counter sekarang: {}", *c)
}

#[tokio::main]
async fn main() {
    let state = AppState {
        counter: Arc::new(Mutex::new(0)),
        nama_app: String::from("Aplikasi Rust"),
    };

    let app = Router::new()
        .route("/counter", get(ambil_counter))
        .route("/increment", get(increment))
        .with_state(state);  // injeksi state ke semua route

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Error Handling di Axum #

use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde_json::json;

// Tipe error kustom
enum AppError {
    TidakDitemukan(String),
    ServerError(String),
    BadRequest(String),
}

// Implementasikan IntoResponse agar bisa dikembalikan dari handler
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, pesan) = match self {
            AppError::TidakDitemukan(msg) => (StatusCode::NOT_FOUND, msg),
            AppError::ServerError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
            AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
        };

        let body = Json(json!({
            "error": pesan,
            "status": status.as_u16()
        }));

        (status, body).into_response()
    }
}

async fn cari_pengguna(axum::extract::Path(id): axum::extract::Path<u64>)
    -> Result<Json<serde_json::Value>, AppError>
{
    if id == 0 {
        return Err(AppError::BadRequest("ID tidak boleh 0".into()));
    }

    // Simulasi: hanya ID 1–100 yang ada
    if id > 100 {
        return Err(AppError::TidakDitemukan(format!("Pengguna ID {} tidak ada", id)));
    }

    Ok(Json(json!({
        "id": id,
        "nama": format!("Pengguna {}", id),
        "email": format!("user{}@contoh.com", id)
    })))
}

Actix-web — Performa Tertinggi #

Instalasi #

[dependencies]
actix-web = "4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Server dengan Routing dan JSON #

use actix_web::{
    delete, get, post, put,
    web::{self, Data, Json, Path, Query},
    App, HttpResponse, HttpServer, Responder,
};
use serde::{Deserialize, Serialize};
use std::sync::Mutex;

#[derive(Serialize, Deserialize, Clone)]
struct Produk {
    id: u64,
    nama: String,
    harga: f64,
}

struct AppState {
    produk: Mutex<Vec<Produk>>,
}

// GET /produk — ambil semua produk
#[get("/produk")]
async fn daftar_produk(state: Data<AppState>) -> impl Responder {
    let produk = state.produk.lock().unwrap();
    HttpResponse::Ok().json(produk.clone())
}

// GET /produk/{id}
#[get("/produk/{id}")]
async fn ambil_produk(path: Path<u64>, state: Data<AppState>) -> impl Responder {
    let id = path.into_inner();
    let produk = state.produk.lock().unwrap();

    match produk.iter().find(|p| p.id == id) {
        Some(p) => HttpResponse::Ok().json(p),
        None => HttpResponse::NotFound().json(serde_json::json!({
            "error": format!("Produk {} tidak ditemukan", id)
        })),
    }
}

// POST /produk — buat produk baru
#[post("/produk")]
async fn buat_produk(body: Json<Produk>, state: Data<AppState>) -> impl Responder {
    let mut produk = state.produk.lock().unwrap();
    produk.push(body.into_inner());
    HttpResponse::Created().json(produk.last().unwrap())
}

// DELETE /produk/{id}
#[delete("/produk/{id}")]
async fn hapus_produk(path: Path<u64>, state: Data<AppState>) -> impl Responder {
    let id = path.into_inner();
    let mut produk = state.produk.lock().unwrap();
    let panjang_awal = produk.len();
    produk.retain(|p| p.id != id);

    if produk.len() < panjang_awal {
        HttpResponse::Ok().json(serde_json::json!({"dihapus": id}))
    } else {
        HttpResponse::NotFound().body("Tidak ditemukan")
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let state = Data::new(AppState {
        produk: Mutex::new(vec![
            Produk { id: 1, nama: "Laptop".into(), harga: 15_000_000.0 },
            Produk { id: 2, nama: "Mouse".into(), harga: 250_000.0 },
        ]),
    });

    println!("Server actix-web di http://localhost:8080");
    HttpServer::new(move || {
        App::new()
            .app_data(state.clone())
            .service(daftar_produk)
            .service(ambil_produk)
            .service(buat_produk)
            .service(hapus_produk)
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

Middleware di Actix-web #

use actix_web::{
    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
    middleware::Logger,
    web, App, Error, HttpServer,
};
use std::future::{ready, Future, Ready};
use std::pin::Pin;
use std::time::Instant;

// Middleware logging waktu proses request
pub struct TimingMiddleware;

impl<S, B> Transform<S, ServiceRequest> for TimingMiddleware
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type InitError = ();
    type Transform = TimingService<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(TimingService { service }))
    }
}

pub struct TimingService<S> {
    service: S,
}

impl<S, B> Service<ServiceRequest> for TimingService<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;

    forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let mulai = Instant::now();
        let metode = req.method().to_string();
        let path = req.path().to_string();
        let fut = self.service.call(req);

        Box::pin(async move {
            let resp = fut.await?;
            println!("{} {} selesai dalam {:?}", metode, path, mulai.elapsed());
            Ok(resp)
        })
    }
}

Rocket — Ramah Pemula #

Instalasi #

[dependencies]
rocket = { version = "0.5", features = ["json"] }
serde = { version = "1", features = ["derive"] }

Server dengan Routing, JSON, dan State #

#[macro_use]
extern crate rocket;

use rocket::{
    serde::{json::Json, Deserialize, Serialize},
    State,
};
use std::sync::Mutex;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
struct Catatan {
    id: u64,
    judul: String,
    isi: String,
}

struct DbCatatan {
    daftar: Mutex<Vec<Catatan>>,
    id_berikutnya: Mutex<u64>,
}

// GET / — halaman utama
#[get("/")]
fn index() -> &'static str {
    "API Catatan dengan Rocket"
}

// GET /catatan — semua catatan
#[get("/catatan")]
fn daftar_catatan(db: &State<DbCatatan>) -> Json<Vec<Catatan>> {
    Json(db.daftar.lock().unwrap().clone())
}

// GET /catatan/<id>
#[get("/catatan/<id>")]
fn ambil_catatan(id: u64, db: &State<DbCatatan>) -> Option<Json<Catatan>> {
    let daftar = db.daftar.lock().unwrap();
    daftar.iter().find(|c| c.id == id).cloned().map(Json)
}

// POST /catatan
#[post("/catatan", data = "<catatan>")]
fn buat_catatan(catatan: Json<Catatan>, db: &State<DbCatatan>) -> Json<Catatan> {
    let mut id_counter = db.id_berikutnya.lock().unwrap();
    let mut daftar = db.daftar.lock().unwrap();

    let baru = Catatan {
        id: *id_counter,
        judul: catatan.judul.clone(),
        isi: catatan.isi.clone(),
    };
    *id_counter += 1;
    daftar.push(baru.clone());
    Json(baru)
}

// DELETE /catatan/<id>
#[delete("/catatan/<id>")]
fn hapus_catatan(id: u64, db: &State<DbCatatan>) -> &'static str {
    let mut daftar = db.daftar.lock().unwrap();
    let panjang = daftar.len();
    daftar.retain(|c| c.id != id);
    if daftar.len() < panjang { "Berhasil dihapus" } else { "Tidak ditemukan" }
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(DbCatatan {
            daftar: Mutex::new(Vec::new()),
            id_berikutnya: Mutex::new(1),
        })
        .mount("/", routes![index, daftar_catatan, ambil_catatan, buat_catatan, hapus_catatan])
}

Deployment dengan Docker #

Dockerfile multi-stage untuk binary Rust yang ringan:

# Stage 1: Build
FROM rust:1.77-slim as builder

WORKDIR /app
COPY Cargo.toml Cargo.lock ./
# Build dependensi dulu (di-cache) agar rebuild lebih cepat
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm src/main.rs

# Copy source dan build ulang hanya source
COPY src ./src
RUN touch src/main.rs && cargo build --release

# Stage 2: Runtime — image sangat kecil
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/target/release/nama-aplikasi /usr/local/bin/

EXPOSE 3000
CMD ["nama-aplikasi"]
# Build image
docker build -t rust-webserver .

# Jalankan container
docker run -p 3000:3000 rust-webserver

# Dengan environment variable
docker run -p 3000:3000 -e DATABASE_URL=postgres://... rust-webserver

Binary Rust yang di-compile dengan --release dan strip = true (di Cargo.toml) bisa sekecil 2–5 MB, membuat container akhir sangat ringan.


Ringkasan #

  • Pilih framework berdasarkan prioritasaxum untuk API modern yang ergonomis dan terintegrasi baik dengan ekosistem tower, actix-web untuk performa maksimal, rocket untuk kemudahan pengembangan.
  • Semua framework mendukung JSON via serdeJson<T> sebagai extractor otomatis deserialize request body; Json<T> sebagai return type otomatis serialize response.
  • Shared state dengan Arc<Mutex<T>> — inject ke semua handler via State (axum) atau Data (actix-web) atau State rocket. Untuk akses baca banyak-tulis sedikit, pertimbangkan RwLock.
  • Handler di axum mengembalikan impl IntoResponse — berbagai tipe bisa dikembalikan: string, StatusCode, tuple, Json, dan tipe kustom yang mengimplementasikan IntoResponse.
  • Error handling di axum via Result<T, E> — buat enum error dan implementasikan IntoResponse, lalu handler bisa return Result<Json<T>, AppError>.
  • Middleware di axum via tower — gunakan tower-http untuk CORS, request logging, timeout, dan kompresi dengan mudah: .layer(CorsLayer::permissive()).
  • Docker multi-stage untuk binary kecil — build di image Rust lengkap, copy binary ke debian:slim atau bahkan scratch. Hasil akhir 5–20 MB vs 1+ GB image builder.
  • Actix-web #[get], #[post], dll. adalah proc-macro yang mendefinisikan route dan method HTTP sekaligus — lebih ringkas dari registrasi manual.

← Sebelumnya: Web Socket   Berikutnya: Unit Test →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact