Rocket #

Rocket adalah web framework untuk Rust yang terkenal dengan ergonomi API, type-safety yang kuat, dan macro-based routing. Berbeda dari Axum atau Actix yang fleksibel tapi verbose, Rocket menyembunyikan banyak kompleksitas di balik macro — #[get], #[post], #[launch] — sehingga kode aplikasi terasa deklaratif dan mudah dibaca. Rocket juga punya sistem request guard yang sangat ekspresif untuk validasi dan authorization.

Artikel ini membahas setup Docker Compose untuk local development Rocket, dari Dockerfile multi-stage, hot reload dengan cargo-watch, integrasi database async, hingga best practice.

Prasyarat #

Pastikan sudah terinstall:

  • Docker dan Docker Compose versi terbaru
  • Rust toolchain (opsional, untuk development di host) — install via rustup
  • Git

Struktur project Rocket standar:

my-rocket-app/
├── src/
│   ├── main.rs
│   ├── lib.rs
│   ├── routes/
│   │   ├── mod.rs
│   │   ├── health.rs
│   │   └── users.rs
│   ├── models/
│   │   ├── mod.rs
│   │   └── user.rs
│   ├── repositories/
│   │   ├── mod.rs
│   │   └── user_repository.rs
│   ├── errors.rs
│   └── fairings.rs
├── migrations/
│   └── 001_create_users.sql
├── Rocket.toml
├── Cargo.toml
├── Cargo.lock
├── Dockerfile
├── Dockerfile.dev
├── docker-compose.yml
├── .env
├── .dockerignore
└── .cargo/
    └── config.toml

Rocket.toml adalah file konfigurasi khusus Rocket — analog dengan application.yml di Rails atau settings.py di Django. Formatnya [profile]-based, dengan profile debug, release, dan custom.

Catatan Penting tentang Rocket #

Rocket memiliki beberapa karakteristik unik:

