Axum #

Axum adalah web framework untuk Rust yang dibangun di atas Tokio dan Tower. Dengan fokus pada type-safety, composability, dan performa, Axum menjadi pilihan utama untuk microservice dan API high-performance di ekosistem Rust modern. Berbeda dari Actix Web yang punya runtime sendiri, Axum sepenuhnya menggunakan Tokio — menjadikannya interoperable dengan library async Rust apapun.

Docker Compose melengkapi Axum dengan cara yang signifikan karena Rust terkenal dengan compile time yang berat dan dependency native yang sering bentrok. Artikel ini membahas setup Docker Compose untuk local development Axum, dari Dockerfile multi-stage, hot reload dengan cargo-watch, hingga integrasi database async dan 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 Axum standar:

my-axum-app/
├── src/
│   ├── main.rs
│   ├── lib.rs              # opsional, untuk library crate
│   ├── routes/
│   │   ├── mod.rs
│   │   ├── health.rs
│   │   └── users.rs
│   ├── handlers/
│   │   ├── mod.rs
│   │   └── user.rs
│   ├── models/
│   │   ├── mod.rs
│   │   └── user.rs
│   ├── db/
│   │   ├── mod.rs
│   │   └── pool.rs
│   └── error.rs
├── Cargo.toml
├── Cargo.lock
├── Dockerfile
├── Dockerfile.dev
├── docker-compose.yml
├── .env
├── .dockerignore
└── .cargo/
    └── config.toml          # konfigurasi cargo untuk Docker

Pemisahan ke routes/, handlers/, models/, db/ adalah pola yang umum untuk proyek Axum skala menengah. Untuk service kecil, struktur flat di main.rs saja cukup.

Dockerfile Multi-Stage #

Rust compile time terkenal berat. Multi-stage build memisahkan builder (butuh toolchain) dari runtime (cukup binary).

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

WORKDIR /app

# Install build dependencies
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/axum_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

# Non-root user
RUN addgroup -S app && adduser -S app -G app
USER app:app

COPY --from=builder /app/target/release/axum-app /app/axum-app

EXPOSE 3000

ENTRYPOINT ["/app/axum-app"]

Pattern dummy build adalah trik penting untuk Rust: build binary kosong dulu, sehingga semua dependency di-compile. Lalu replace source code asli dan build lagi — bagian dependency tidak perlu re-compile, hanya kode aplikasi.

Image akhir berbasis debian:12-slim (~80 MB) atau bahkan scratch (~20 MB) untuk image paling ramping.

Dockerfile untuk Development #

Untuk development, single-stage dengan cargo-watch.

# 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

# Source code di-mount via volume

EXPOSE 3000

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

.cargo/config.toml untuk Optimasi Build #

File .cargo/config.toml untuk tuning compiler di dalam container:

# .cargo/config.toml
[net]
git-fetch-with-cli = true

[build]
# Pakai mold untuk link lebih cepat (jika tersedia)
# rustc-wrapper = "sccache"
# Pakai lebih banyak jobs paralel
jobs = 4

Atau pakai sccache untuk cache compile artifact antar build:

[build]
rustc-wrapper = "/usr/local/bin/sccache"

sccache menyimpan hasil compile di disk, sehingga re-build jauh lebih cepat.

docker-compose.yml #

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: axum-app:dev
    container_name: axum-api
    command: cargo watch -x run
    volumes:
      - ./src:/app/src
      - ./Cargo.toml:/app/Cargo.toml
      - ./Cargo.lock:/app/Cargo.lock
      - cargo-registry:/usr/local/cargo/registry
      - cargo-target:/app/target
    ports:
      - "3000:3000"
    environment:
      - RUST_LOG=debug
      - DATABASE_URL=postgres://app:dev@db:5432/axumapp
      - REDIS_URL=redis://cache:6379/0
      - JWT_SECRET=local-dev-secret-change-me
      - SERVER_PORT=3000
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy

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

  cache:
    image: redis:7-alpine
    container_name: axum-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:

Volume cargo-registry dan cargo-target sangat penting — Rust compile time berat, dan tanpa cache volume, setiap restart container akan re-download dan re-compile semua dependency. Dengan volume, container pertama build lambat, tapi container berikutnya hanya butuh beberapa detik.

