#react #actix #vite #browser #dev-server #starter #client-side-rendering #embedded

bin+lib haven

Actix + React + Vite integration for server-rendered applications

6 releases

Uses new Rust 2024

0.1.5 May 6, 2026
0.1.4 May 6, 2026
0.1.0 Apr 8, 2026

#1771 in HTTP server

MIT license

240KB
7K SLoC

Rust 4K SLoC // 0.0% comments JSX 2K SLoC JavaScript 740 SLoC TSX 63 SLoC Templ 13 SLoC Shell 8 SLoC

haven

haven is a Rust crate for building server-rendered React apps on top of actix-web.

It gives an Actix application a React rendering layer. Your Rust server stays in charge of HTTP, routing, middleware, cookies, sessions, and responses, while Haven runs your React server entry inside an embedded deno_core runtime and turns page renders into either full HTML responses or JSON page envelopes for client-side visits.

A good mental model is:

  • Actix owns the request.
  • Haven renders the page.
  • Vite builds the browser and server bundles.
  • React owns the UI.

This repository contains the Rust crate, the browser package published as @ovior/haven, a small starter template, and an example Actix server.

Why Haven exists

Haven is meant for apps where Rust is already the backend, but you still want the ergonomics of React pages, Vite, client-side navigation, SSR, redirects, partial reloads, and shared page data.

The crate keeps the integration point small. You create one shared RendererState when the server starts, store it in Actix app data, and then use a request-scoped Renderer inside handlers.

That gives you a flow that feels natural in Actix:

async fn home(renderer: Renderer) -> HttpResponse {
    renderer
        .render("index")
        .props(json!({
            "message": "Hello from Actix"
        }))
        .await
}

The handler decides what page to render and which props to pass. Haven takes care of calling the JavaScript server entry and formatting the response for either a normal browser load or an in-app visit.

Crate API

The main Rust types are:

  • RendererState
  • Renderer
  • RendererConfig
  • Redirect

RendererState owns the JavaScript runtime bridge. In a typical app, you build it once at startup and share it through web::Data.

Renderer is request-scoped. It wraps the current Actix request and uses the shared RendererState to render pages, stream pages, redirect, and inspect Haven-specific request metadata.

use actix_web::{App, HttpResponse, HttpServer, web};
use haven::actix::Renderer;
use haven::{RendererConfig, RendererMode, RendererState};
use serde_json::json;

async fn home(renderer: Renderer) -> HttpResponse {
    renderer
        .render("index")
        .props(json!({
            "message": "Hello from Actix"
        }))
        .await
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let renderer_state = RendererState::new(RendererConfig {
        mode: RendererMode::Development,
        ..RendererConfig::default()
    })
    .expect("renderer");

    let renderer_data = web::Data::new(renderer_state);

    HttpServer::new(move || {
        App::new()
            .app_data(renderer_data.clone())
            .route("/", web::get().to(home))
    })
    .bind(("127.0.0.1", 3000))?
    .run()
    .await
}

Rendering from a request

Most handlers use one of these patterns:

renderer.render("page").await
renderer.render("page").props(data).await
renderer.stream("page").await
Redirect::to("/other-page")
Redirect::back()

Renderer also exposes request-aware helpers:

renderer.uri()
renderer.headers()
renderer.is_visit()
renderer.partial_reload()

These are useful when a handler needs to adjust props based on the current request, detect a client-side visit, or handle partial reload behavior.

Create redirects directly with Redirect::to(...), Redirect::temporary(...), Redirect::permanent(...), Redirect::hard(...), or Redirect::back().

Redirect::back() returns a 303 See Other to the request Referer when it is present. If the request has no Referer header, Haven falls back to the current request URL.

Rendering outside a request

For background jobs, tests, or places where you do not have an Actix request, use RendererState directly:

let page = renderer_state
    .render("index")
    .url("/")
    .props(serde_json::json!({ "message": "hello" }))?
    .await?;

This path is useful when you want the same page rendering machinery without going through an Actix handler.

JavaScript app layout

The app source usually lives in app/.

