resuma 0.3.1

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, lazy handler chunks, automatic Rust→JS handler compilation.

Install: cargo install resuma Β· Docs: resuma-docs.fly.dev Β· API: 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 Every #[component] is resumable; #[island] optional
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. SSR serialises signals and handler references into HTML; the tiny client runtime resumes execution lazily β€” on first interaction or when a boundary scrolls into view.

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 (optional)

#[island(load = "visible")]
fn LiveChart() -> View {
    let points = use_signal(vec![1, 4, 2, 8]);
    view! { /* heavy widget β€” JS loads when visible */ }
}

Every #[component] is already resumable (lazy handler chunks + viewport prefetch). Use #[island] only for heavy client bundles, load = "visible", or dev HMR.

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.3)                    β”‚
β”‚                                                          β”‚
β”‚   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.3", 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.3

βœ… Signal<T>, use_signal, use_effect, use_computed (SSR-only; use macros for client replay) βœ… view!{} macro with JSX-like syntax (no $ noise) βœ… #[component] with auto-generated props builder β€” resumable boundary by default βœ… #[server] async actions with JSON-RPC endpoint βœ… #[island] optional β€” heavy widgets, visible load, dev HMR βœ… js!{} escape hatch for raw JS handlers βœ… Rust β†’ JS compiler for common handler patterns βœ… SSR with resumability payload embedded in HTML βœ… Lazy handler chunks externalized from payload + viewport prefetch βœ… ~3KB client runtime (lazy event delegation + signals + RPC) βœ… axum-based server with built-in /_resuma/* routes βœ… File-based routing scanner (src/pages/[id].rs β†’ /users/:id) βœ… Flow static routes receive FlowRequest (query, headers, method) βœ… computed! / debounce! / effect! β€” client-replayable (rs2js) βœ… #[island(load = "visible")] lazy island loading βœ… Island HMR refresh + dev WebSocket (resuma dev) βœ… resuma build --static export scaffold βœ… resuma CLI: new (basic/todo/flow), dev, build, routes

Resumability model (default)

Every #[component] is a resumable boundary:

  • SSR always β€” Rust renders HTML on the server
  • Handlers register under the component chunk (lazy-fetched from /_resuma/handler/{Component}.js)
  • Small page handlers stay inline in the payload (__page__, under 256 bytes)
  • Signals + computed! / effect! β€” client replay without re-running components

#[island] is optional β€” use it only for heavy lazy JS bundles, load = "visible", or dev HMR. Most apps need only #[component] + view!.

#[component]
fn Counter() -> View {
    let n = use_signal(0);
    let doubled = computed!([n], move || n.get() * 2); // client + SSR
    view! { <p>{doubled}</p> <button onClick={move |_| n.update(|v| *v += 1)}>"+"</button> }
}

Client-side reactivity

use_signal updates work on the client via the resumability payload. For derived state and effects in the browser, use computed!([deps], move || …), effect!([deps], move || …), and debounce!([deps], ms, move || …) (rs2js-translated). Plain use_computed() / use_effect() run on SSR only.

Roadmap (v0.4+)

  • Partial pre-rendering (PPR) β€” server shell + dynamic boundaries
  • 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.3: resumability everywhere, client effect replay, lazy handler externalization, viewport prefetch, dev HMR, static export, HTTP context in Flow routes, env-based bind, flow scaffold, crypto CSRF. v0.2 brought 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