Rocket

Rocket #

Rocket adalah web framework Rust yang paling ramah pemula — filosofinya adalah convention over configuration dan memanfaatkan sistem tipe Rust untuk validasi request secara otomatis. Jika Actix-web mengutamakan performa dan kontrol, Rocket mengutamakan ergonomi dan kejelasan kode. Parameter route di-extract dan di-validasi secara otomatis; jika tipe tidak cocok, Rocket langsung mengembalikan 422 tanpa kode boilerplate. Request Guard adalah fitur unik Rocket yang memungkinkan autentikasi dan validasi dikodekan sebagai tipe — jika guard gagal, handler tidak dipanggil sama sekali. Artikel ini membahas Rocket 0.5 (stable, async native) secara menyeluruh.

Instalasi #

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

Aplikasi Dasar #

#[macro_use]
extern crate rocket;

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

#[get("/")]
fn index() -> &'static str {
    "Selamat datang di Rocket!"
}

#[get("/halo/<nama>")]
fn halo(nama: &str) -> String {
    format!("Halo, {}!", nama)
}

#[derive(Serialize)]
struct InfoAplikasi {
    nama: &'static str,
    versi: &'static str,
    framework: &'static str,
}

#[get("/info")]
fn info() -> Json<InfoAplikasi> {
    Json(InfoAplikasi {
        nama: "API Server",
        versi: "1.0.0",
        framework: "Rocket 0.5",
    })
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index, halo, info])
}

Routing Lanjutan #

Path dan Query Parameter #

use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};

// Path parameter dengan berbagai tipe — otomatis dikonversi dan divalidasi
#[get("/pengguna/<id>")]
fn ambil_pengguna(id: u64) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "id": id,
        "nama": format!("Pengguna {}", id)
    }))
}

// Multiple path parameter
#[get("/pengguna/<user_id>/artikel/<slug>")]
fn artikel_pengguna(user_id: u64, slug: &str) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "pengguna_id": user_id,
        "slug": slug
    }))
}

// Query parameter
#[derive(FromForm)]
struct ParameterDaftar {
    halaman: Option<u32>,
    per_halaman: Option<u32>,
    cari: Option<String>,
}

#[get("/pengguna?<params..>")]
fn daftar_pengguna(params: ParameterDaftar) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "halaman": params.halaman.unwrap_or(1),
        "per_halaman": params.per_halaman.unwrap_or(20),
        "cari": params.cari,
        "data": []
    }))
}

// Route ranking — Rocket mencocokkan yang paling spesifik
#[get("/pengguna/aktif")]  // lebih spesifik dari <id>
fn pengguna_aktif() -> &'static str {
    "Daftar pengguna aktif"
}

// Catch-all segment dengan <path..>
#[get("/file/<path..>")]
fn file(path: std::path::PathBuf) -> String {
    format!("Path: {}", path.display())
}

JSON Request dan Response #

use rocket::serde::json::{Json, Value};

#[derive(Deserialize, Serialize, Debug)]
struct Produk {
    #[serde(skip_deserializing)]  // tidak bisa dikirim dari client
    id: Option<u64>,
    nama: String,
    harga: f64,
    stok: u32,
    kategori: String,
}

#[post("/produk", data = "<produk>")]
fn buat_produk(produk: Json<Produk>) -> (rocket::http::Status, Json<Produk>) {
    let mut p = produk.into_inner();
    p.id = Some(1001);  // assign ID dari database

    (rocket::http::Status::Created, Json(p))
}

#[put("/produk/<id>", data = "<produk>")]
fn perbarui_produk(id: u64, produk: Json<Produk>) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "id": id,
        "nama": produk.nama,
        "harga": produk.harga,
        "pesan": "Berhasil diperbarui"
    }))
}

#[delete("/produk/<id>")]
fn hapus_produk(id: u64) -> Json<serde_json::Value> {
    Json(serde_json::json!({"dihapus": id}))
}

Form Validation #

Rocket mengintegrasikan validasi form langsung melalui tipe dan FromForm:

use rocket::form::{Form, FromForm, Validate};

#[derive(FromForm, Debug)]
struct FormRegistrasi {
    #[field(validate = len(3..=50))]
    nama: String,

    #[field(validate = contains('@').or_else(msg!("Email tidak valid")))]
    email: String,