Contoh Aplikasi Axum #

Cargo.toml:

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

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["trace", "cors", "request-id", "util"] }
hyper = "1"

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

sqlx = { version = "0.7", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "macros"] }

redis = { version = "0.25", features = ["tokio-comp", "connection-manager"] }

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"

[dev-dependencies]
reqwest = { version = "0.12", features = ["json"] }

src/main.rs:

use axum::{routing::{get, post}, Router};
use std::net::SocketAddr;
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

mod error;
mod handlers;
mod models;
mod routes;

use error::AppError;
use routes::{health, users};

#[derive(Clone)]
pub struct AppState {
    pub db: sqlx::PgPool,
    pub redis: redis::aio::ConnectionManager,
    pub jwt_secret: String,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Logging
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "axum_app=debug,tower_http=debug,info".into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    // Load environment
    let database_url = std::env::var("DATABASE_URL")?;
    let redis_url = std::env::var("REDIS_URL")?;
    let jwt_secret = std::env::var("JWT_SECRET")?;
    let port: u16 = std::env::var("SERVER_PORT")
        .unwrap_or_else(|_| "3000".into())
        .parse()?;

    // Database pool dengan retry
    let db = sqlx::postgres::PgPoolOptions::new()
        .max_connections(10)
        .acquire_timeout(std::time::Duration::from_secs(5))
        .connect(&database_url)
        .await?;

    // Run migrations
    sqlx::migrate!("./migrations").run(&db).await?;
    tracing::info!("database migrations applied");

    // Redis connection manager
    let redis_client = redis::Client::open(redis_url)?;
    let redis = redis::aio::ConnectionManager::new(redis_client).await?;
    tracing::info!("redis connected");

    // State
    let state = AppState { db, redis, jwt_secret };

    // Router
    let app = Router::new()
        .route("/health", get(health::healthz))
        .route("/readyz", get(health::readyz))
        .nest("/api/v1/users", users::router())
        .layer(TraceLayer::new_for_http())
        .layer(CorsLayer::permissive())
        .with_state(state);

    // Server
    let addr = SocketAddr::from(([0, 0, 0, 0], port));
    tracing::info!("listening on {}", addr);
    let listener = tokio::net::TcpListener::bind(addr).await?;
    axum::serve(listener, app)
        .with_graceful_shutdown(shutdown_signal())
        .await?;

    Ok(())
}

async fn shutdown_signal() {
    let ctrl_c = async {
        tokio::signal::ctrl_c()
            .await
            .expect("failed to install Ctrl+C handler");
    };

    #[cfg(unix)]
    let terminate = async {
        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
            .expect("failed to install signal handler")
            .recv()
            .await;
    };

    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        _ = ctrl_c => {},
        _ = terminate => {},
    }

    tracing::info!("shutdown signal received");
}

src/error.rs:

use axum::{http::StatusCode, response::IntoResponse, Json};
use serde_json::json;
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("redis error: {0}")]
    Redis(#[from] redis::RedisError),

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

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

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

impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        let (status, message) = match &self {
            AppError::NotFound => (StatusCode::NOT_FOUND, "not found".to_string()),
            AppError::Validation(msg) => (StatusCode::UNPROCESSABLE_ENTITY, msg.clone()),
            AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized".to_string()),
            AppError::Conflict(msg) => (StatusCode::CONFLICT, msg.clone()),
            _ => (StatusCode::INTERNAL_SERVER_ERROR, "internal server error".to_string()),
        };

        if status == StatusCode::INTERNAL_SERVER_ERROR {
            tracing::error!(error = ?self, "internal error");
        }

        (status, Json(json!({ "error": message }))).into_response()
    }
}

src/routes/mod.rs:

pub mod health;
pub mod users;

src/routes/health.rs:

use axum::{extract::State, http::StatusCode, Json};
use serde_json::{json, Value};

use crate::AppState;

pub async fn healthz() -> (StatusCode, Json<Value>) {
    (StatusCode::OK, Json(json!({"status": "ok"})))
}

