Rocket #

Rocket is the most beginner-friendly Rust web framework — its philosophy is convention over configuration, leveraging Rust’s type system for automatic request validation. If Actix-web prioritizes performance and control, Rocket prioritizes ergonomics and code clarity. Route parameters are extracted and validated automatically; if a type doesn’t match, Rocket immediately returns 422 with no boilerplate code. Request Guards are Rocket’s unique feature that lets authentication and validation be encoded as types — if a guard fails, the handler is never called at all. This article covers Rocket 0.5 (stable, natively async) thoroughly.

Installation #

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

A Basic Application #

#[macro_use]
extern crate rocket;

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

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

#[get("/halo/<nama>")]
fn halo(nama: &str) -> String {
    format!("Hello, {}!", 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])
}

Advanced Routing #

Path and Query Parameters #

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

// Path parameters with various types — automatically converted and validated
#[get("/pengguna/<id>")]
fn ambil_pengguna(id: u64) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "id": id,
        "nama": format!("Pengguna {}", id)
    }))
}

// Multiple path parameters
#[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 parameters
#[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 matches the most specific one
#[get("/pengguna/aktif")]  // more specific than <id>
fn pengguna_aktif() -> &'static str {
    "List of active users"
}

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

JSON Requests and Responses #

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

#[derive(Deserialize, Serialize, Debug)]
struct Produk {
    #[serde(skip_deserializing)]  // can't be sent from the 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 the ID from the 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": "Updated successfully"
    }))
}

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

Form Validation #

Rocket integrates form validation directly through types and 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!("Invalid email")))]
    email: String,

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

    #[field(validate = eq(self.password).or_else(msg!("Passwords don't match")))]
    konfirmasi_password: String,

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

#[post("/daftar", data = "<form>")]
fn daftar(form: Form<FormRegistrasi>) -> String {
    if !form.setuju_syarat {
        return "Must agree to the terms and conditions".to_string();
    }
    format!("Registration successful for: {}", form.nama)
}

// Form with optional fields
#[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!("Profile {} updated", form.nama)
}

Request Guards — Authentication as a Type #

Request Guards are Rocket’s most unique feature. A guard is a type implementing FromRequest — if the guard fails, the handler is never called:

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

// Guard: a valid token from the Authorization header
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, "Invalid token")),
            None    => Outcome::Error((Status::Unauthorized, "Missing token")),
        }
    }
}

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

// Guard: user info from the 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> {
        // Get the token first
        let token = req.guard::<AuthToken>().await
            .map_failure(|(s, _)| (s, "Invalid token"))?;

        // Get the user from the token (in production: from the DB or JWT payload)
        Outcome::Success(PenggunaLogin {
            id: 42,
            nama: String::from("Budi"),
            peran: String::from("admin"),
        })
    }
}

// Guard: admin only
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, "Only admins can access this"))
        }
    }
}

// A route that requires authentication
#[get("/profil")]
fn profil_saya(pengguna: PenggunaLogin) -> String {
    format!("Hello, {}! Your ID: {}", pengguna.nama, pengguna.id)
}

// A route that requires admin access
#[get("/admin/pengguna")]
fn daftar_semua_pengguna(admin: HanyaAdmin) -> String {
    format!("Admin {} is viewing all users", admin.0.nama)
}

// Public route — no guard
#[get("/publik")]
fn route_publik() -> &'static str {
    "Accessible to anyone"
}

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 #{}", 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])
}

Fairings — Rocket’s Middleware #

Fairings are Rocket’s middleware mechanism, attached to specific lifecycle stages (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!("{} {}{} in {:?}", 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 Catchers #

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: "Invalid request" })
}

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

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

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

#[catch(422)]
fn unprocessable(req: &Request) -> Json<ErrorBody> {
    Json(ErrorBody { kode: 422, pesan: "Data cannot be processed — check the request format" })
}

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

#[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("Invalid Rocket")
    }

    #[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(), "Welcome to 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("this is not json")
            .dispatch();

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

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

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

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

Configuration via Rocket.toml #

# Rocket.toml — per-environment configuration

[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 = number of CPU cores
log_level = "critical"
secret_key = "production-secret-key-min-256-bits"
// Read custom configuration in code
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" }

Summary #

  • Automatic type validation — Rocket converts and validates path parameters automatically. <id> with type u64 immediately returns 422 if the value isn’t a number or is negative, with no manual validation code.
  • Request Guards as types — authentication and authorization are encoded as Rust types (AuthToken, PenggunaLogin, HanyaAdmin). If a guard fails, the handler is never called at all — no chance to forget an auth check.
  • #[derive(FromForm)] for form validation — Rocket integrates form validation at the type level: len(3..=50), contains('@'), range(0..=150). No separate validation crate needed.
  • #[catch(404)] for global error handlers — catch all HTTP errors with catchers coded per status code. Register with the instance via .register("/", catchers![...]).
  • Fairings as middleware — hook into the request/response lifecycle. Kind::Request | Kind::Response for timing; add CORS headers in on_response.
  • Managed state for shared data.manage(state) injects state into all handlers via &State<T>. The state must be Send + Sync since it’s accessed from many threads.
  • #[launch] replaces main() — Rocket configures and runs the server; no need to write #[tokio::main] manually.
  • Testing with Client::trackedrocket::local::blocking::Client for synchronous testing; rocket::local::asynchronous::Client for async testing.
  • Rocket.toml for per-environment configuration[debug], [release], [default] with port, workers, log level, and limits settings.

← Previous: Actix   Next: Axum →

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