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
FromRequesttrait, otomatis di-invoke oleh Rocket.- Catcher untuk custom error page:
#[catch(404)],#[catch(500)]. Return JSON konsisten.- Form handling type-safe dengan
FromFormderive 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-registrydancargo-target— Rust compile time berat.- Rocket.toml untuk konfigurasi. Profile
default/debug/release. Override viaROCKET_*env var.- rocket_db_pools untuk integrasi database dengan
sqlx/diesel.#[derive(Database)]+#[database("name")].- AppError dengan Responder: enum yang implement
rocket::Responder, plusFromtrait untuk auto-convert.- Bind ke
0.0.0.0viaRocket.tomlatauROCKET_ADDRESSenv 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.