Middleware API
This example shows how to build a small but serious API with Vix middleware.
The goal is to demonstrate a realistic middleware stack around API routes:
recovery
request id
timing
security headers
CORS
rate limit
body limit
strict JSON parsing
route handlersThis is the kind of structure you can use as a base for a real backend.
What this example builds
The API exposes:
GET /api/health
GET /api/products
POST /api/productsThe middleware stack protects /api and parses JSON for POST /api/products.
/api
recovery
request id
timing
security headers
CORS
rate limit
body limit
/api/products
strict JSON parserSource
Create a file:
middleware_api.cppAdd this code:
#include <string>
#include <vector>
#include <vix.hpp>
#include <vix/middleware.hpp>
#include <vix/json.hpp>
using namespace vix;
struct Product
{
int id;
std::string name;
double price;
bool available;
};
static std::vector<Product> products{
{1, "Laptop", 999.99, true},
{2, "Phone", 499.50, true}
};
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,
"available", product.available
);
}
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;
}
static void install_api_middleware(App &app)
{
app.use("/api", middleware::app::recovery_dev());
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::cors_dev({
"http://localhost:5173",
"http://127.0.0.1:5173",
"https://example.com"
}));
app.use("/api", middleware::app::rate_limit_custom_dev(
60.0,
1.0,
"x-forwarded-for"
));
app.use("/api", middleware::app::body_limit_write_dev(
1024 * 1024
));
app.use("/api/products", middleware::app::json_strict_dev(
4096,
false,
true
));
}
static void register_options_routes(App &app)
{
app.options("/api/health", [](Request &, Response &res)
{
res.status(204).send();
});
app.options("/api/products", [](Request &, Response &res)
{
res.status(204).send();
});
}
static void register_routes(App &app)
{
app.get("/", [](Request &, Response &res)
{
res.text("Middleware API example. Try /api/health or /api/products.");
});
app.get("/api/health", [](Request &req, Response &res)
{
auto *request_id =
req.try_state<middleware::basics::RequestId>();
auto *timing =
req.try_state<middleware::basics::Timing>();
res.json({
"ok", true,
"service", "middleware-api",
"request_id", request_id ? request_id->value : "",
"duration_ms", timing ? timing->total_ms : 0
});
});
app.get("/api/products", [](Request &, Response &res)
{
using namespace vix::json;
res.json(o(
"ok", true,
"products", products_to_json()
));
});
app.post("/api/products", [](Request &req, Response &res)
{
using namespace vix::json;
auto &body = req.state<middleware::parsers::JsonBody>();
auto name = get_opt<std::string>(body.value, "name");
const double price = get_or<double>(body.value, "price", 0.0);
const bool available = get_or<bool>(body.value, "available", true);
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,
available
});
res.status(201).json(o(
"ok", true,
"product", product_to_json(products.back())
));
});
}
int main()
{
App app;
install_api_middleware(app);
register_options_routes(app);
register_routes(app);
app.run(8080);
return 0;
}Run it
vix run middleware_api.cppThe server listens on:
http://127.0.0.1:8080Test the public route
curl -i http://127.0.0.1:8080/Expected body:
Middleware API example. Try /api/health or /api/products.The public route is not under /api, so the API middleware stack does not apply to it.
Test the health route
curl -i http://127.0.0.1:8080/api/healthExpected body shape:
{
"ok": true,
"service": "middleware-api",
"request_id": "...",
"duration_ms": 0
}Expected response 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: ...This shows that the middleware stack is active.
Test the products list
curl -i http://127.0.0.1:8080/api/productsExpected body shape:
{
"ok": true,
"products": [
{
"id": 1,
"name": "Laptop",
"price": 999.99,
"available": true
},
{
"id": 2,
"name": "Phone",
"price": 499.5,
"available": true
}
]
}This route uses JSON builders:
using namespace vix::json;
res.json(o(
"ok", true,
"products", products_to_json()
));Use builders when responses become nested or when helper functions return JSON values.
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,"available":true}'Expected status:
201 CreatedExpected body shape:
{
"ok": true,
"product": {
"id": 3,
"name": "Tablet",
"price": 299.99,
"available": true
}
}The JSON body is parsed by middleware:
app.use("/api/products", middleware::app::json_strict_dev(
4096,
false,
true
));The handler reads the parsed body:
auto &body = req.state<middleware::parsers::JsonBody>();Then it validates application fields.
Test validation errors
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 EntityExpected body shape:
{
"ok": false,
"error": "Missing required field",
"field": "name"
}Invalid price:
curl -i \
-X POST http://127.0.0.1:8080/api/products \
-H "Content-Type: application/json" \
-d '{"name":"Broken product","price":0}'Expected status:
422 Unprocessable EntityExpected body shape:
{
"ok": false,
"error": "Price must be greater than zero",
"field": "price"
}The parser validates JSON format.
The handler validates business rules.
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 handler is not called.
The JSON parser middleware rejects the request before route logic runs.
Test wrong Content-Type
curl -i \
-X POST http://127.0.0.1:8080/api/products \
-H "Content-Type: text/plain" \
-d '{"name":"Tablet","price":299.99}'Expected status:
415 Unsupported Media TypeThe strict JSON parser requires:
Content-Type: application/jsonor a compatible JSON content type such as:
application/json; charset=utf-8Test body limit
The API has a broad body limit:
app.use("/api", middleware::app::body_limit_write_dev(
1024 * 1024
));And a smaller JSON parser limit for products:
app.use("/api/products", middleware::app::json_strict_dev(
4096,
false,
true
));Test a large JSON body:
BIG="$(python3 -c 'print("{\"name\":\"" + "x"*5000 + "\",\"price\":10}")')"
curl -i \
-X POST http://127.0.0.1:8080/api/products \
-H "Content-Type: application/json" \
-d "$BIG"Expected status:
413 Payload Too LargeThe broad body limit protects the API group.
The parser limit protects the specific JSON route.
Test CORS preflight
Allowed 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 ContentExpected CORS header:
Access-Control-Allow-Origin: http://localhost:5173Blocked 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 ForbiddenCORS is installed before the JSON parser so browser preflight requests can be handled before body parsing.
Test rate limit
The demo stack uses:
app.use("/api", middleware::app::rate_limit_custom_dev(
60.0,
1.0,
"x-forwarded-for"
));For normal testing, 60 requests is high.
To see rate limiting quickly, temporarily change it to:
app.use("/api", middleware::app::rate_limit_custom_dev(
5.0,
0.0,
"x-forwarded-for"
));Then run:
for i in $(seq 1 6); do
echo "---- $i"
curl -i \
http://127.0.0.1:8080/api/health \
-H "x-forwarded-for: 10.0.0.1"
doneExpected sixth response:
429 Too Many RequestsRate limiting should happen before expensive parsing and handler work.
Middleware order
The middleware installation function is:
static void install_api_middleware(App &app)
{
app.use("/api", middleware::app::recovery_dev());
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::cors_dev({
"http://localhost:5173",
"http://127.0.0.1:5173",
"https://example.com"
}));
app.use("/api", middleware::app::rate_limit_custom_dev(
60.0,
1.0,
"x-forwarded-for"
));
app.use("/api", middleware::app::body_limit_write_dev(
1024 * 1024
));
app.use("/api/products", middleware::app::json_strict_dev(
4096,
false,
true
));
}The order is intentional.
recovery
catches unhandled exceptions
request id
gives every request an identifier
timing
measures downstream work
security headers
hardens responses
CORS
handles browser origin checks and preflight
rate limit
rejects abusive clients early
body limit
rejects oversized bodies before parsing
JSON parser
parses only the route that expects JSONThe handler should be the last part of the flow.
Why JSON parser is not global
This example installs the JSON parser on:
app.use("/api/products", middleware::app::json_strict_dev(...));not on:
app.use("/api", middleware::app::json_strict_dev(...));That is important.
GET /api/health and GET /api/products do not need JSON request bodies.
If a strict parser is installed globally, routes without request bodies may be rejected unnecessarily.
Use route-specific parser prefixes.
Recovery
The recovery middleware protects the server from uncaught exceptions escaping the pipeline.
app.use("/api", middleware::app::recovery_dev());It is installed early so downstream middleware and route handlers are protected.
In production, avoid returning internal exception details to clients.
Request id
Request id middleware adds a stable identifier for each request.
app.use("/api", middleware::app::request_id_dev());It stores:
middleware::basics::RequestIdRead it inside a handler:
auto *request_id =
req.try_state<middleware::basics::RequestId>();It can also be written to the response header.
Request ids are useful for logs, debugging, support, and tracing.
Timing
Timing middleware measures the time spent downstream.
app.use("/api", middleware::app::timing_dev());It stores:
middleware::basics::TimingRead it inside a handler:
auto *timing =
req.try_state<middleware::basics::Timing>();It can also emit headers such as:
x-response-time
server-timingSecurity headers
Security headers middleware adds browser hardening headers.
app.use("/api", middleware::app::security_headers_dev());Typical headers include:
X-Content-Type-Options
X-Frame-Options
Referrer-Policy
Permissions-PolicyUse it on API and web routes where browser behavior matters.
CORS
CORS controls which browser origins can call the API.
app.use("/api", middleware::app::cors_dev({
"http://localhost:5173",
"http://127.0.0.1:5173",
"https://example.com"
}));CORS should run before parsers.
Preflight requests are OPTIONS requests and usually have no body.
Rate limit
Rate limiting protects the API from too many requests from the same client key.
app.use("/api", middleware::app::rate_limit_custom_dev(
60.0,
1.0,
"x-forwarded-for"
));The values mean:
capacity = 60
refill_per_sec = 1
key header = x-forwarded-forFor production behind a proxy, make sure the proxy controls forwarding headers.
Do not blindly trust client-provided X-Forwarded-For when your app is exposed directly.
Body limit
Body limit protects the API before parsers run.
app.use("/api", middleware::app::body_limit_write_dev(
1024 * 1024
));It should be installed before JSON, form, or multipart parsers.
This prevents large invalid requests from reaching expensive parsing logic.
JSON parser
The strict JSON parser is installed only where it is needed.
app.use("/api/products", middleware::app::json_strict_dev(
4096,
false,
true
));It checks:
Content-Type
empty body
maximum size
JSON validityThen it stores:
middleware::parsers::JsonBodyThe handler reads it with:
auto &body = req.state<middleware::parsers::JsonBody>();Complete request flow
For POST /api/products, the flow is:
request
-> recovery
-> request id
-> timing
-> security headers
-> CORS
-> rate limit
-> body limit
-> strict JSON parser
-> handler
-> responseIf any middleware rejects the request, the handler is not called.
Examples:
invalid origin
CORS returns 403
too many requests
rate limit returns 429
body too large
body limit or parser returns 413
wrong content type
JSON parser returns 415
invalid JSON
JSON parser returns 400
business validation failed
handler returns 422Why this example matters
A real backend should not put everything inside route handlers.
Bad route design:
handler parses JSON
handler checks body size
handler checks CORS
handler rate limits
handler validates business fields
handler writes responseBetter route design:
middleware handles reusable HTTP concerns
handler handles application rulesThat is what this example demonstrates.
Summary
This example shows a practical API stack:
app.use("/api", middleware::app::recovery_dev());
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::cors_dev(...));
app.use("/api", middleware::app::rate_limit_dev());
app.use("/api", middleware::app::body_limit_write_dev(...));
app.use("/api/products", middleware::app::json_strict_dev(...));The rule is:
middleware protects and prepares the request
handlers implement application behaviorUse this structure when you want a small Vix API that already looks like a real backend.