pub async fn readyz(State(state): State<AppState>) -> (StatusCode, Json<Value>) {
    let db_ok = sqlx::query("SELECT 1").execute(&state.db).await.is_ok();
    let redis_ok = {
        let mut conn = state.redis.clone();
        redis::cmd("PING").query_async::<_, String>(&mut conn).await.is_ok()
    };

    if db_ok && redis_ok {
        (StatusCode::OK, Json(json!({"status": "ready", "database": "up", "cache": "up"})))
    } else {
        (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(json!({"status": "degraded", "database": db_ok, "cache": redis_ok})),
        )
    }
}

src/routes/users.rs:

use axum::{
    extract::{Path, State},
    http::StatusCode,
    routing::{get, post},
    Json, Router,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::{error::AppError, handlers::user, AppState};

pub fn router() -> Router<AppState> {
    Router::new()
        .route("/", post(create_user).get(list_users))
        .route("/:id", get(get_user).put(update_user).delete(delete_user))
        .route("/me", get(get_me))
}

#[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>,
}

async fn create_user(
    State(state): State<AppState>,
    Json(req): Json<CreateUserRequest>,
) -> Result<(StatusCode, Json<UserResponse>), AppError> {
    let u = user::create(&state, req.email, req.name, req.password).await?;
    Ok((StatusCode::CREATED, Json(u)))
}

