resuma 0.2.2

Resuma β€” SSR + Resumability + Islands + Server Actions + JS Bridge for Rust
Documentation

🌊 Resuma

Crates.io docs.rs License

The first Rust web framework with SSR + Resumability + Islands + Server Actions + a friendly JS Bridge.

Zero hydration, true resumability, native islands, automatic Rust→JS handler compilation.

Install: cargo install resuma Β· Docs: docs.rs/resuma Β· Repo: GitHub


What is this?

Resuma is a from-scratch Rust framework for building modern web apps with resumability instead of hydration:

Resumability vs hydration

Aspect Classic SSR + hydration Resuma
Client work after load Re-run components to attach listeners Resume serialized state and handlers
Initial JS Grows with app size ~3KB runtime + lazy chunks
Interactive boundaries Often manual First-class #[island]
Server RPC Custom wiring #[server] async fn + built-in endpoint
Handler code on client Ship framework runtime + app logic Compile handlers to small JS via rs2js
Templates Varies JSX-like view!{} β€” no extra sigils

The mental model: components only run on the server. The browser never re-executes them. Instead, the SSR pass serialises every signal, handler reference and island into the HTML, and the tiny client runtime resumes execution lazily β€” exactly when the user clicks something.

Hello, Resuma

use resuma::prelude::*;

#[component]
fn Counter() -> View {
    let count = use_signal(0);
    view! {
        <main>
            <h1>"Count: " {count}</h1>
            <button onClick={ move |_| count.update(|c| *c += 1) }>"+"</button>
        </main>
    }
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    ResumaApp::new()
        .with_title("Counter")
        .page("/", || Counter::render(CounterProps::default()))
        .serve(ServeOptions::default())
        .await
}

That single click handler is automatically translated to JavaScript by resuma-macros (rs2js), lazy-loaded on first interaction, and runs against the resumed signal state. No hydration, no re-execution, no WASM bundle.

Server actions

#[server]
async fn search(q: String) -> Vec<String> {
    db::search(&q).await
}

#[component]
fn LiveSearch() -> View {
    let query   = use_signal(String::new());
    let results = use_signal::<Vec<String>>(vec![]);

    view! {
        <input
            onInput={ js! {
                state.query.set(event.target.value);
                const r = await __resuma.action('search', [event.target.value]);
                state.results.set(r);
            }}
        />
        <ul>{format!("{} results", results.peek().len())}</ul>
    }
}

#[server] registers an RPC endpoint at /_resuma/action/search. The handler is dispatched there transparently.

Islands

#[island]
fn LiveCounter() -> View {
    let count = use_signal(0);
    view! {
        <button onClick={ move |_| count.update(|c| *c += 1) }>{count}</button>
    }
}

Mark any component with #[island] and Resuma will package its handlers into an isolated chunk that ships only when the island scrolls into view (or immediately, configurable).

Resuma Flow (full-stack layer)

One crate β€” resuma includes core + Flow in a single dependency.

Resuma Flow Purpose
FlowApp App builder with page registry
#[load] Server data before render
#[submit] Form mutations
src/pages/ File-based pages

See docs/PACKAGE.md and docs/FLOW.md.

Live docs site: https://resuma-docs.fly.dev Β· or cargo run -p example-website β†’ http://127.0.0.1:3000

resuma new my-app                    # static SSR (default)
resuma new my-app --template todo    # full Resuma showcase

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   resuma crate (v0.2)                    β”‚
β”‚                                                          β”‚
β”‚   core ──► ssr ──► server (axum)                         β”‚
β”‚     β”‚              GET  /_resuma/runtime.js              β”‚
β”‚     β”‚              POST /_resuma/action/:name            β”‚
β”‚     └──► flow + router (pages, loads, submits)           β”‚
β”‚                                                          β”‚
β”‚   resuma-macros (separate crate)                         β”‚
β”‚     view! / #[component] / rs2js β†’ JS handlers           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚ HTTP
                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Browser (~3KB)                         β”‚
β”‚   parse resuma/state Β· delegate events Β· lazy handlers   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

See docs/ARCHITECTURE.md for a deep dive.

Security: docs/SECURITY.md β€” CSRF, headers, rate limits, production checklist.

Backend patterns: docs/BACKEND.md β€” live in examples/todo.

All docs: docs/README.md Β· cargo run -p example-website

Publishing: docs/PUBLISHING.md β€” crates.io release checklist

Project layout

Resuma/
β”œβ”€β”€ crates/
β”‚   β”œβ”€β”€ resuma/             # single runtime crate (core, ssr, server, flow, cli)
β”‚   └── resuma-macros/      # proc-macros + rs2js (required separate crate)
β”œβ”€β”€ runtime/                # TypeScript source for the ~3KB client runtime
└── examples/
    β”œβ”€β”€ counter/
    β”œβ”€β”€ todo/
    β”œβ”€β”€ flow-demo/
    β”œβ”€β”€ flow-pages/
    └── website/            # docs site

Docs: docs/README.md Β· live site: cargo run -p example-website

Getting started

Pre-requisites: Rust 1.91+ (rustup).

Install from crates.io (recommended)

cargo install resuma
resuma new my-app --template todo
cd my-app
resuma dev

Library only (no CLI binary):

[dependencies]
resuma = { version = "0.2", default-features = false }
tokio = { version = "1", features = ["full"] }

From source (development)

git clone https://github.com/GolfredoPerezFernandez/resuma
cd resuma
cargo install --path crates/resuma --features cli

# Examples
cargo run -p example-counter   # http://127.0.0.1:3000
cargo run -p example-todo      # full-stack + security showcase
cargo run -p example-website   # docs site

What works in v0.2

βœ… Signal<T>, use_signal, use_effect, use_computed βœ… view!{} macro with JSX-like syntax (no $ noise) βœ… #[component] with auto-generated props builder βœ… #[server] async actions with JSON-RPC endpoint βœ… #[island] interactive component boundary βœ… js!{} escape hatch for raw JS handlers βœ… Rust β†’ JS compiler for common handler patterns βœ… SSR with resumability payload embedded in HTML βœ… ~3KB client runtime (lazy event delegation + signals + RPC) βœ… axum-based server with built-in /_resuma/* routes βœ… File-based routing scanner (src/routes/[id].rs β†’ /users/:id) βœ… resuma CLI: new, dev, build, routes

Roadmap (v0.3+)

  • Hot Module Reload via resuma CLI + websocket bridge
  • Build-time pre-rendering for static sites
  • Partial pre-rendering (PPR) β€” server shell + dynamic islands
  • #[island(load = "visible")] lazy load policies
  • Devtools extension for resumability payload inspection
  • First-class TypeScript bindings for js!{} blocks
  • WASM-backed islands for compute-heavy code (opt-in)

Already shipped in v0.2: single-crate layout, streaming SSR (Flow), layouts, file-based routing, security defaults, crates.io publish.

Why "Resuma"?

Spanish for both resumes (continues) and summary β€” fitting because the framework's superpower is resuming execution from a serialised summary of the server-side render.

License

MIT OR Apache-2.0