Aspek Rocket 0.5+
Runtime Async (Tokio)
Async syntax Native async fn
Request guard Built-in via trait FromRequest
Response Built-in via trait Responder
Database rocket_db_pools atau sqlx
State management rocket::State<T>
Form handling rocket::form::Form<T>
JSON rocket::serde::json::Json<T>
Routing Macro-based (#[get], #[post])
Routing patterns /users/<id> (path param), /users/<id..> (segments)

Rocket 0.5+ sudah full async dengan Tokio. Versi 0.4 sebelumnya sync-only dan butuh rocket_contrib untuk async. Untuk proyek baru, selalu pakai 0.5+.

Dockerfile Multi-Stage #

# syntax=docker/dockerfile:1.6
FROM rust:1.75-slim AS builder

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    pkg-config \
    libssl-dev \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Dummy build untuk cache dependencies
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs \
    && cargo build --release \
    && rm -rf src target/release/deps/rocket_app*

# Real build
COPY . .
RUN cargo build --release

# Stage kedua: runtime ramping
FROM debian:12-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    libssl3 \
    curl \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

RUN addgroup -S app && adduser -S app -G app
USER app:app

COPY --from=builder /app/target/release/rocket-app /app/rocket-app
COPY --from=builder /app/Rocket.toml /app/Rocket.toml

EXPOSE 8000

ENTRYPOINT ["/app/rocket-app"]

Perhatikan: Rocket.toml di-copy ke runtime image agar konfigurasi bisa di-load saat startup. Konfigurasi bisa di-override via environment variable dengan prefix ROCKET_.

Dockerfile untuk Development #

# Dockerfile.dev
FROM rust:1.75-slim

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    pkg-config \
    libssl-dev \
    ca-certificates \
    git \
    curl \
    && rm -rf /var/lib/apt/lists/*

# Install cargo-watch
RUN cargo install cargo-watch

# Cache dependencies
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs \
    && cargo fetch

EXPOSE 8000

CMD ["cargo", "watch", "-x", "run"]

Rocket.toml Configuration #

Rocket.toml:

[default]
address = "0.0.0.0"
port = 8000
log_level = "normal"
workers = 2
ident = "my-rocket-app"
cli_colors = false

[default.databases]
postgres = { url = "postgres://app:dev@db:5432/rocketapp" }

[default.redis]
url = "redis://cache:6379/0"

[debug]
log_level = "normal"

[release]
log_level = "critical"

Profile default adalah fallback. Profile debug aktif saat cargo run, release saat cargo run --release. Environment variable ROCKET_PROFILE bisa override.

Konfigurasi di-override via environment variable dengan prefix ROCKET_:

ROCKET_ADDRESS=0.0.0.0
ROCKET_PORT=8000
ROCKET_LOG_LEVEL=normal
ROCKET_DATABASES__POSTGRES__URL=postgres://...

docker-compose.yml #

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: rocket-app:dev
    container_name: rocket-api
    command: cargo watch -x run
    volumes:
      - ./src:/app/src
      - ./Cargo.toml:/app/Cargo.toml
      - ./Cargo.lock:/app/Cargo.lock
      - ./Rocket.toml:/app/Rocket.toml
      - cargo-registry:/usr/local/cargo/registry
      - cargo-target:/app/target
    ports:
      - "8000:8000"
    environment:
      - ROCKET_ADDRESS=0.0.0.0
      - ROCKET_PORT=8000
      - ROCKET_LOG_LEVEL=normal
      - ROCKET_PROFILE=debug
      - ROCKET_DATABASES__POSTGRES__URL=postgres://app:dev@db:5432/rocketapp
      - ROCKET_REDIS__URL=redis://cache:6379/0
      - JWT_SECRET=local-dev-secret-change-me
      - RUST_LOG=debug
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    container_name: rocket-db
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=dev
      - POSTGRES_DB=rocketapp
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./db/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d rocketapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    ports:
      - "5432:5432"

  cache:
    image: redis:7-alpine
    container_name: rocket-cache
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    volumes:
      - cache-data:/data
    ports:
      - "6379:6379"

volumes:
  db-data:
  cache-data:
  cargo-registry:
  cargo-target:

Contoh Aplikasi Rocket #

Cargo.toml:

[package]
name = "rocket-app"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = { version = "0.5", features = ["json", "uuid", "secrets"] }
rocket_db_pools = { version = "0.2", features = ["sqlx_postgres"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "macros"] }

tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }

serde = { version = "1", features = ["derive"] }
serde_json = "1"

uuid = { version = "1", features = ["serde", "v4"] }
chrono = { version = "0.4", features = ["serde"] }

tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

thiserror = "1"
anyhow = "1"

bcrypt = "0.15"
jsonwebtoken = "9"

src/main.rs:

#[macro_use]
extern crate rocket;

mod errors;
mod models;
mod repositories;
mod routes;

use rocket::fairing::AdHoc;
use rocket_db_pools::Database;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

use crate::errors::AppError;

#[derive(Database)]
#[database("postgres")]
pub struct PgPool(sqlx::PgPool);

#[launch]
async fn rocket() -> _ {
    // Logging
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "rocket=info,rocket_app=debug,info".into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    rocket::build()
        .attach(PgPool::init())
        .attach(AdHoc::try_on_ignite("DB Migrations", |rocket| async move {
            let pool = PgPool::get_one(&rocket)
                .await
                .expect("database pool");
            match sqlx::migrate!("./migrations").run(pool.inner()).await {
                Ok(_) => tracing::info!("migrations applied"),
                Err(e) => tracing::error!("migration failed: {}", e),
            }
            Ok(rocket)
        }))
        .mount("/", routes![routes::health::healthz, routes::health::readyz])
        .mount("/api/v1/users", routes::routes::routes())
        .register("/", catchers![not_found, internal_error])
}

#[catch(404)]
fn not_found() -> rocket::serde::json::Value {
    rocket::serde::json::json!({ "error": "not found" })
}

#[catch(500)]
fn internal_error() -> rocket::serde::json::Value {
    rocket::serde::json::json!({ "error": "internal server error" })
}

src/errors.rs:

use rocket::http::Status;
use rocket::response::{self, Responder, Response};
use rocket::serde::json::Json;
use rocket::Request;
use serde_json::{json, Value};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum AppError {
    #[error("not found")]
    NotFound,

    #[error("validation error: {0}")]
    Validation(String),

    #[error("unauthorized")]
    Unauthorized,

    #[error("conflict: {0}")]
    Conflict(String),

    #[error("database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("bcrypt error: {0}")]
    Bcrypt(#[from] bcrypt::BcryptError),

    #[error("jwt error: {0}")]
    Jwt(#[from] jsonwebtoken::errors::Error),

    #[error("internal error: {0}")]
    Internal(#[from] anyhow::Error),
}

impl<'r> Responder<'r, 'static> for AppError {
    fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'static> {
        let (status, message) = match &self {
            AppError::NotFound => (Status::NotFound, "not found".to_string()),
            AppError::Validation(msg) => (Status::UnprocessableEntity, msg.clone()),
            AppError::Unauthorized => (Status::Unauthorized, "unauthorized".to_string()),
            AppError::Conflict(msg) => (Status::Conflict, msg.clone()),
            _ => {
                tracing::error!(error = ?self, "internal error");
                (Status::InternalServerError, "internal server error".to_string())
            }
        };

        let body = json!({ "error": message }).to_string();
        Response::build()
            .status(status)
            .header(rocket::http::ContentType::JSON)
            .sized_body(body.len(), std::io::Cursor::new(body))
            .ok()
    }
}

pub type AppResult<T> = Result<T, AppError>;

src/routes/mod.rs:

pub mod health;
pub mod users;

pub fn routes() -> Vec<rocket::Route> {
    routes![users::create_user, users::get_user, users::list_users, users::update_user, users::delete_user]
}

src/routes/health.rs:

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

use crate::PgPool;

#[get("/healthz")]
pub fn healthz() -> Json<Value> {
    Json(json!({"status": "ok"}))
}

#[get("/readyz")]
pub async fn readyz(pool: &State<PgPool>) -> (rocket::http::Status, Json<Value>) {
    let pool = pool.inner();
    let db_ok = sqlx::query("SELECT 1").execute(pool).await.is_ok();
    let status = if db_ok {
        rocket::http::Status::OK
    } else {
        rocket::http::Status::ServiceUnavailable
    };
    (status, Json(json!({
        "status": if db_ok { "ready" } else { "degraded" },
        "database": if db_ok { "up" } else { "down" },
    })))
}

src/routes/users.rs:

use rocket::serde::json::Json;
use rocket::{delete, get, post, put, State};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::errors::{AppError, AppResult};
use crate::PgPool;

#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
    pub email: String,
    pub name: String,
    pub password: String,
}

#[derive(Debug, Serialize)]
pub struct UserResponse {
    pub id: Uuid,
    pub email: String,
    pub name: String,
    pub is_verified: bool,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Deserialize)]
pub struct UpdateUserRequest {
    pub name: Option<String>,
    pub is_verified: Option<bool>,
}

#[derive(Debug, Deserialize)]
pub struct ListQuery {
    pub limit: Option<i64>,
    pub offset: Option<i64>,
}

#[post("/", format = "json", data = "<body>")]
pub async fn create_user(
    pool: &State<PgPool>,
    body: Json<CreateUserRequest>,
) -> AppResult<(rocket::http::Status, Json<UserResponse>)> {
    if body.password.len() < 8 {
        return Err(AppError::Validation("password too short".into()));
    }

    let hash = bcrypt::hash(&body.password, bcrypt::DEFAULT_COST)?;
    let row = sqlx::query!(
        r#"
        INSERT INTO users (email, name, password_hash)
        VALUES ($1, $2, $3)
        RETURNING id, email, name, is_verified, created_at
        "#,
        body.email.to_lowercase(),
        body.name,
        hash,
    )
    .fetch_one(pool.inner())
    .await
    .map_err(|e| match e {
        sqlx::Error::Database(db_err) if db_err.constraint() == Some("users_email_key") => {
            AppError::Conflict("email already registered".into())
        }
        other => AppError::Database(other),
    })?;

    Ok((rocket::http::Status::Created, Json(UserResponse {
        id: row.id,
        email: row.email,
        name: row.name,
        is_verified: row.is_verified,
        created_at: row.created_at,
    })))
}

#[get("/<id>", rank = 1)]
pub async fn get_user(pool: &State<PgPool>, id: &str) -> AppResult<Json<UserResponse>> {
    let uuid = Uuid::parse_str(id).map_err(|_| AppError::Validation("invalid id".into()))?;
    let row = sqlx::query!(
        r#"SELECT id, email, name, is_verified, created_at FROM users WHERE id = $1"#,
        uuid,
    )
    .fetch_optional(pool.inner())
    .await?;

    match row {
        Some(r) => Ok(Json(UserResponse {
            id: r.id,
            email: r.email,
            name: r.name,
            is_verified: r.is_verified,
            created_at: r.created_at,
        })),
        None => Err(AppError::NotFound),
    }
}

#[get("/?<limit>&<offset>")]
pub async fn list_users(
    pool: &State<PgPool>,
    limit: Option<i64>,
    offset: Option<i64>,
) -> AppResult<Json<serde_json::Value>> {
    let limit = limit.unwrap_or(20).min(100);
    let offset = offset.unwrap_or(0);
    let rows = sqlx::query!(
        r#"SELECT id, email, name, is_verified, created_at
           FROM users
           ORDER BY created_at DESC
           LIMIT $1 OFFSET $2"#,
        limit,
        offset,
    )
    .fetch_all(pool.inner())
    .await?;

    let data: Vec<UserResponse> = rows
        .into_iter()
        .map(|r| UserResponse {
            id: r.id,
            email: r.email,
            name: r.name,
            is_verified: r.is_verified,
            created_at: r.created_at,
        })
        .collect();

    Ok(Json(serde_json::json!({
        "data": data,
        "limit": limit,
        "offset": offset,
    })))
}

#[put("/<id>", format = "json", data = "<body>")]
pub async fn update_user(
    pool: &State<PgPool>,
    id: &str,
    body: Json<UpdateUserRequest>,
) -> AppResult<Json<UserResponse>> {
    let uuid = Uuid::parse_str(id).map_err(|_| AppError::Validation("invalid id".into()))?;
    let row = sqlx::query!(
        r#"
        UPDATE users
        SET name = COALESCE($2, name),
            is_verified = COALESCE($3, is_verified),
            updated_at = NOW()
        WHERE id = $1
        RETURNING id, email, name, is_verified, created_at
        "#,
        uuid,
        body.name,
        body.is_verified,
    )
    .fetch_optional(pool.inner())
    .await?;

    match row {
        Some(r) => Ok(Json(UserResponse {
            id: r.id,
            email: r.email,
            name: r.name,
            is_verified: r.is_verified,
            created_at: r.created_at,
        })),
        None => Err(AppError::NotFound),
    }
}

#[delete("/<id>")]
pub async fn delete_user(pool: &State<PgPool>, id: &str) -> AppResult<rocket::http::Status> {
    let uuid = Uuid::parse_str(id).map_err(|_| AppError::Validation("invalid id".into()))?;
    let result = sqlx::query!("DELETE FROM users WHERE id = $1", uuid)
        .execute(pool.inner())
        .await?;

    if result.rows_affected() > 0 {
        Ok(rocket::http::Status::NoContent)
    } else {
        Err(AppError::NotFound)
    }
}

Migration #

migrations/20240101000000_create_users.sql:

CREATE TABLE IF NOT EXISTS users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(255) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    is_verified BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_users_created_at ON users(created_at);

Rocket Fairing #

Fairing adalah middleware di Rocket — analog dengan Tower middleware di Axum. Pakai fairing untuk cross-cutting concern: logging, CORS, request ID, rate limit.

src/fairings.rs:

use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Header;
use rocket::{Data, Request, Response};

pub struct CorsFairing;

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

    async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
        // CORS preflight
        if request.method() == rocket::http::Method::Options {
            request.set_method(rocket::http::Method::Get);
        }
    }

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

        if request.method() == rocket::http::Method::Options {
            response.set_status(rocket::http::Status::NoContent);
        }
    }
}

pub struct RequestLogger;

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

    async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
        tracing::info!(
            method = %request.method(),
            uri = %request.uri(),
            "incoming request"
        );
    }
}

Daftarkan di main.rs:

use crate::fairings::{CorsFairing, RequestLogger};

rocket::build()
    .attach(CorsFairing)
    .attach(RequestLogger)
    // ... route mounting

Request Guard untuk Authentication #

Rocket punya sistem request guard yang elegant. Implement FromRequest untuk custom guard.

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

pub struct AuthUser {
    pub id: Uuid,
    pub email: String,
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthUser {
    type Error = ();

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

        let token = match token {
            Some(t) => t,
            None => return Outcome::Error((Status::Unauthorized, ())),
        };

        let payload = match crate::services::jwt::decode(token) {
            Some(p) => p,
            None => return Outcome::Error((Status::Unauthorized, ())),
        };

        Outcome::Success(AuthUser {
            id: Uuid::parse_str(&payload["user_id"]).unwrap(),
            email: payload["email"].clone(),
        })
    }
}

// Pakai di handler
#[get("/me")]
pub async fn me(user: AuthUser) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "id": user.id,
        "email": user.email,
    }))
}

Request guard dipanggil otomatis oleh Rocket saat handler di-invoke. Return Outcome::Error untuk reject request.

Form Handling #

Rocket punya form handling yang aman dari CSRF dan type-safe.

use rocket::form::Form;
use serde::Deserialize;

#[derive(FromForm)]
pub struct LoginForm {
    pub email: String,
    pub password: String,
    #[field(validate = len(1..))]
    pub csrf_token: String,
}

#[post("/login", data = "<form>")]
pub async fn login(form: Form<LoginForm>) -> AppResult<Json<UserResponse>> {
    // form.email, form.password, form.csrf_token
    // Validasi otomatis
    todo!()
}

Build dan Run #

# Build
docker compose build

# Jalankan
docker compose up -d

# Lihat log
docker compose logs -f api

# Stop
docker compose down

Akses:

  • API: http://localhost:8000
  • Health: http://localhost:8000/healthz
  • Ready: http://localhost:8000/readyz
  • PostgreSQL: localhost:5432
  • Redis: localhost:6379

Test:

# Create user
curl -X POST http://localhost:8000/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","name":"Ina","password":"password123"}'

# Get user
curl http://localhost:8000/api/v1/users/<uuid>

# List users
curl "http://localhost:8000/api/v1/users?limit=10&offset=0"

Kapan Rocket Tepat #

Pakai Rocket jika:
  ✓ Suka macro-based routing yang deklaratif
  ✓ Butuh request guard untuk authorization
  ✓ Aplikasi dengan form handling tradisional
  ✓ Microservice dengan type-safety tinggi
  ✓ Tim Rust yang suka API ergonomis

Hindari Rocket jika:
  ✗ Butuh kompatibilitas dengan tower middleware (pakai Axum)
  ✗ Butuh library ecosystem Actix (pakai Actix Web)
  ✗ Microservice super minimal (Axum lebih simpel)
  ✗ Butuh kontrol rendah (warp/actix lebih flexible)
  ✗ Compile time menjadi blocker (Rocket macros bisa perlambat build)

Rocket adalah pilihan tepat untuk service yang mengutamakan developer experience. Untuk service yang butuh interoperabilitas luas, Axum lebih cocok. Untuk performa tertinggi, Actix.

Best Practice #

Cache Cargo dengan Volume #

Rust compile time berat. Volume cargo-target dan cargo-registry wajib.

Dummy Build untuk Cache Layer #

Pattern mkdir src && echo "fn main() {}" > src/main.rs && cargo build cache layer dependencies.

Pakai cargo-watch #

cargo watch -x run memantau file .rs dan rebuild + restart. DX yang sangat dibutuhkan Rust.

Pakai Rocket.toml untuk Konfigurasi #

Jangan hardcode konfigurasi di kode. Pakai Rocket.toml dengan profile default/debug/release. Override via environment variable ROCKET_*.

rocket_db_pools untuk Database Pool #

Crate rocket_db_pools integrasi dengan sqlx dan diesel. Pakai #[derive(Database)] dan #[database("postgres")] untuk deklaratif setup.

AppError dengan Responder #

Implement Responder trait untuk custom error type. Pattern yang sama dengan ResponseError di Actix.

Request Guard untuk Auth #

Implement FromRequest untuk custom guard. Lebih ergonomis dari middleware manual.

Macro routes! untuk Collecting Routes #

Pakai routes![handler1, handler2, ...] untuk collect multiple routes. Mount dengan .mount("/path", routes!).

Catchers untuk Error Page #

Implement catcher untuk 404, 422, 500. Return JSON yang konsisten. Register dengan .register("/", catchers![...]).

Pakai sqlx::query! untuk Compile-time Check #

sqlx::query! validasi query saat compile. Butuh DATABASE_URL di build, atau cargo sqlx prepare untuk offline cache.

Bind ke 0.0.0.0 #

Set ROCKET_ADDRESS=0.0.0.0 atau di Rocket.toml. Default Rocket bind ke localhost saja.

Troubleshooting #

“Rocket” Not Found #

Pastikan #[macro_use] extern crate rocket; di main.rs. Atau pakai use rocket::*;.

Database Connection Refused #

Pastikan ROCKET_DATABASES__POSTGRES__URL benar. Format env var di Rocket pakai double underscore __ sebagai separator nested.

Cargo Build Sangat Lambat #

Pastikan volume cargo-target di-mount. Cek apakah cargo fetch cache jalan.

Hot Reload Tidak Detect #

Pastikan cargo-watch terinstall. Naikkan delay di konfigurasi.

Port 8000 Sudah Dipakai #

lsof -i :8000

Ubah ROCKET_PORT atau port mapping di Compose.

SQLx “No such table” saat Compile #

Set DATABASE_URL saat compile, atau generate offline cache dengan cargo sqlx prepare.

Ringkasan #

  • Rocket ideal untuk service Rust yang mengutamakan developer experience dan type-safety.
  • Rocket 0.5+ full async dengan Tokio. Versi 0.4 lama sync-only — selalu pakai 0.5+ untuk proyek baru.
  • Macro-based routing: #[get], #[post], #[put], #[delete] — deklaratif dan ergonomis. Lebih mudah dibaca dari Axum/Actix.
  • Request guard untuk authorization: implement FromRequest trait, otomatis di-invoke oleh Rocket.
  • Catcher untuk custom error page: #[catch(404)], #[catch(500)]. Return JSON konsisten.
  • Form handling type-safe dengan FromForm derive macro. Validasi built-in.
  • Dockerfile multi-stage untuk production: dummy build cache dependency, build asli untuk binary. Image akhir < 80 MB.
  • Dockerfile.dev untuk development: full toolchain + cargo-watch.
  • Volume wajib untuk cargo-registry dan cargo-target — Rust compile time berat.
  • Rocket.toml untuk konfigurasi. Profile default/debug/release. Override via ROCKET_* env var.
  • rocket_db_pools untuk integrasi database dengan sqlx/diesel. #[derive(Database)] + #[database("name")].
  • AppError dengan Responder: enum yang implement rocket::Responder, plus From trait untuk auto-convert.
  • Bind ke 0.0.0.0 via Rocket.toml atau ROCKET_ADDRESS env var. Default localhost.
  • Best practice: cache Cargo, dummy build, cargo-watch, Rocket.toml, rocket_db_pools, AppError, request guard, catcher.
  • Pakai Rocket untuk service yang mengutamakan DX. Hindari untuk service yang butuh kompatibilitas tower middleware (pakai Axum).
  • Alternatif: Axum untuk idiomatic Tokio + tower, Actix Web untuk performa tertinggi + library ecosystem.

← Sebelumnya: Actix   Berikutnya: Express.js →

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