Tutorial 20 of 21 · for platform engineers who already have a Rust service

Add FSR to an existing Rust service

Stop treating FSR as a web framework you build an app inside. It's a tower::Service you mount wherever you want. Your binary, your runtime, your middleware, your listener.

Before you start

Straight from crates.io. No Node, no package manager.

cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version

Every command and screenshot on this page was captured with fsr 0.x.

Start a server

rust
use std::sync::Arc;
use snapfire_fsr_host::Host;

#[tokio::main]
async fn main() -> std::io::Result<()> {
  let host = Host::from_cwd().and_then(|b| b.build()).map_err(std::io::Error::other)?;
  print!("{}", host.report());
  let listen = host.listen().to_owned();
  Arc::new(host).serve(&listen).await
}

That's a complete FSR server. Host::from_cwd reads config/, the plan and the contracts. build binds every name and refuses to start if one is unanswered.

Print the report. It's the same table the build printed with the host's rows added.

Bind the listener yourself

serve binds an address. serve_listener takes one you already have, which is how a test picks port zero:

rust
let host = Arc::new(host);

let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
host.serve_listener(listener).await?;

Nest it in a router you already have

The host is a tower::Service. An existing axum router nests it under a prefix and keeps its own middleware:

rust
use axum::Router;

let app = Router::new()
  .route("/healthz", get(healthz))
  .nest_service("/shop", host.service());

Your API stays yours. Your auth layer, your rate limiter and your tracing middleware all still apply.

Actix works too, through the actix feature:

rust
snapfire_fsr_host::actix::serve(host, ("127.0.0.1", 8080)).await?;

Run a request with no socket

handle runs the whole edge for one request, cookies included, with nothing bound. That's what makes an integration test cheap and what lets you embed the host somewhere that isn't HTTP.

Terminate TLS when there's no proxy

Most deployments put a proxy in front and the host never sees a certificate. For the one that doesn't, the tls feature makes the hyper listener terminate. ALPN picks HTTP/2 or HTTP/1.1 per connection.

toml
[server]
http2 = true

Reload without dropping connections

Everything a request reads is one set of tables the host swaps whole: the plan, the contracts, the clients, the head, the static roots, the locales, the identity flow.

rust
let host = Host::from(".")?
  .reloader(|| Host::from("."))
  .build()?;
let report = host.reload()?;
print!("{report}");

A request already running finishes on the tables it started with. The next one sees the new ones. Sessions aren't part of the tables, so nobody is signed out by a deploy. A reload whose [session] settings differ from the running ones is refused rather than applied.

Wire that to SIGHUP and you have config reload. Wire it to your control plane and you have something better. 310 uses the same machinery to swap a mounted site while the process keeps serving.

Take names back in Rust

The builder binds a name to whatever answers it. That can be your code:

rust
Host::from(root)?
  .source_override("pricing", |ctx| async move { /* ... */ })
  .action_override("cart.checkout", |ctx, input| async move { /* ... */ })
  .route("/healthz", healthz_plan())
  .build()?

500 covers that, including why replacing something is spelled _override and why overriding nothing is a boot error.

Why do it this way

One binary holds your service and your web tier. No sidecar, no second deployment, no internal HTTP hop between your API and what renders it.

And when you need the web tier to do something your framework never anticipated, it's a tower service in your own main.rs rather than a configuration file you're negotiating with.

Next up: 150. Render with Tera and no TypeScript, which drops React entirely and renders from templates.

Built with SnapFire FSR. Pure Rust runtime, zero Node.js on the server.

Proudly Created by Excerion Sun LLC