If you've ever built a server-rendered web application with Actix Web and the Tera templating engine, you know the rhythm of the development dance all too well:
- Make a change to your
.html
template. - Save the file.
- Go to your terminal, hit
Ctrl+C
. - Run
cargo run
. - Wait for the compile.
- Go back to your browser and hit refresh.
This cycle is a productivity killer. It's a constant context switch that pulls you out of your flow. For years, developers in other ecosystems have enjoyed "hot reloading" or "live reloading," where changes are reflected instantly. I wanted that experience for my Rust web projects.
Today, I'm excited to announce the release of Snapfire 0.4.0, a library I've built to solve this exact problem.
Snapfire is an ergonomic web templating library with an integrated live-reload server, featuring first-class support for Tera and Actix Web.
You can find it here:
- GitHub Repository: https://github.com/excsn/snapfire
- Crates.io: https://crates.io/crates/snapfire
The "Before": Manual Tera in Actix
Let's be honest about how most of us start with Tera in an Actix project. We reach for lazy_static
to create a global, static Tera
instance.
// in src/server/templates.rs
use lazy_static::lazy_static;
use tera::Tera;
lazy_static! {
pub static ref TEMPLATES: Tera = {
// This runs once, at the start of the program.
let mut tera = Tera::new("templates/**/*").expect("Tera parsing error");
tera.autoescape_on(vec![".html"]);
tera
};
}
This works, but it has some major drawbacks:
- No Live Reload: The templates are parsed once when the server starts. To see a change, you have to restart the entire application.
- Boilerplate in Handlers: Every single route handler ends up with repetitive logic to render a template, handle potential errors, and build an
HttpResponse
.
A typical handler looks like this:
// A typical handler BEFORE Snapfire
use crate::server::templates::TEMPLATES;
async fn index(req: HttpRequest) -> impl Responder {
let mut context = Context::new();
context.insert("page_title", "Home");
match TEMPLATES.render("pages/home.html", &context) {
Ok(body) => {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(body)
}
Err(err) => {
log::error!("Rendering error: {}", err);
HttpResponse::InternalServerError().finish()
}
}
}
This is fine for one route, but across dozens of routes, it's a lot of repeated, noisy code.
The "After": The Snapfire Experience
Snapfire replaces all of that with a clean, managed application state and a ridiculously simple rendering API.
Step 1: The Setup
Instead of a lazy_static
, you use the TeraWebBuilder
during your app initialization.
// In your main.rs
use snapfire::TeraWeb;
let app_state = TeraWeb::builder("templates/**/*.html")
.add_global("site_name", "My Awesome Site")
// Tell Snapfire to watch your static assets, too!
.watch_static("static/css")
.build()
.expect("Failed to build Snapfire");
// Then, add it to your Actix App
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(app_state.clone()))
// The middleware and configure calls are no-ops in production
.wrap(snapfire::actix::dev::InjectSnapfireScript::default())
.configure(|cfg| app_state.configure_routes(cfg))
// ... your routes ...
})
Step 2: The Handler (The Payoff)
That verbose handler from before? It now looks like this:
// The same handler AFTER Snapfire
async fn index(app_state: web::Data<TeraWeb>) -> impl Responder {
let mut context = Context::new();
context.insert("page_title", "Home");
// That's it.
app_state.render("pages/home.html", context)
}
That's the entire function.
The ugly match
statement is gone. The manual HttpResponse
construction is gone. The error handling is handled for you. It's
clean, concise, and focused on what matters: your page's data.
So What Do You Get?
By making this change, you unlock a much better development experience:
- 🔥 Template Live Reload: Change an
.html
file, save it, and your browser will perform a full page refresh automatically. No more restarting your server. - 🎨 CSS Hot Swapping: Change a
.css
file in a watched static directory, and Snapfire will push the change to the browser without a full page reload. Your styles update instantly. - ✅ Zero-Cost in Production: The file watcher, WebSocket server, and script injection middleware are all gated by a default feature flag,
dev-reload
. When you build for production with--no-default-features
, all of that code is completely compiled out. There is zero performance overhead.
The Vision
Right now, Snapfire provides a first-class integration for Tera and Actix Web because that's the stack I use most often. But it's designed with a modular core. In the future, I'd love to see or add support for other backends like Axum, Rocket, Handlebars, or Maud. The goal is to bring this high-productivity workflow to as much of the Rust web ecosystem as possible.
Give It a Try!
I'm really excited about this release and I hope it can save you some of the time and frustration it has saved me. You can get started right now.
- Check out the code on GitHub: https://github.com/excsn/snapfire
- Add it to your project from Crates.io: https://crates.io/crates/snapfire
If you have any feedback, find a bug, or have an idea for an improvement, please open an issue on GitHub. I'd love to hear what you think