async fn get_user(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<Json<UserResponse>, AppError> {
    let u = user::find_by_id(&state, id).await?;
    u.map(Json).ok_or(AppError::NotFound)
}

async fn list_users(State(state): State<AppState>) -> Result<Json<Vec<UserResponse>>, AppError> {
    let users = user::list(&state).await?;
    Ok(Json(users))
}

async fn update_user(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    Json(req): Json<UpdateUserRequest>,
) -> Result<Json<UserResponse>, AppError> {
    let u = user::update(&state, id, req).await?;
    u.map(Json).ok_or(AppError::NotFound)
}

async fn delete_user(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<StatusCode, AppError> {
    user::delete(&state, id).await?;
    Ok(StatusCode::NO_CONTENT)
}

async fn get_me(State(state): State<AppState>) -> Result<Json<UserResponse>, AppError> {
    // Implement auth middleware dulu
    Err(AppError::Unauthorized)
}

src/handlers/user.rs:

use uuid::Uuid;

use crate::{
    error::AppError,
    models::User,
    routes::users::{CreateUserRequest, UpdateUserRequest, UserResponse},
    AppState,
};

pub async fn create(
    state: &AppState,
    email: String,
    name: String,
    password: String,
) -> Result<UserResponse, AppError> {
    // Validasi
    if password.len() < 8 {
        return Err(AppError::Validation("password too short".into()));
    }
    if !email.contains('@') {
        return Err(AppError::Validation("invalid email".into()));
    }

    // Hash password
    let hash = bcrypt::hash(&password, bcrypt::DEFAULT_COST)?;

    // Insert
    let user = sqlx::query_as!(
        User,
        r#"
        INSERT INTO users (email, name, password_hash)
        VALUES ($1, $2, $3)
        RETURNING id, email, name, is_verified, created_at, updated_at
        "#,
        email.to_lowercase(),
        name,
        hash,
    )
    .fetch_one(&state.db)
    .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(to_response(&user))
}

pub async fn find_by_id(state: &AppState, id: Uuid) -> Result<Option<UserResponse>, AppError> {
    let user = sqlx::query_as!(
        User,
        r#"SELECT id, email, name, is_verified, created_at, updated_at FROM users WHERE id = $1"#,
        id,
    )
    .fetch_optional(&state.db)
    .await?;

    Ok(user.map(|u| to_response(&u)))
}

pub async fn list(state: &AppState) -> Result<Vec<UserResponse>, AppError> {
    let users = sqlx::query_as!(
        User,
        r#"SELECT id, email, name, is_verified, created_at, updated_at
           FROM users
           ORDER BY created_at DESC
           LIMIT 100"#
    )
    .fetch_all(&state.db)
    .await?;

    Ok(users.iter().map(to_response).collect())
}

pub async fn update(
    state: &AppState,
    id: Uuid,
    req: UpdateUserRequest,
) -> Result<Option<UserResponse>, AppError> {
    let user = sqlx::query_as!(
        User,
        r#"
        UPDATE users
        SET name = COALESCE($2, name),
            is_verified = COALESCE($3, is_verified)
        WHERE id = $1
        RETURNING id, email, name, is_verified, created_at, updated_at
        "#,
        id,
        req.name,
        req.is_verified,
    )
    .fetch_optional(&state.db)
    .await?;

    Ok(user.map(|u| to_response(&u)))
}

pub async fn delete(state: &AppState, id: Uuid) -> Result<bool, AppError> {
    let result = sqlx::query!("DELETE FROM users WHERE id = $1", id)
        .execute(&state.db)
        .await?;
    Ok(result.rows_affected() > 0)
}

fn to_response(user: &User) -> UserResponse {
    UserResponse {
        id: user.id,
        email: user.email.clone(),
        name: user.name.clone(),
        is_verified: user.is_verified,
        created_at: user.created_at,
    }
}

src/models/user.rs:

use uuid::Uuid;

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

Migration dengan SQLx #

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);

sqlx::migrate!("./migrations") di main.rs otomatis apply migration saat startup. Untuk generate migration baru, install sqlx-cli:

# Di host (opsional)
cargo install sqlx-cli

# Generate migration
sqlx migrate add create_users

Middleware: Trace, CORS, Request ID #

use tower_http::{cors::CorsLayer, request_id::MakeRequestUuid, trace::TraceLayer};
use axum::Router;

let app = Router::new()
    .route("/", get(handler))
    .layer(TraceLayer::new_for_http())
    .layer(CorsLayer::permissive())
    .layer(
        tower_http::request_id::SetRequestIdLayer::new(
            tower_http::request_id::HeaderName::from_static("x-request-id"),
            MakeRequestUuid,
        ),
    );

tower-http adalah library yang menyediakan middleware siap pakai untuk Axum. CorsLayer, TraceLayer, RequestId, Compression, Timeout — semua tinggal layer().

Build dan Run #

# Build
docker compose build

# Jalankan
docker compose up -d

# Lihat log
docker compose logs -f api

# Lihat hanya log error
docker compose logs -f api | grep ERROR

# Shell ke container
docker compose exec api sh

# Stop
docker compose down

Akses:

  • API: http://localhost:3000
  • Health: http://localhost:3000/health
  • Ready: http://localhost:3000/readyz
  • PostgreSQL: localhost:5432
  • Redis: localhost:6379

Test:

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

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

# List users
curl http://localhost:3000/api/v1/users

Kapan Axum Tepat #

Pakai Axum jika:
  ✓ Microservice high-performance
  ✓ Butuh type-safety end-to-end
  ✓ Aplikasi async dengan banyak concurrent request
  ✓ Tim Rust yang ingin framework idiomatic
  ✓ Butuh interoperabilitas dengan library async Rust
  ✓ API publik dengan OpenAPI spec

Hindari Axum jika:
  ✗ Aplikasi dengan business logic kompleks (Rust verbose)
  ✗ Tim belum familiar dengan async/Tokio
  ✗ Butuh produktivitas tinggi (Rust lebih verbose dari Go/Python)
  ✗ Aplikasi CRUD sederhana
  ✗ Compile time menjadi blocker

Axum sangat cocok untuk service yang butuh throughput tinggi, type safety, dan async. Untuk aplikasi bisnis dengan CRUD dominan, Rust terasa overkill — Go dengan Gin lebih produktif.

Best Practice #

Cache Cargo Registry dan Target #

Rust compile time berat. Volume cargo-registry dan cargo-target wajib. Tanpa ini, setiap restart container akan re-download dan re-compile semua dependency (bisa 10+ menit).

Dummy Build untuk Cache Layer #

Pattern mkdir src && echo "fn main() {}" > src/main.rs && cargo build memungkinkan cache layer dependencies. Saat source code asli di-copy, hanya kode aplikasi yang perlu di-compile.

Pakai cargo-watch untuk Hot Reload #

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

Bind Mount Source Code, Bukan COPY #

Di development, mount source code. Volume cargo-target cache hasil build. Untuk production, COPY source ke image final.

Connection Pool untuk Database #

sqlx::PgPool dengan max_connections yang sesuai (default 10, naikkan untuk beban tinggi). Pool handle auto-reconnect.

Tracing untuk Observability #

Axum integrasi bagus dengan tracing ecosystem. Pakai TraceLayer untuk HTTP tracing, tracing-subscriber untuk output, dan export ke Jaeger/Honeycomb.

Error Handling dengan thiserror #

Pakai thiserror untuk custom error enum yang implement IntoResponse. Pattern ini konsisten di seluruh handler.

Pakai sqlx untuk Compile-time Checked Query #

sqlx::query! macro melakukan compile-time check terhadap database — kalau query salah, build gagal. Set DATABASE_URL di environment build.

Bind Server ke 0.0.0.0 #

Penting untuk Docker! SocketAddr::from(([0, 0, 0, 0], port)) agar bisa diakses dari host.

Graceful Shutdown #

Implement shutdown_signal() dengan tokio::signal::ctrl_c(). Axum serve() dengan with_graceful_shutdown menunggu shutdown signal sebelum close.

Troubleshooting #

Cargo Build Sangat Lambat #

Pastikan volume cargo-target di-mount. Cek apakah cargo fetch cache jalan. Pakai sccache untuk cache compile.

Hot Reload Tidak Detect #

Pastikan cargo-watch terinstall. Naikkan delay di konfigurasi. Cek apakah include_dir mencakup direktori source.

Database Connection Refused #

Pakai retry loop di PgPool::connect. Pakai depends_on: condition: service_healthy di Compose.

Port 3000 Sudah Dipakai #

lsof -i :3000
# ubah port mapping

SQLx Compile Error “no such table” #

Set DATABASE_URL saat compile agar sqlx::query! bisa check schema. Atau pakai sqlx::query (runtime check) untuk flexibility.

Cargo Watch Loop Terlalu Cepat #

Tambahkan .gitignore patterns ke cargo watch --ignore:

cargo watch -x run -i target -i .git -i node_modules

Ringkasan #

  • Axum ideal untuk microservice Rust high-performance dengan type-safety end-to-end.
  • Axum di atas Tokio — interoperable dengan library async Rust apapun. Berbeda dari Actix yang punya runtime sendiri.
  • Dockerfile multi-stage untuk production: dummy build untuk cache dependency, lalu build asli. Image akhir < 80 MB.
  • Dockerfile.dev untuk development: full toolchain + cargo-watch untuk hot reload.
  • Volume wajib untuk cargo-registry dan cargo-target — Rust compile time berat, cache antar restart container wajib.
  • Dummy build pattern: mkdir src && echo "fn main() {}" > src/main.rs && cargo build cache layer dependencies.
  • Cargo-watch memantau file .rs, rebuild, restart otomatis. DX yang sangat dibutuhkan Rust.
  • SQLx dengan compile-time check: sqlx::query! macro validasi query terhadap schema database saat compile.
  • Connection pool untuk Postgres (sqlx::PgPool) dan Redis (ConnectionManager). Atur max_connections sesuai beban.
  • Tracing untuk observability: TraceLayer + tracing-subscriber + Jaeger/Honeycomb.
  • Error handling dengan thiserror enum yang implement IntoResponse. Pattern konsisten untuk semua handler.
  • Middleware tower-http: TraceLayer, CorsLayer, RequestId, Timeout, Compression. Tinggal .layer().
  • Bind ke 0.0.0.0 — penting untuk Docker. SocketAddr::from(([0, 0, 0, 0], port)).
  • Graceful shutdown dengan tokio::signal::ctrl_c() + with_graceful_shutdown().
  • Best practice: cache Cargo, dummy build, cargo-watch, bind mount, connection pool, tracing, thiserror, sqlx, bind 0.0.0.0.
  • Pakai Axum untuk service Rust yang butuh type-safety dan async. Hindari untuk aplikasi bisnis CRUD-sentris.
  • Alternatif: Actix Web untuk performa tertinggi, Rocket untuk developer experience ergonomis, warp untuk low-level control.

← Sebelumnya: Sinatra   Berikutnya: Actix →

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