Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tutorial 30: Axum Escape-Hatch Snapshot (cargo rullst eject) 🔓

eject generates a minimal Axum/Tokio entry point that can begin a manual migration away from Rullst’s server wrapper. It does not statically expand macros, copy the application’s route graph, convert middleware, or remove Rullst dependencies automatically.


Step 1: Generate a separate starting point

cargo rullst eject

The default output is src/ejected_main.rs; the existing src/main.rs remains unchanged. Cargo does not build src/ejected_main.rs (it is not a binary target): review it and compare it with src/main.rs; only --force (Step 2) replaces the entry point. The generated template serves a placeholder route from application_routes() and wraps it in the configured Rullst security baseline, the same apply_security_baseline step rullst::Server performs with Rullst.toml. Axum is reached through the rullst::web::axum re-export, so the template compiles without adding Axum to Cargo.toml. Without its header comment and start-up message, the template is:

use rullst::RullstConfig;
use rullst::web::axum::{Router, response::Html, routing::get};
use std::net::SocketAddr;

/// Mount the application's routes here.
fn application_routes() -> Router {
    Router::new().route("/", get(|| async { Html("<h1>Ejected Axum Server</h1>") }))
}

#[rullst::runtime::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = if std::path::Path::new("Rullst.toml").exists() {
        RullstConfig::load_from_file("Rullst.toml").await?
    } else {
        RullstConfig::default()
    };
    let environment = config.environment()?;
    let app = rullst::apply_security_baseline(application_routes(), config.security, environment)?;

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = rullst::async_runtime::tokio::net::TcpListener::bind(addr).await?;
    rullst::web::axum::serve(
        listener,
        app.into_make_service_with_connect_info::<SocketAddr>(),
    )
    .await?;
    Ok(())
}

The baseline covers secure headers, CORS, WAF/RASP and CSRF (headers, WAF and CSRF in staging/production). Static files, rate limiting, lifecycle probes, hot reload, sessions, authentication and authorization are not included.

Move application routes into application_routes() one bounded group at a time and retain equivalent limits, telemetry, graceful shutdown, health behavior, state, and authorization tests.


Step 2: Treat --force as a deliberate replacement

cargo rullst eject --force

The hardened command first preserves the original entry point as src/main.rs.rullst-backup and refuses to overwrite an existing backup. The template does not carry over the original routes or module declarations; move them from the backup before building or deploying. Keep a normal version-control commit as the authoritative recovery path. Custom output paths are restricted to relative Rust files under src/ and existing targets are not overwritten implicitly.


Key takeaways

  • Ejection is a migration aid, not a semantics-preserving compiler transform.
  • The application remains responsible for dependency cleanup and replacements for ORM, auth, queues, Studio, Nexus, Capital, AI, and other selected crates.
  • Run cargo fmt, strict Clippy, the complete test suite, and application security/operational checks after every migrated route group.