---
title: "Add FSR to an existing Rust service"
audience: "platform engineers who already have a Rust service"
description: "The FSR host is a tower::Service, so you can mount it inside a binary you already have, with your own runtime, middleware and listener."
snippet_caption: "src/main.rs"
snippet: "use std::sync::Arc;\nuse snapfire_fsr_host::Host;\n\n#[tokio::main]\nasync fn main() -> std::io::Result<()> {\n  let host = Host::from_cwd().and_then(|b| b.build()).map_err(std::io::Error::other)?;\n  print!(\"{}\", host.report());\n  let listen = host.listen().to_owned();\n  Arc::new(host).serve(&listen).await\n}"
---

import { Install } from "@src/ui/Install";

The FSR host is a `tower::Service`, so you can mount it inside a binary you already have, with your own runtime, middleware and listener.

<Install captured="0.x" />

## Start a server

```rust title="src/main.rs"
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 `fsr build --verbose` 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 title="src/main.rs" {7-9}
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)?;
  let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
  println!("listening on {}", listener.local_addr()?);
  Arc::new(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. This needs `axum` in your `Cargo.toml`:

```rust title="src/main.rs" {3,6-8,13-18}
use std::sync::Arc;

use axum::{routing::get, Router};
use snapfire_fsr_host::Host;

async fn healthz() -> &'static str {
  "ok"
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
  let host = Host::from_cwd().and_then(|b| b.build()).map_err(std::io::Error::other)?;
  let host = Arc::new(host);
  let app = Router::new()
    .route("/healthz", get(healthz))
    .nest_service("/shop", host.service());
  let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await?;
  axum::serve(listener, app).await
}
```

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

Actix works too. Turn on the host's `actix` feature and add `actix-web = "4"`, then run `main` on actix's runtime:

```rust title="src/main.rs" {4,7}
use std::sync::Arc;
use snapfire_fsr_host::Host;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
  let host = Host::from_cwd().and_then(|b| b.build()).map_err(std::io::Error::other)?;
  snapfire_fsr_host::actix::serve(Arc::new(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. You can use it for integration tests and to 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 title="config/app.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.

`host.reload()` rereads the configuration, the plan and the contracts and swaps the tables. This `main` does it on every `SIGHUP`, which needs tokio's `signal` feature:

```rust title="src/main.rs" {4,9-20,23}
use std::sync::Arc;

use snapfire_fsr_host::Host;
use tokio::signal::unix::{signal, SignalKind};

#[tokio::main]
async fn main() -> std::io::Result<()> {
  let host = Host::from_cwd().and_then(|b| b.build()).map_err(std::io::Error::other)?;
  let host = Arc::new(host);

  let reloading = host.clone();
  let mut hangup = signal(SignalKind::hangup())?;
  tokio::spawn(async move {
    while hangup.recv().await.is_some() {
      match reloading.reload() {
        Ok(report) => print!("{report}"),
        Err(e) => eprintln!("reload refused: {e}"),
      }
    }
  });

  let listen = host.listen().to_owned();
  host.serve(&listen).await
}
```

> A host built from its configuration alone reloads by reading it again. One you gave overrides, services or an evaluator by hand needs `.reloader(...)` on the builder, a function that builds it again with those added.

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.

Call it from a `SIGHUP` handler for config reload or from your control plane. [310](https://snapfirers.com/fsr/learn/310-put-a-docs-site-inside-your-app) 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. With the `shop` app from [500](https://snapfirers.com/fsr/learn/500-move-a-loader-into-rust), the overrides chain between `Host::from_cwd()` and `.build()`:

```rust title="src/main.rs" {3-4,9-15}
use std::sync::Arc;

use snapfire_fsr::Plan;
use snapfire_fsr_core::{Data, Value};
use snapfire_fsr_host::Host;

#[tokio::main]
async fn main() -> std::io::Result<()> {
  let host = Host::from_cwd()
    .and_then(|builder| {
      builder
        .source_override("product.$id", |_ctx| async move { Ok(Data::default()) })
        .action_override("product.$id.addToCart", |_ctx, _input| async move { Ok(Value::Null) })
        .route("/about", Plan::of("shell#document").slot("content", Plan::of("routes/page.tsx#default")))
        .build()
    })
    .map_err(std::io::Error::other)?;
  let listen = host.listen().to_owned();
  Arc::new(host).serve(&listen).await
}
```

500 covers each of these in full, including why replacing something is spelled `_override` and why overriding nothing is a boot error.

Next up: [520. Serve Tera templates from Rust](https://snapfirers.com/fsr/learn/520-serve-tera-templates-from-rust), which renders pages from Tera templates.
