tower-serve-embedded
Embed content-hashed, cache-busted static web assets (CSS, JS, images) into your Rust binary —
and serve them from any tower/axum
stack.
It's in the same family as rust-embed, but tuned for
serving web assets:
- Content hashing + cache busting. Every file is exposed at a hashed URL that mirrors its
location in your project —
assets/css/style.css→/assets/css/style.9f3a1c2b.css— so it can be servedimmutablewith a one-year cache and still update the instant its content changes. - A compile-time
asset!macro.asset!("assets/css/style.css")expands to the hashed URL as a&'static strfor SSR templates (e.g.maud). Typos are compile errors. - A
tower::Servicethat serves the embedded assets withContent-Type,ETag/304, andCache-Control. Drop it into axum or any othertower-based stack.
Design goals: simple (point it at a directory, done), hot-reloadable in development (via
build.rs + cargo:rerun-if-changed, so cargo watch rebuilds on change), and no proc
macros — all codegen happens in a build script, so IDE support stays sharp.
There's no URL-prefix to configure: an asset's path under your crate root is its URL.
Crates
| Crate | Where it goes | Purpose |
|---|---|---|
tower-serve-embedded |
[dependencies] |
Runtime types, the asset! macro, the tower service. |
tower-serve-embedded-build |
[build-dependencies] |
Walks the asset dir, hashes files, generates code. |
Quickstart
# Cargo.toml
[]
= "0.1"
[]
= "0.1"
// build.rs
// src/main.rs (or any module)
embed!; // brings `ASSETS` and the `asset!` macro into scope
Reference assets in templates
Refer to assets by their path relative to the crate root. asset! resolves at compile time
to the full, cache-busted URL:
let css = asset!; // "/assets/css/style.9f3a1c2b.css"
It drops straight into any engine that interpolates &str, e.g. maud:
html!
Need a name that isn't known at compile time? Use the runtime equivalent:
let url: = ASSETS.url;
Serve them
Each embedded path is already the full URL, so mount the service as a fallback — no nesting or prefix to keep in sync:
use ;
let app = new
.route
.fallback_service;
The service handles GET/HEAD, returns 304 Not Modified for matching If-None-Match, and
sets Content-Type, a strong ETag, and Cache-Control: public, max-age=31536000, immutable.
It's a plain tower::Service, so wrap it in any tower layer (compression, tracing, …) you like.
Prefer not to use the fallback slot? Scope it to the asset directory instead — the service still matches the full path, so route it without stripping the prefix:
Router::route_service("/assets/{*path}", ASSETS.service()).
Hot reloading
The build script emits cargo:rerun-if-changed for the asset directory and every file, so:
Editing an asset re-runs the build script, recomputes the hash, and rebuilds — the new
cache-busted URL flows through asset! automatically. Dev and release behave identically; there's
no separate "debug" embedding mode to reason about.
Two mechanisms cooperate here, and it's worth knowing they're distinct:
cargo watchis a filesystem watcher; its job is to invoke cargo when a file changes. With no-wflags it watches the whole crate (bothsrcandassets, ignoringtarget), so you don't need to enumerate paths.- The build script's
cargo:rerun-if-changedlines are a cargo signal that decides whether to re-run the build script once cargo is invoked. They don't watch the filesystem, so they don't replacecargo watch— they're what makes the rebuild actually pick up the new bytes and hash.
Notes
- Hashing is BLAKE3, truncated to 16 hex chars (64 bits) for filenames and ETags.
- Hidden files/directories (names starting with
.) and symlinks are skipped. - One asset set per crate: the generated
asset!is#[macro_export]ed at your crate root. - Every served asset is treated as immutable. A stable, always-fresh HTML entry point (SPA
index.html) is intentionally out of scope — keep that route in your app.
Examples
Runnable, self-contained setups live in examples/, each with its own build.rs,
assets/, and main.rs:
| Example | Run | How it serves assets |
|---|---|---|
axum |
cargo run -p axum-example |
Mounts ASSETS.service() (a tower::Service) as a fallback. |
actix |
cargo run -p actix-example |
Uses the framework-agnostic data API (ASSETS.get). |
warp |
cargo run -p warp-example |
Uses the framework-agnostic data API (ASSETS.get). |
axum is tower-based, so it mounts the service directly. actix-web and warp aren't, so they look
assets up with Assets::get and build the response by hand — the asset! macro and the
build.rs embedding step are identical across all three.
License
MIT OR Apache-2.0.