    #[field(validate = len(8..))]
    password: String,

    #[field(validate = eq(self.password).or_else(msg!("Password tidak cocok")))]
    konfirmasi_password: String,

    #[field(default = false)]
    setuju_syarat: bool,
}

#[post("/daftar", data = "<form>")]
fn daftar(form: Form<FormRegistrasi>) -> String {
    if !form.setuju_syarat {
        return "Harus setuju dengan syarat dan ketentuan".to_string();
    }
    format!("Pendaftaran berhasil untuk: {}", form.nama)
}

// Form dengan field opsional
#[derive(FromForm)]
struct FormProfil {
    nama: String,
    bio: Option<String>,
    website: Option<String>,
    #[field(validate = range(0..=150))]
    usia: Option<u8>,
}

#[post("/profil", data = "<form>")]
fn perbarui_profil(form: Form<FormProfil>) -> String {
    format!("Profil {} diperbarui", form.nama)
}

Request Guard — Autentikasi sebagai Tipe #

Request Guard adalah fitur paling unik Rocket. Guard adalah tipe yang mengimplementasikan FromRequest — jika guard gagal, handler tidak dipanggil:

use rocket::http::Status;
use rocket::request::{FromRequest, Outcome, Request};
use rocket::outcome::IntoOutcome;

// Guard: token yang valid dari header Authorization
struct AuthToken(String);

#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthToken {
    type Error = &'static str;

    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let token = req.headers().get_one("Authorization")
            .and_then(|h| h.strip_prefix("Bearer "))
            .map(|t| t.to_string());

        match token {
            Some(t) if validasi_token(&t) => Outcome::Success(AuthToken(t)),
            Some(_) => Outcome::Error((Status::Unauthorized, "Token tidak valid")),
            None    => Outcome::Error((Status::Unauthorized, "Token tidak ada")),
        }
    }
}

fn validasi_token(token: &str) -> bool {
    // Validasi JWT atau simple token
    token.starts_with("valid-") || token.len() > 20
}

// Guard: info pengguna dari token
#[derive(Debug)]
struct PenggunaLogin {
    id: u64,
    nama: String,
    peran: String,
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for PenggunaLogin {
    type Error = &'static str;

    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        // Ambil token dulu
        let token = req.guard::<AuthToken>().await
            .map_failure(|(s, _)| (s, "Token tidak valid"))?;

        // Ambil pengguna dari token (dalam produksi: dari DB atau JWT payload)
        Outcome::Success(PenggunaLogin {
            id: 42,
            nama: String::from("Budi"),
            peran: String::from("admin"),
        })
    }
}

// Guard: hanya admin
struct HanyaAdmin(PenggunaLogin);

#[rocket::async_trait]
impl<'r> FromRequest<'r> for HanyaAdmin {
    type Error = &'static str;

    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let pengguna = req.guard::<PenggunaLogin>().await
            .map_failure(|(s, e)| (s, e))?;

        if pengguna.peran == "admin" {
            Outcome::Success(HanyaAdmin(pengguna))
        } else {
            Outcome::Error((Status::Forbidden, "Hanya admin yang boleh akses"))
        }
    }
}

// Route yang butuh autentikasi
#[get("/profil")]
fn profil_saya(pengguna: PenggunaLogin) -> String {
    format!("Halo, {}! ID kamu: {}", pengguna.nama, pengguna.id)
}

// Route yang butuh akses admin
#[get("/admin/pengguna")]
fn daftar_semua_pengguna(admin: HanyaAdmin) -> String {
    format!("Admin {} melihat semua pengguna", admin.0.nama)
}

// Route publik — tanpa guard
#[get("/publik")]
fn route_publik() -> &'static str {
    "Bisa diakses siapa saja"
}

Managed State #

use rocket::{State, serde::json::Json};
use std::sync::{Arc, atomic::{AtomicU64, Ordering}};
use std::collections::HashMap;
use tokio::sync::RwLock;

struct AppState {
    request_count: AtomicU64,
    cache: RwLock<HashMap<String, String>>,
}

#[get("/counter")]
async fn counter(state: &State<AppState>) -> String {
    let n = state.request_count.fetch_add(1, Ordering::Relaxed);
    format!("Request ke-{}", n + 1)
}

