heisenberg

Crates.ioheisenberg
lib.rsheisenberg
version0.4.0
created_at2025-08-25 01:51:17.001011+00
updated_at2025-12-23 07:57:28.090086+00
descriptionFramework-agnostic dual-mode web serving for Rust applications. Seamlessly switch between proxy mode (forwarding to frontend dev servers) and embed mode (serving embedded static assets).
homepagehttps://github.com/adlio/heisenberg
repositoryhttps://github.com/adlio/heisenberg
max_upload_size
id1808968
size259,885
Aaron Longwell (adlio)

documentation

https://docs.rs/heisenberg

README

Heisenberg

Crates.io Documentation License

Heisenberg serves SPAs from Rust web applications. It switches between proxy mode (forwarding to a frontend dev server) and embed mode (serving assets compiled into your binary).

How It Works

In development, run cargo heisenberg run. This starts your frontend dev server and your Rust backend, proxying frontend requests (including WebSocket HMR) to the dev server.

For release builds, run cargo heisenberg build --release. This builds your frontend, then compiles the assets into your Rust binary using the embed_spa! macro.

Quick Start

Add to your Cargo.toml:

[dependencies]
heisenberg = "0.4"
axum = "0.7"
tokio = { version = "1.35", features = ["full"] }
rust-embed = "8.0"

Write your server:

use axum::{routing::get, Router};
use heisenberg::{Heisenberg, HeisenbergLayer};

#[tokio::main]
async fn main() {
    let spa = heisenberg::embed_spa!();
    let config = Heisenberg::new()
        .route("/*", spa)
        .dev_server("http://localhost:5173")
        .build();

    let app = Router::new()
        .route("/api/hello", get(|| async { "Hello!" }))
        .layer(HeisenbergLayer::new(config));

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();

    axum::serve(listener, app)
        .with_graceful_shutdown(heisenberg::shutdown_signal())
        .await
        .unwrap();
}

Install and run the cargo plugin:

cargo install cargo-heisenberg

# Development (proxy mode with HMR)
cargo heisenberg run

# Release build (embeds assets)
cargo heisenberg build --release

Cargo Plugin Commands

cargo heisenberg init    # Generate heisenberg.toml
cargo heisenberg build   # Build frontend, then cargo build
cargo heisenberg run     # Start frontend + backend with split-pane TUI

Add --no-tui to cargo heisenberg run for plain output (useful for copying error messages).

Configuration

When You Don't Need heisenberg.toml

The plugin auto-detects your frontend if you have a single SPA in ./web or ./frontend. No config file needed.

When You Need heisenberg.toml

Create heisenberg.toml when you have:

  • Multiple SPAs
  • A frontend in a non-standard directory
  • Custom build or dev commands

Single SPA example:

[spa]
working_dir = "./client"
output_dir = "./client/dist"

Multiple SPAs:

[[spa]]
name = "app"
working_dir = "./app"
output_dir = "./app/dist"
dev_server = "http://localhost:5173"

[[spa]]
name = "admin"
working_dir = "./admin"
output_dir = "./admin/dist"
dev_server = "http://localhost:5174"

In your Rust code, reference named SPAs:

let app = heisenberg::embed_spa!("app");
let admin = heisenberg::embed_spa!("admin");

let config = Heisenberg::new()
    .route("/admin/*", admin)
    .route("/*", app)
    .build();

Mode Detection

Command Mode Behavior
cargo heisenberg run Proxy Forwards to dev server with HMR
cargo run Embed Serves embedded assets
cargo build --release Embed Compiles assets into binary
HEISENBERG_MODE=proxy cargo run Proxy Force proxy mode
HEISENBERG_MODE=embed cargo run Embed Force embed mode

Framework Support

Axum

let spa = heisenberg::embed_spa!();
let config = Heisenberg::new()
    .route("/*", spa)
    .build();

let app = Router::new()
    .route("/api/hello", get(handler))
    .layer(HeisenbergLayer::new(config));

Actix-web

let spa = heisenberg::embed_spa!();
let config = Heisenberg::new()
    .route("/*", spa)
    .build();

HttpServer::new(move || {
    App::new()
        .app_data(web::Data::new(config.clone()))
        .route("/api/hello", web::get().to(handler))
        .default_service(web::to(heisenberg::adapters::actix::serve_spa))
})

Rocket

let spa = heisenberg::embed_spa!();
let config = Heisenberg::new()
    .route("/*", spa)
    .build();

#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(config)
        .mount("/api", routes![hello])
        .mount("/", spa_routes())
}

Examples

License

MIT License. See LICENSE.

Commit count: 0

cargo fmt