Static Site
This example shows how to serve public files with vix::App.
Static files are handled by Core App, not by middleware.
Use this example when you want to serve:
HTML
CSS
JavaScript
images
SPA frontend files
public assetsThe important API is:
app.static_dir(...);Middleware can enhance static responses, for example by adding optional compression, but static file serving itself belongs to vix::App.
What this example builds
The app serves files from:
public/Mounted at:
/With:
index.html as the default file
Cache-Control for browser caching
optional SPA fallback
optional static compression hookThe app also exposes:
GET /api/healthThis lets you serve a frontend and an API from the same Vix app.
Project structure
Create this structure:
static_site_demo/
├── static_site.cpp
└── public/
├── index.html
├── app.js
└── style.csspublic/index.html
Create:
public/index.htmlAdd:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Vix Static Site</title>
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<main class="page">
<h1>Hello from Vix</h1>
<p>This page is served from the public directory.</p>
<button id="health-button">Check API health</button>
<pre id="output">Click the button.</pre>
</main>
<script src="/app.js"></script>
</body>
</html>public/style.css
Create:
public/style.cssAdd:
body {
margin: 0;
font-family: system-ui, sans-serif;
background: #f6f7f9;
color: #111827;
}
.page {
max-width: 720px;
margin: 80px auto;
padding: 32px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 16px;
}
button {
padding: 10px 16px;
border: 0;
border-radius: 10px;
cursor: pointer;
}
pre {
margin-top: 24px;
padding: 16px;
background: #111827;
color: #f9fafb;
border-radius: 12px;
overflow: auto;
}public/app.js
Create:
public/app.jsAdd:
const button = document.querySelector("#health-button");
const output = document.querySelector("#output");
button.addEventListener("click", async () => {
const response = await fetch("/api/health");
const data = await response.json();
output.textContent = JSON.stringify(data, null, 2);
});static_site.cpp
Create:
static_site.cppAdd:
#include <vix.hpp>
using namespace vix;
static void register_static_files(App &app)
{
app.static_dir(
"public",
"/",
"index.html",
true,
"public, max-age=3600",
true,
false
);
}
static void register_routes(App &app)
{
app.get("/api/health", [](Request &, Response &res)
{
res.json({
"ok", true,
"service", "static-site"
});
});
}
int main()
{
App app;
register_static_files(app);
register_routes(app);
app.run(8080);
return 0;
}Run it
From the project directory:
vix run static_site.cppOpen:
http://127.0.0.1:8080/Or test with curl:
curl -i http://127.0.0.1:8080/Expected response:
200 OKThe body should be the contents of public/index.html.
Test static assets
CSS:
curl -i http://127.0.0.1:8080/style.cssJavaScript:
curl -i http://127.0.0.1:8080/app.jsBoth files are served from the public/ directory.
Test API route
curl -i http://127.0.0.1:8080/api/healthExpected body shape:
{
"ok": true,
"service": "static-site"
}This shows that the same app can serve static files and dynamic API routes.
Understanding app.static_dir(...)
The example uses:
app.static_dir(
"public",
"/",
"index.html",
true,
"public, max-age=3600",
true,
false
);The arguments are:
public directory
mount path
index file
enable index file
Cache-Control value
allow fallthrough
SPA fallbackIn this example:
public
files are read from ./public
/
files are mounted at the root URL
index.html
/ returns public/index.html
public, max-age=3600
browser cache header for public assets
SPA fallback false
unknown paths do not automatically return index.htmlStatic files are not middleware
This is Core App behavior:
app.static_dir(...);This is middleware behavior:
app.use(...);Keep the distinction clear:
app.static_dir(...)
serves files from disk
app.use(...)
installs route middleware
App::set_static_response_hook(...)
optionally modifies static responses after Core writes themStatic files are served before your route handler is needed.
Middleware can still be used for API routes, but static file serving itself is not a middleware feature.
Serve a SPA
For a Single Page Application, enable SPA fallback.
app.static_dir(
"public",
"/",
"index.html",
true,
"public, max-age=3600",
true,
true
);The last argument is:
SPA fallback = trueWith SPA fallback enabled, paths such as:
/dashboard
/settings
/products/123can return:
public/index.htmlThis is useful for frontend routers.
Example:
static void register_static_files(App &app)
{
app.static_dir(
"public",
"/",
"index.html",
true,
"public, max-age=3600",
true,
true
);
}Use SPA fallback only when your frontend needs client-side routing.
For normal static sites, keep it false.
Static site with middleware for API routes
Static files do not require middleware, but API routes often do.
Example:
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
static void register_static_files(App &app)
{
app.static_dir(
"public",
"/",
"index.html",
true,
"public, max-age=3600",
true,
true
);
}
static void register_api_middleware(App &app)
{
app.use("/api", middleware::app::request_id_dev());
app.use("/api", middleware::app::timing_dev());
app.use("/api", middleware::app::security_headers_dev());
app.use("/api", middleware::app::rate_limit_dev());
}
static void register_routes(App &app)
{
app.get("/api/health", [](Request &, Response &res)
{
res.json({
"ok", true,
"service", "static-site"
});
});
}
int main()
{
App app;
register_static_files(app);
register_api_middleware(app);
register_routes(app);
app.run(8080);
return 0;
}This is a common shape:
static files
served by Core App
/api routes
protected by middlewareEnable static compression
Static compression is optional.
It uses a static response hook from the middleware performance module.
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
vix::middleware::performance::CompressionOptions options{
.min_size = 1024,
.add_vary = true,
.enabled = true
};
vix::App::set_static_response_hook(
vix::middleware::performance::compressed_static_response_hook(options)
);
app.static_dir(
"public",
"/",
"index.html",
true,
"public, max-age=3600",
true,
true
);
app.get("/api/health", [](Request &, Response &res)
{
res.json({
"ok", true
});
});
app.run(8080);
}This line does not serve files:
vix::App::set_static_response_hook(...);It only enhances eligible static responses after Core has produced them.
The file serving still comes from:
app.static_dir(...);Test static compression
Request with gzip support:
curl -i \
http://127.0.0.1:8080/app.js \
-H "Accept-Encoding: gzip"Depending on the build configuration and compression support, eligible responses can include:
Content-Encoding: gzip
Vary: Accept-EncodingSmall files may not be compressed because of min_size.
Already compressed assets should generally not be compressed again.
Dynamic compression is different
This compresses dynamic route responses:
app.use(vix::middleware::app::adapt_ctx(
vix::middleware::performance::compression(options)
));This compresses eligible static file responses:
vix::App::set_static_response_hook(
vix::middleware::performance::compressed_static_response_hook(options)
);They are different paths.
Use both only when you want both dynamic route compression and static response compression.
Configuration-driven static site
In a generated or production-style app, static behavior can come from configuration.
Example .env values:
PUBLIC_PATH=public
PUBLIC_MOUNT=/
PUBLIC_INDEX=index.html
PUBLIC_CACHE_CONTROL=public, max-age=3600
PUBLIC_SPA_FALLBACK=true
PUBLIC_COMPRESSION=false
PUBLIC_COMPRESSION_MIN_SIZE=1024Bootstrap code can wire those values:
const std::string publicPath =
cfg.getString("public.path", "public");
const std::string publicMount =
cfg.getString("public.mount", "/");
const std::string publicIndex =
cfg.getString("public.index", "index.html");
const std::string publicCacheControl =
cfg.getString("public.cache_control", "public, max-age=3600");
const bool publicSpaFallback =
cfg.getBool("public.spa_fallback", false);
app.static_dir(
publicPath,
publicMount,
publicIndex,
true,
publicCacheControl,
true,
publicSpaFallback
);If static compression is enabled:
const bool publicCompression =
cfg.getBool("public.compression", false);
const int publicCompressionMinSize =
cfg.getInt("public.compression_min_size", 1024);
if (publicCompression)
{
vix::middleware::performance::CompressionOptions options{
.min_size = static_cast<std::size_t>(publicCompressionMinSize),
.add_vary = true,
.enabled = true
};
vix::App::set_static_response_hook(
vix::middleware::performance::compressed_static_response_hook(options)
);
}This keeps static behavior controlled by configuration instead of hardcoding every value.
Cache-Control
The static directory call can set a Cache-Control value:
"public, max-age=3600"That means browsers and caches may reuse files for a period of time.
For development, you may prefer a short cache:
no-cacheFor production assets with hashed filenames, you may prefer a longer cache:
public, max-age=31536000, immutableUse a policy that matches how your frontend assets are built.
If filenames change when content changes, longer caching is safer.
If filenames do not change, keep caching shorter.
Static site plus API example
Here is a complete version with static files, API middleware, SPA fallback, and optional static compression.
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
static void configure_static_compression(bool enabled)
{
if (!enabled)
return;
vix::middleware::performance::CompressionOptions options{
.min_size = 1024,
.add_vary = true,
.enabled = true
};
vix::App::set_static_response_hook(
vix::middleware::performance::compressed_static_response_hook(options)
);
}
static void register_static_files(App &app)
{
app.static_dir(
"public",
"/",
"index.html",
true,
"public, max-age=3600",
true,
true
);
}
static void register_api_middleware(App &app)
{
app.use("/api", middleware::app::request_id_dev());
app.use("/api", middleware::app::timing_dev());
app.use("/api", middleware::app::security_headers_dev());
app.use("/api", middleware::app::rate_limit_dev());
}
static void register_routes(App &app)
{
app.get("/api/health", [](Request &, Response &res)
{
res.json({
"ok", true,
"service", "static-site"
});
});
}
int main()
{
App app;
configure_static_compression(false);
register_static_files(app);
register_api_middleware(app);
register_routes(app);
app.run(8080);
return 0;
}Complete test flow
Run:
vix run static_site.cppHome page:
curl -i http://127.0.0.1:8080/Static CSS:
curl -i http://127.0.0.1:8080/style.cssStatic JS:
curl -i http://127.0.0.1:8080/app.jsAPI health:
curl -i http://127.0.0.1:8080/api/healthSPA fallback test, only when enabled:
curl -i http://127.0.0.1:8080/dashboardExpected result with SPA fallback enabled:
public/index.htmlSummary
Use app.static_dir(...) to serve public files.
app.static_dir(
"public",
"/",
"index.html",
true,
"public, max-age=3600",
true,
true
);Remember the separation:
Core App
serves static files
Middleware
protects API routes
can optionally enhance static responses
Static response hook
can compress eligible static filesA good structure is:
register static files
register API middleware
register API routes
run appStatic files are not middleware.
They are a core vix::App feature.