#[get("/cache/<kunci>")]
async fn baca_cache(kunci: &str, state: &State<AppState>) -> Option<String> {
    state.cache.read().await.get(kunci).cloned()
}

#[post("/cache/<kunci>", data = "<nilai>")]
async fn tulis_cache(kunci: &str, nilai: String, state: &State<AppState>) -> &'static str {
    state.cache.write().await.insert(kunci.to_string(), nilai);
    "OK"
}

#[launch]
fn rocket() -> _ {
    let state = AppState {
        request_count: AtomicU64::new(0),
        cache: RwLock::new(HashMap::new()),
    };

    rocket::build()
        .manage(state)
        .mount("/", routes![counter, baca_cache, tulis_cache])
}

Fairing — Middleware di Rocket #

Fairing adalah mekanisme middleware Rocket yang dipasang ke lifecycle tertentu (launch, request, response):

use rocket::{fairing::{Fairing, Info, Kind}, http::Header, Request, Response};
use std::time::Instant;

pub struct TimingFairing;

#[rocket::async_trait]
impl Fairing for TimingFairing {
    fn info(&self) -> Info {
        Info {
            name: "Request Timing",
            kind: Kind::Request | Kind::Response,
        }
    }

    async fn on_request(&self, req: &mut Request<'_>, _: &mut rocket::Data<'_>) {
        req.local_cache(|| Instant::now());
    }

    async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) {
        let mulai = req.local_cache(|| Instant::now());
        let elapsed = mulai.elapsed();
        res.set_header(Header::new("X-Response-Time", format!("{:?}", elapsed)));
        println!("{} {}{} dalam {:?}", req.method(), req.uri(), res.status(), elapsed);
    }
}

// CORS Fairing
pub struct CorsFairing;

#[rocket::async_trait]
impl Fairing for CorsFairing {
    fn info(&self) -> Info {
        Info {
            name: "CORS",
            kind: Kind::Response,
        }
    }

    async fn on_response<'r>(&self, _: &'r Request<'_>, res: &mut Response<'r>) {
        res.set_header(Header::new("Access-Control-Allow-Origin", "*"));
        res.set_header(Header::new("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"));
        res.set_header(Header::new("Access-Control-Allow-Headers", "Content-Type, Authorization"));
    }
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(TimingFairing)
        .attach(CorsFairing)
        .mount("/", routes![index])
}

#[get("/")]
fn index() -> &'static str { "OK" }

Error Catcher #

use rocket::{catch, catchers, Request};
use rocket::serde::json::Json;
use serde::Serialize;

#[derive(Serialize)]
struct ErrorBody {
    kode: u16,
    pesan: &'static str,
}

#[catch(400)]
fn bad_request(req: &Request) -> Json<ErrorBody> {
    Json(ErrorBody { kode: 400, pesan: "Request tidak valid" })
}

#[catch(401)]
fn unauthorized(_: &Request) -> Json<ErrorBody> {
    Json(ErrorBody { kode: 401, pesan: "Autentikasi diperlukan" })
}

#[catch(403)]
fn forbidden(_: &Request) -> Json<ErrorBody> {
    Json(ErrorBody { kode: 403, pesan: "Akses ditolak" })
}

#[catch(404)]
fn not_found(req: &Request) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "kode": 404,
        "pesan": "Resource tidak ditemukan",
        "path": req.uri().to_string()
    }))
}

#[catch(422)]
fn unprocessable(req: &Request) -> Json<ErrorBody> {
    Json(ErrorBody { kode: 422, pesan: "Data tidak bisa diproses — cek format request" })
}

#[catch(500)]
fn server_error(_: &Request) -> Json<ErrorBody> {
    Json(ErrorBody { kode: 500, pesan: "Terjadi kesalahan server" })
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index])
        .register("/", catchers![
            bad_request, unauthorized, forbidden, not_found,
            unprocessable, server_error
        ])
}

#[get("/")]
fn index() -> &'static str { "OK" }

Testing #

#[cfg(test)]
mod tests {
    use super::*;
    use rocket::http::{ContentType, Status};
    use rocket::local::blocking::Client;

    fn buat_client() -> Client {
        Client::tracked(rocket()).expect("Rocket tidak valid")
    }

    #[test]
    fn test_index() {
        let client = buat_client();
        let resp = client.get("/").dispatch();

        assert_eq!(resp.status(), Status::Ok);
        assert_eq!(resp.into_string().unwrap(), "Selamat datang di Rocket!");
    }

