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 handlers2
3
4
5
6
7
8
9
This 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/products2
3
The 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 parser2
3
4
5
6
7
8
9
10
11
Source
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;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
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
}2
3
4
5
6
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: ...2
3
4
5
6
7
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
}
]
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
This route uses JSON builders:
using namespace vix::json;
res.json(o(
"ok", true,
"products", products_to_json()
));2
3
4
5
6
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}'2
3
4
Expected status:
201 CreatedExpected body shape:
{
"ok": true,
"product": {
"id": 3,
"name": "Tablet",
"price": 299.99,
"available": true
}
}2
3
4
5
6
7
8
9
The JSON body is parsed by middleware:
app.use("/api/products", middleware::app::json_strict_dev(
4096,
false,
true
));2
3
4
5
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}'2
3
4
Expected status:
422 Unprocessable EntityExpected body shape:
{
"ok": false,
"error": "Missing required field",
"field": "name"
}2
3
4
5
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}'2
3
4
Expected status:
422 Unprocessable EntityExpected body shape:
{
"ok": false,
"error": "Price must be greater than zero",
"field": "price"
}2
3
4
5
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":}'2
3
4
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}'2
3
4
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
));2
3
And a smaller JSON parser limit for products:
app.use("/api/products", middleware::app::json_strict_dev(
4096,
false,
true
));2
3
4
5
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"2
3
4
5
6
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"2
3
4
5
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"2
3
4
5
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"
));2
3
4
5
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"
));2
3
4
5
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"
done2
3
4
5
6
Expected 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
));
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 JSON2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
The 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>();2
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>();2
It can also emit headers such as:
x-response-time
server-timing2
Security 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-Policy2
3
4
Use 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"
}));2
3
4
5
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"
));2
3
4
5
The values mean:
capacity = 60
refill_per_sec = 1
key header = x-forwarded-for2
3
For 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
));2
3
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
));2
3
4
5
It checks:
Content-Type
empty body
maximum size
JSON validity2
3
4
Then 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
-> response2
3
4
5
6
7
8
9
10
11
If 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 4222
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Why 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 response2
3
4
5
6
Better route design:
middleware handles reusable HTTP concerns
handler handles application rules2
That 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(...));2
3
4
5
6
7
8
The rule is:
middleware protects and prepares the request
handlers implement application behavior2
Use this structure when you want a small Vix API that already looks like a real backend.