The important files are:

  • app/entry-server.jsx or app/entry-server.tsx
  • app/entry-client.jsx or app/entry-client.tsx
  • app/renderer.jsx or app/renderer.tsx
  • app/pages/**

A minimal server entry looks like this:

import { defineServerEntry } from "@ovior/haven/page";
import { renderEnvelope, streamEnvelope } from "./renderer";

export const { renderPage, streamPage } = defineServerEntry({
  renderEnvelope,
  streamEnvelope,
});

defineServerEntry(...) wires up the SSR globals used by Haven's embedded runtime and returns the named exports expected by the Vite dev/runtime bridge.

Pages are referenced by string ids derived from the file layout under app/pages.

For example:

  • app/pages/index.jsx becomes "index"
  • app/pages/about.jsx becomes "about"
  • app/pages/admin/users.tsx becomes "admin/users"

Browser package

The browser helpers are published as @ovior/haven.

Public entry points include:

  • @ovior/haven/page
  • @ovior/haven/data
  • @ovior/haven/router
  • @ovior/haven/framework
  • @ovior/haven/runtime
  • @ovior/haven/i18n

These modules provide the client-side pieces for page envelopes, navigation, runtime data, translations, and framework-level helpers.

Starter app

The repository includes a CLI that can generate a local starter app from templates/starter.

Create a new app:

cargo run --bin haven -- new my-app

Then install JavaScript dependencies and run the development supervisor:

cd my-app
npm install
cargo run --bin haven -- dev

The generated app uses:

  • actix-web for the Rust server
  • actix-files for serving built client assets
  • @ovior/haven for browser-side helpers
  • Vite for client and server JavaScript builds

Development mode

In development, Haven usually talks to a Haven-aware Vite dev server.

The key RendererConfig fields are:

  • mode: RendererMode::Development
  • vite_dev_server_origin
  • start_vite_dev_server

When vite_dev_server_origin is set, Haven sends SSR requests through the dev runtime at that origin. This keeps the development loop close to a normal Vite React app while still letting Actix handle the HTTP request.

By default, Haven leaves process management to you. If you want the renderer to start the Vite dev runtime itself, set:

start_vite_dev_server: true

For local apps, the bundled command is usually the easiest path:

cargo run --bin haven -- dev

That command starts both sides and restarts the Rust server when Rust files change.

Production mode

In production, Haven reads the built server entry and client manifest from the Vite output.

RendererConfig::default() points at the conventional paths:

  • server entry: dist/server/entry-server.mjs
  • client manifest: dist/client/.vite/manifest.json
  • app source: app/

A production build needs both pieces:

  1. the Rust binary
  2. the Vite client/server output

Build the JavaScript assets:

npm install
npm run build:js

Then build or run the Rust app as usual:

cargo build --release

Browser protocol

Haven supports both full-page HTML responses and JSON page visits.

On a normal browser request, the server returns HTML.

On an in-app Haven navigation, the browser sends protocol headers that allow the server to return a serialized page envelope instead. The client can then update the current page without doing a full browser reload.

The same protocol supports:

  • same-app redirects
  • hard redirects
  • validation errors on the page envelope
  • partial reloads for selected top-level props or resources

This is the part that lets a Rust-backed React app feel like a modern client-side app while still rendering through the server.

Translations

Translations live under:

locales/<locale>/*.json

Rust loads the dictionaries, and the embedded runtime exposes translation helpers to JavaScript. That allows pages to translate during SSR without each page module carrying the full translation payload.

Example server

The repository includes an example Actix server:

cargo run --example actix_server

By default, the example expects a dev runtime at:

http://127.0.0.1:5174

and binds the Rust app on port 4000.

Repository commands

Useful commands while working on this repository:

npm install
npm run build:public
npm run build:js
cargo check
cargo test

Browser tests use Playwright:

npm run test:browser

Design notes

Haven is intentionally focused on one integration path: Actix plus React plus Vite.

That focus keeps the crate small and predictable. It also means a production app should be built with the expected Vite output available, and page ids currently come from the file layout rather than generated Rust types.

For apps that already use Actix as the server boundary, this keeps the moving parts easy to reason about: Rust handles the web server, React handles the UI, and Haven connects the two at the page rendering layer.

License

MIT

Dependencies

~129MB
~2.5M SLoC