    #[test]
    fn test_halo() {
        let client = buat_client();
        let resp = client.get("/halo/Budi").dispatch();

        assert_eq!(resp.status(), Status::Ok);
        assert!(resp.into_string().unwrap().contains("Budi"));
    }

    #[test]
    fn test_buat_produk_valid() {
        let client = buat_client();
        let resp = client
            .post("/produk")
            .header(ContentType::JSON)
            .body(r#"{"nama":"Laptop","harga":15000000,"stok":10,"kategori":"Elektronik"}"#)
            .dispatch();

        assert_eq!(resp.status(), Status::Created);
        let body: serde_json::Value = serde_json::from_str(&resp.into_string().unwrap()).unwrap();
        assert_eq!(body["nama"], "Laptop");
        assert_eq!(body["id"], 1001);
    }

    #[test]
    fn test_buat_produk_invalid_json() {
        let client = buat_client();
        let resp = client
            .post("/produk")
            .header(ContentType::JSON)
            .body("ini bukan json")
            .dispatch();

        assert_eq!(resp.status(), Status::BadRequest);
    }

    #[test]
    fn test_route_butuh_auth() {
        let client = buat_client();

        // Tanpa token
        let resp = client.get("/profil").dispatch();
        assert_eq!(resp.status(), Status::Unauthorized);

        // Dengan token valid
        let resp = client
            .get("/profil")
            .header(rocket::http::Header::new("Authorization", "Bearer valid-token-123"))
            .dispatch();
        assert_eq!(resp.status(), Status::Ok);
    }
}

Konfigurasi via Rocket.toml #

# Rocket.toml — konfigurasi per environment

[default]
address = "0.0.0.0"
port = 8080
workers = 4
log_level = "normal"
limits = { form = "64 kB", json = "1 MiB", file = "10 MiB" }

[debug]
port = 8000
log_level = "debug"

[release]
port = 8080
workers = 0  # 0 = jumlah CPU core
log_level = "critical"
secret_key = "production-secret-key-min-256-bits"
// Baca konfigurasi kustom dalam kode
use rocket::Config;
use serde::Deserialize;

#[derive(Deserialize)]
struct KonfigAplikasi {
    database_url: String,
    jwt_secret: String,
    redis_url: Option<String>,
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(rocket::fairing::AdHoc::config::<KonfigAplikasi>())
        .mount("/", routes![index])
}

#[get("/")]
fn index() -> &'static str { "OK" }

Ringkasan #

  • Validasi tipe otomatis — Rocket mengonversi dan memvalidasi path parameter secara otomatis. <id> dengan tipe u64 langsung mengembalikan 422 jika nilai bukan angka atau negatif, tanpa kode validasi manual.
  • Request Guard sebagai tipe — autentikasi dan otorisasi dikodekan sebagai tipe Rust (AuthToken, PenggunaLogin, HanyaAdmin). Jika guard gagal, handler tidak dipanggil sama sekali — tidak ada peluang lupa cek autentikasi.
  • #[derive(FromForm)] untuk validasi form — Rocket terintegrasi validasi form langsung di level tipe: len(3..=50), contains('@'), range(0..=150). Tidak perlu crate validasi terpisah.
  • #[catch(404)] untuk error handler global — tangkap semua error HTTP dengan catcher yang dikodekan per status code. Register ke instance dengan .register("/", catchers![...]).
  • Fairing sebagai middleware — hook ke lifecycle request/response. Kind::Request | Kind::Response untuk mengukur waktu; tambah header CORS di on_response.
  • managed state untuk shared data.manage(state) menyuntikkan state ke semua handler via &State<T>. State harus Send + Sync karena diakses dari banyak thread.
  • #[launch] menggantikan main() — Rocket mengkonfigurasi dan menjalankan server; tidak perlu menulis #[tokio::main] secara manual.
  • Testing dengan Client::trackedrocket::local::blocking::Client untuk testing synchronous; rocket::local::asynchronous::Client untuk testing async.
  • Rocket.toml untuk konfigurasi per environment[debug], [release], [default] dengan pengaturan port, workers, log level, dan limits.

← Sebelumnya: Actix   Berikutnya: Axum →

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