Static Files
Serve frontend assets — CSS, JavaScript, images — directly from disk, and
support Single Page Application (SPA) routing with a fallback to index.html.
Requires the static-files Cargo feature:
ultimo = { version = "0.5", features = ["static-files"] }Serving a directory
serve_static(prefix, dir) registers a GET {prefix}/* route that reads
files from dir on disk.
use ultimo::prelude::*;
let mut app = Ultimo::new();
// GET /assets/style.css → reads ./public/style.css
app.serve_static("/assets", "./public");
app.listen("127.0.0.1:3000").awaitContent-Type— detected from the file extension.ETag—"{size}-{mtime_secs}", used for conditional GET.Content-Length.
Conditional GET: If the client sends If-None-Match matching the current
ETag, the server returns 304 Not Modified with an empty body, saving
bandwidth on repeat visits.
SPA fallback
For Single Page Applications where the client-side router handles URLs (React
Router, Vue Router, etc.), every GET request that doesn't match an API route
should return index.html. Use serve_spa:
use ultimo::prelude::*;
let mut app = Ultimo::new();
// API routes first — they take precedence over the SPA fallback
app.get("/api/user", |ctx: Context| async move {
ctx.json(serde_json::json!({ "name": "Ada" })).await
});
// Serve static assets from ./dist/assets under /assets
app.serve_static("/assets", "./dist/assets");
// Fallback: any unmatched GET → ./dist/index.html
app.serve_spa("./dist", "index.html");
app.listen("127.0.0.1:3000").await
serve_spaonly interceptsGETrequests that returned 404.POST,PUT,DELETE, etc. 404s pass through unchanged.
Security
Path traversal is prevented at the filesystem level: the resolved path is
canonicalized and must remain inside the configured root directory. Requests
like GET /assets/../../etc/passwd return 404 — no existence leak, no
directory escape.
Directory listing is not supported — requesting a path that resolves to a
directory returns 404. Use serve_spa if you want a catch-all.
Pairing with compression
Mount the compression() middleware before serving static files to enable
automatic gzip/brotli on text assets:
use ultimo::middleware::builtin::compression;
use ultimo::prelude::*;
let mut app = Ultimo::new();
app.use_middleware(compression());
app.serve_static("/assets", "./dist/assets");
app.serve_spa("./dist", "index.html");See the Compression section for details.
Full example
See examples/spa-demo
for a runnable end-to-end demo combining static files, SPA fallback, and
compression.
cargo run -p spa-demo
# → http://127.0.0.1:3000