Production Bootstrap
This example shows a production-style Vix application structure.
The goal is to keep main() small and move startup wiring into an application bootstrap layer.
This is useful when your app needs:
configuration from .env
server settings
public files
SPA fallback
optional static compression
API middleware
route registration
clean startup structureThis example is not a framework.
It is a clean way to organize a real Vix backend.
What this example builds
The app exposes:
GET /
GET /api/health
GET /api/products
POST /api/productsIt also serves static files from:
public/The app is organized like this:
main.cpp
calls AppBootstrap::run()
AppBootstrap
loads configuration
creates vix::App
configures static files
configures optional static compression
installs middleware
registers routes
starts the server
MiddlewareRegistry
installs API middleware
RouteRegistry
registers application routesProject structure
Create this structure:
production_bootstrap_demo/
├── .env
├── CMakeLists.txt
├── main.cpp
├── public/
│ ├── index.html
│ ├── app.js
│ └── style.css
└── src/
├── AppBootstrap.hpp
├── AppBootstrap.cpp
├── MiddlewareRegistry.hpp
├── MiddlewareRegistry.cpp
├── RouteRegistry.hpp
└── RouteRegistry.cppFor a larger project, you can move these into namespaces such as:
blog::app
blog::presentation::middleware
blog::presentation::routesThis example keeps names simple so the structure is easy to understand.
.env
Create:
.envAdd:
APP_NAME=production-bootstrap-demo
APP_ENV=development
SERVER_HOST=0.0.0.0
SERVER_PORT=8080
SERVER_REQUEST_TIMEOUT=5000
SERVER_IO_THREADS=0
SERVER_SESSION_TIMEOUT_SEC=20
SERVER_BENCH_MODE=false
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=1024
VIX_LOG_LEVEL=info
VIX_LOG_FORMAT=kvThe important static file values are:
PUBLIC_PATH
PUBLIC_MOUNT
PUBLIC_INDEX
PUBLIC_CACHE_CONTROL
PUBLIC_SPA_FALLBACK
PUBLIC_COMPRESSION
PUBLIC_COMPRESSION_MIN_SIZEpublic/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 Production Bootstrap</title>
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<main class="page">
<h1>Vix Production Bootstrap</h1>
<p>This page is served from the public directory.</p>
<button id="health-button">Check API health</button>
<button id="products-button">Load products</button>
<pre id="output">Click a 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: 760px;
margin: 80px auto;
padding: 32px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 16px;
}
button {
margin-right: 8px;
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 output = document.querySelector("#output");
async function showJson(url) {
const response = await fetch(url);
const data = await response.json();
output.textContent = JSON.stringify(data, null, 2);
}
document
.querySelector("#health-button")
.addEventListener("click", () => showJson("/api/health"));
document
.querySelector("#products-button")
.addEventListener("click", () => showJson("/api/products"));main.cpp
Create:
main.cppAdd:
#include "src/AppBootstrap.hpp"
int main()
{
return AppBootstrap::run();
}main() stays small.
It does not know about middleware, routes, static files, or configuration details.
src/AppBootstrap.hpp
Create:
src/AppBootstrap.hppAdd:
#ifndef APP_BOOTSTRAP_HPP
#define APP_BOOTSTRAP_HPP
class AppBootstrap
{
public:
static int run();
};
#endifsrc/AppBootstrap.cpp
Create:
src/AppBootstrap.cppAdd:
#include "AppBootstrap.hpp"
#include "MiddlewareRegistry.hpp"
#include "RouteRegistry.hpp"
#include <cstddef>
#include <string>
#include <vix.hpp>
#include <vix/middleware.hpp>
static std::string config_string(
vix::config::Config &cfg,
const std::string &key,
const std::string &fallback)
{
return cfg.getString(key, fallback);
}
static bool config_bool(
vix::config::Config &cfg,
const std::string &key,
bool fallback)
{
return cfg.getBool(key, fallback);
}
static int config_int(
vix::config::Config &cfg,
const std::string &key,
int fallback)
{
return cfg.getInt(key, fallback);
}
static void configure_static_compression(vix::config::Config &cfg)
{
const bool public_compression =
config_bool(cfg, "public.compression", false);
if (!public_compression)
return;
const int min_size =
config_int(cfg, "public.compression_min_size", 1024);
const auto options =
vix::middleware::performance::CompressionOptions{
.min_size = static_cast<std::size_t>(min_size),
.add_vary = true,
.enabled = true
};
vix::App::set_static_response_hook(
vix::middleware::performance::compressed_static_response_hook(options)
);
}
static void configure_static_files(
vix::App &app,
vix::config::Config &cfg)
{
const std::string public_path =
config_string(cfg, "public.path", "public");
const std::string public_mount =
config_string(cfg, "public.mount", "/");
const std::string public_index =
config_string(cfg, "public.index", "index.html");
const std::string public_cache_control =
config_string(cfg, "public.cache_control", "public, max-age=3600");
const bool public_spa_fallback =
config_bool(cfg, "public.spa_fallback", true);
app.static_dir(
public_path,
public_mount,
public_index,
true,
public_cache_control,
true,
public_spa_fallback
);
}
int AppBootstrap::run()
{
vix::config::Config cfg{".env"};
vix::App app;
configure_static_compression(cfg);
configure_static_files(app, cfg);
MiddlewareRegistry::register_all(app);
RouteRegistry::register_all(app);
app.run(cfg);
return 0;
}This file owns startup wiring.
It does four important things:
loads .env
configures optional static compression hook
configures static files through app.static_dir(...)
registers middleware and routes
runs the app from configStatic files are still Core App
This line serves static files:
app.static_dir(...);This line does not serve files:
vix::App::set_static_response_hook(...);The hook only enhances static responses after Core App has produced them.
Keep the model clear:
app.static_dir(...)
serves public files
set_static_response_hook(...)
optionally compresses eligible static responses
app.use(...)
installs middleware for route handlingsrc/MiddlewareRegistry.hpp
Create:
src/MiddlewareRegistry.hppAdd:
#ifndef MIDDLEWARE_REGISTRY_HPP
#define MIDDLEWARE_REGISTRY_HPP
#include <vix.hpp>
class MiddlewareRegistry
{
public:
static void register_all(vix::App &app);
};
#endifsrc/MiddlewareRegistry.cpp
Create:
src/MiddlewareRegistry.cppAdd:
#include "MiddlewareRegistry.hpp"
#include <vix/middleware.hpp>
void MiddlewareRegistry::register_all(vix::App &app)
{
app.use("/api", vix::middleware::app::recovery_dev());
app.use("/api", vix::middleware::app::request_id_dev());
app.use("/api", vix::middleware::app::timing_dev());
app.use("/api", vix::middleware::app::security_headers_dev());
app.use("/api", vix::middleware::app::cors_dev({
"http://localhost:5173",
"http://127.0.0.1:5173"
}));
app.use("/api", vix::middleware::app::rate_limit_custom_dev(
60.0,
1.0,
"x-forwarded-for"
));
app.use("/api", vix::middleware::app::body_limit_write_dev(
1024 * 1024
));
app.use("/api/products", vix::middleware::app::json_strict_dev(
4096,
false,
true
));
}This registry owns the API middleware stack.
The order is intentional:
recovery
request id
timing
security headers
CORS
rate limit
body limit
JSON parser for product writesThe JSON parser is installed only on /api/products, not globally on /api.
That avoids rejecting API routes that do not need a request body.
src/RouteRegistry.hpp
Create:
src/RouteRegistry.hppAdd:
#ifndef ROUTE_REGISTRY_HPP
#define ROUTE_REGISTRY_HPP
#include <vix.hpp>
class RouteRegistry
{
public:
static void register_all(vix::App &app);
};
#endifsrc/RouteRegistry.cpp
Create:
src/RouteRegistry.cppAdd:
#include "RouteRegistry.hpp"
#include <string>
#include <vector>
#include <vix/json.hpp>
#include <vix/middleware.hpp>
struct Product
{
int id;
std::string name;
double price;
};
static std::vector<Product> products{
{1, "Laptop", 999.99},
{2, "Phone", 499.50}
};
static vix::json::Json product_to_json(const Product &product)
{
using namespace vix::json;
return o(
"id", product.id,
"name", product.name,
"price", product.price
);
}
static vix::json::Json products_to_json()
{
using namespace vix::json;
Json items = arr();
for (const auto &product : products)
{
items.push_back(product_to_json(product));
}
return items;
}
void RouteRegistry::register_all(vix::App &app)
{
app.get("/api/health", [](vix::Request &req, vix::Response &res)
{
auto *request_id =
req.try_state<vix::middleware::basics::RequestId>();
res.json({
"ok", true,
"service", "production-bootstrap",
"request_id", request_id ? request_id->value : ""
});
});
app.get("/api/products", [](vix::Request &, vix::Response &res)
{
using namespace vix::json;
res.json(o(
"ok", true,
"products", products_to_json()
));
});
app.post("/api/products", [](vix::Request &req, vix::Response &res)
{
using namespace vix::json;
auto &body =
req.state<vix::middleware::parsers::JsonBody>();
auto name = get_opt<std::string>(body.value, "name");
const double price = get_or<double>(body.value, "price", 0.0);
if (!name || name->empty())
{
res.status(422).json({
"ok", false,
"error", "Missing required field",
"field", "name"
});
return;
}
if (price <= 0.0)
{
res.status(422).json({
"ok", false,
"error", "Price must be greater than zero",
"field", "price"
});
return;
}
const int next_id = products.empty() ? 1 : products.back().id + 1;
products.push_back(Product{
next_id,
*name,
price
});
res.status(201).json(o(
"ok", true,
"product", product_to_json(products.back())
));
});
}Routes are now separated from startup code.
This makes the application easier to grow.
CMakeLists.txt
Create:
CMakeLists.txtAdd:
cmake_minimum_required(VERSION 3.20)
project(production_bootstrap_demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(vix CONFIG REQUIRED)
add_executable(production_bootstrap_demo
main.cpp
src/AppBootstrap.cpp
src/MiddlewareRegistry.cpp
src/RouteRegistry.cpp
)
target_include_directories(production_bootstrap_demo
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
target_link_libraries(production_bootstrap_demo
PRIVATE
vix::vix
)If your local Vix package exposes different CMake targets, use the target name installed by your Vix setup.
The example itself focuses on application structure.
Run it with vix
From the project directory:
vix run main.cppFor multi-file projects, use your normal Vix or CMake workflow.
A common CMake flow is:
cmake -S . -B build
cmake --build build
./build/production_bootstrap_demoTest the static site
Open:
http://127.0.0.1:8080/Or use:
curl -i http://127.0.0.1:8080/Expected result:
public/index.htmlTest CSS:
curl -i http://127.0.0.1:8080/style.cssTest JavaScript:
curl -i http://127.0.0.1:8080/app.jsTest SPA fallback
Because .env has:
PUBLIC_SPA_FALLBACK=trueThis should return public/index.html:
curl -i http://127.0.0.1:8080/dashboardUse SPA fallback when your frontend has client-side routes.
Keep it false for a classic static site.
Test API health
curl -i http://127.0.0.1:8080/api/healthExpected body shape:
{
"ok": true,
"service": "production-bootstrap",
"request_id": "..."
}Expected headers may include:
x-request-id: ...
x-response-time: ...
server-timing: total;dur=...
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Permissions-Policy: ...Test products list
curl -i http://127.0.0.1:8080/api/productsExpected body shape:
{
"ok": true,
"products": [
{
"id": 1,
"name": "Laptop",
"price": 999.99
},
{
"id": 2,
"name": "Phone",
"price": 499.5
}
]
}Create a product
curl -i \
-X POST http://127.0.0.1:8080/api/products \
-H "Content-Type: application/json" \
-d '{"name":"Tablet","price":299.99}'Expected status:
201 CreatedExpected body shape:
{
"ok": true,
"product": {
"id": 3,
"name": "Tablet",
"price": 299.99
}
}Test invalid JSON
curl -i \
-X POST http://127.0.0.1:8080/api/products \
-H "Content-Type: application/json" \
-d '{"name":}'Expected status:
400 Bad RequestThe JSON parser rejects invalid JSON before the handler runs.
Test validation
Missing name:
curl -i \
-X POST http://127.0.0.1:8080/api/products \
-H "Content-Type: application/json" \
-d '{"price":299.99}'Expected status:
422 Unprocessable EntityInvalid price:
curl -i \
-X POST http://127.0.0.1:8080/api/products \
-H "Content-Type: application/json" \
-d '{"name":"Broken","price":0}'Expected status:
422 Unprocessable EntityThe parser validates JSON syntax.
The route validates business rules.
Test CORS preflight
Allowed local frontend origin:
curl -i \
-X OPTIONS http://127.0.0.1:8080/api/products \
-H "Origin: http://localhost:5173" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type"Expected status:
204 No ContentBlocked origin:
curl -i \
-X OPTIONS http://127.0.0.1:8080/api/products \
-H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type"Expected status:
403 ForbiddenWhy this structure matters
A small demo can put everything inside main().
A real backend should not.
This is not ideal for growing projects:
main()
load config
configure static files
install middleware
register all routes
start serverThis is better:
main()
AppBootstrap::run()
AppBootstrap
startup wiring
MiddlewareRegistry
middleware stack
RouteRegistry
routesThe result is easier to maintain.
Production notes
For a real deployment:
read secrets from environment or secure config
keep TLS termination clear
use Nginx or another reverse proxy when needed
control PUBLIC_CACHE_CONTROL carefully
enable PUBLIC_SPA_FALLBACK only for SPA frontends
enable PUBLIC_COMPRESSION only if Vix should compress static responses
avoid double compression if Nginx or CDN already compresses
keep API middleware separate from static file servingStatic files and API routes are different concerns.
static files
app.static_dir(...)
API middleware
app.use("/api", ...)
routes
app.get(...)
app.post(...)Summary
A production-style Vix app should keep startup clean.
Recommended shape:
main.cpp
calls AppBootstrap::run()
AppBootstrap
configures App
MiddlewareRegistry
installs middleware
RouteRegistry
registers routesStatic files are configured with:
app.static_dir(...);Static compression is optional and uses:
vix::App::set_static_response_hook(...);API middleware is installed with:
app.use("/api", ...);This structure gives you a clean base for a real Vix backend.