Cookbook / Files and pages

Static files and single-page apps

Serve a directory with range requests and caching, and fall back to index.html for client-side routes.

static_dir streams files from disk with range requests, ETag/304 handling and correct content types. spa_fallback serves your index.html for any path that is neither a file nor an API route, so deep links in a React, Vue or Svelte app keep working.

src/main.rs
use axeon::{Json, Router, Server};
use axeon::json;

fn main() {
    let mut app = Server::new();

    // API routes win over files.
    let mut api = Router::new();
    api.get("/hello", |_req| async { Json(json!({ "hello": "world" })) });
    app.mount("/api", api);

    app.static_dir("public")                       // ./public/**
        .static_cache_control("public, max-age=3600")
        .spa_fallback("index.html");               // /dashboard/settings -> index.html

    app.listen("127.0.0.1:3000").expect("server failed to start");
}
Terminal
mkdir public && echo '<h1>My app</h1>' > public/index.html
curl -i http://127.0.0.1:3000/                  # index.html
curl -i http://127.0.0.1:3000/dashboard/stats -H 'accept: text/html'   # index.html again (SPA route)
curl -i http://127.0.0.1:3000/api/hello         # JSON

Notes#

  • The fallback only answers GET and HEAD requests that accept HTML, for paths without a file extension. A missing /app.js or an API call still gets a real 404 instead of your HTML page.
  • Path traversal is refused: .. segments, backslashes and absolute paths never escape the directory.
  • Give hashed build assets (app.3f9a1c.js) a long max-age and keep index.html short-lived, so deploys are picked up immediately.
  • Pair with compression for text assets.