JSON API
This example shows how to build a small JSON API with Vix.
It covers the most common JSON patterns:
return JSON directly with res.json(...)
build JSON with vix::json::o(...) and vix::json::a(...)
parse JSON automatically with middleware::app::json_strict_dev(...)
read parsed JSON from JsonBody
validate required fields with get_opt(...)
use defaults with get_or(...)
parse manually with try_loads(...) when you do not want middleware2
3
4
5
6
7
Vix gives you more than one way to work with JSON because different routes need different levels of control.
What this example builds
The API exposes:
GET /api/health
GET /api/users
GET /api/users/{id}
POST /api/users
POST /api/echo-manual2
3
4
5
The example demonstrates two JSON request-body styles:
middleware parsing
app.use("/api/users", middleware::app::json_strict_dev(...))
manual parsing
vix::json::try_loads(req.body())2
3
4
5
Use middleware parsing for normal API routes.
Use manual parsing when a route needs full control over parsing behavior.
Source
Create a file:
json_api.cppAdd this code:
#include <string>
#include <vector>
#include <vix.hpp>
#include <vix/middleware.hpp>
#include <vix/json.hpp>
using namespace vix;
struct User
{
int id;
std::string name;
std::string email;
bool active;
};
static std::vector<User> users{
{1, "Ada", "ada@example.com", true},
{2, "Linus", "linus@example.com", true}
};
static vix::json::Json user_to_json(const User &user)
{
using namespace vix::json;
return o(
"id", user.id,
"name", user.name,
"email", user.email,
"active", user.active
);
}
static vix::json::Json users_to_json()
{
using namespace vix::json;
Json items = arr();
for (const auto &user : users)
{
items.push_back(user_to_json(user));
}
return items;
}
static const User *find_user(int id)
{
for (const auto &user : users)
{
if (user.id == id)
return &user;
}
return nullptr;
}
static void register_routes(App &app)
{
app.get("/api/health", [](Request &, Response &res)
{
res.json({
"ok", true,
"service", "json-api"
});
});
app.get("/api/users", [](Request &, Response &res)
{
using namespace vix::json;
Json payload = o(
"ok", true,
"users", users_to_json()
);
res.json(payload);
});
app.get("/api/users/{id}", [](Request &req, Response &res)
{
using namespace vix::json;
const int id = std::stoi(req.param("id"));
const User *user = find_user(id);
if (!user)
{
res.status(404).json({
"ok", false,
"error", "User not found"
});
return;
}
res.json(o(
"ok", true,
"user", user_to_json(*user)
));
});
app.post("/api/users", [](Request &req, Response &res)
{
using namespace vix::json;
auto &body = req.state<middleware::parsers::JsonBody>();
auto name = get_opt<std::string>(body.value, "name");
auto email = get_opt<std::string>(body.value, "email");
const bool active = get_or<bool>(body.value, "active", true);
if (!name || name->empty())
{
res.status(422).json({
"ok", false,
"error", "Missing required field",
"field", "name"
});
return;
}
if (!email || email->empty())
{
res.status(422).json({
"ok", false,
"error", "Missing required field",
"field", "email"
});
return;
}
const int next_id = users.empty() ? 1 : users.back().id + 1;
users.push_back(User{
next_id,
*name,
*email,
active
});
res.status(201).json(o(
"ok", true,
"user", user_to_json(users.back())
));
});
app.post("/api/echo-manual", [](Request &req, Response &res)
{
using namespace vix::json;
auto body = try_loads(req.body());
if (!body)
{
res.status(400).json({
"ok", false,
"error", "Invalid JSON"
});
return;
}
res.json(o(
"ok", true,
"body", *body
));
});
}
int main()
{
App app;
app.use("/api", middleware::app::security_headers_dev());
app.use("/api", middleware::app::body_limit_write_dev(1024 * 1024));
app.use("/api/users", middleware::app::json_strict_dev(
4096,
false,
true
));
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
183
184
185
186
187
188
189
Run it
vix run json_api.cppThe server listens on:
http://127.0.0.1:8080Test the health route
curl -i http://127.0.0.1:8080/api/healthExpected body shape:
{
"ok": true,
"service": "json-api"
}2
3
4
This route uses the simplest JSON response style:
res.json({
"ok", true,
"service", "json-api"
});2
3
4
Use this style for small responses.
Test the users list
curl -i http://127.0.0.1:8080/api/usersExpected body shape:
{
"ok": true,
"users": [
{
"id": 1,
"name": "Ada",
"email": "ada@example.com",
"active": true
},
{
"id": 2,
"name": "Linus",
"email": "linus@example.com",
"active": true
}
]
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
This route builds a payload first:
using namespace vix::json;
Json payload = o(
"ok", true,
"users", users_to_json()
);
res.json(payload);2
3
4
5
6
7
8
Use this style when the JSON response is larger or reused by helper functions.
Test one user
curl -i http://127.0.0.1:8080/api/users/1Expected body shape:
{
"ok": true,
"user": {
"id": 1,
"name": "Ada",
"email": "ada@example.com",
"active": true
}
}2
3
4
5
6
7
8
9
Request a missing user:
curl -i http://127.0.0.1:8080/api/users/999Expected status:
404 Not FoundExpected body shape:
{
"ok": false,
"error": "User not found"
}2
3
4
Create a user
curl -i \
-X POST http://127.0.0.1:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"Grace","email":"grace@example.com","active":true}'2
3
4
Expected status:
201 CreatedExpected body shape:
{
"ok": true,
"user": {
"id": 3,
"name": "Grace",
"email": "grace@example.com",
"active": true
}
}2
3
4
5
6
7
8
9
The route reads parsed JSON from request state:
auto &body = req.state<middleware::parsers::JsonBody>();That state exists because the route prefix installed the JSON parser middleware:
app.use("/api/users", middleware::app::json_strict_dev(
4096,
false,
true
));2
3
4
5
The route then validates required fields:
auto name = get_opt<std::string>(body.value, "name");
auto email = get_opt<std::string>(body.value, "email");2
And reads an optional field with a default:
const bool active = get_or<bool>(body.value, "active", true);Test validation
Missing name:
curl -i \
-X POST http://127.0.0.1:8080/api/users \
-H "Content-Type: application/json" \
-d '{"email":"missing-name@example.com"}'2
3
4
Expected status:
422 Unprocessable EntityExpected body shape:
{
"ok": false,
"error": "Missing required field",
"field": "name"
}2
3
4
5
Missing email:
curl -i \
-X POST http://127.0.0.1:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"Grace"}'2
3
4
Expected status:
422 Unprocessable EntityExpected body shape:
{
"ok": false,
"error": "Missing required field",
"field": "email"
}2
3
4
5
Invalid JSON:
curl -i \
-X POST http://127.0.0.1:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":}'2
3
4
Expected status:
400 Bad RequestWrong content type:
curl -i \
-X POST http://127.0.0.1:8080/api/users \
-H "Content-Type: text/plain" \
-d '{"name":"Grace","email":"grace@example.com"}'2
3
4
Expected status:
415 Unsupported Media TypeThe JSON parser middleware stops invalid requests before the handler runs.
Manual JSON parsing route
The route below does not use JsonBody.
It parses the body manually:
auto body = try_loads(req.body());
if (!body)
{
res.status(400).json({
"ok", false,
"error", "Invalid JSON"
});
return;
}2
3
4
5
6
7
8
9
10
Test it:
curl -i \
-X POST http://127.0.0.1:8080/api/echo-manual \
-H "Content-Type: application/json" \
-d '{"message":"Hello"}'2
3
4
Expected body shape:
{
"ok": true,
"body": {
"message": "Hello"
}
}2
3
4
5
6
Invalid JSON:
curl -i \
-X POST http://127.0.0.1:8080/api/echo-manual \
-H "Content-Type: application/json" \
-d 'not-json'2
3
4
Expected status:
400 Bad RequestUse manual parsing when:
the route needs special parsing behavior
you want custom error messages
you do not want middleware on that route
you parse optional or mixed body formats2
3
4
For normal JSON APIs, prefer parser middleware.
The different JSON styles
Vix gives you several JSON styles.
They are not competitors.
They are for different cases.
1. Direct res.json({...})
Use this for small responses.
res.json({
"ok", true,
"service", "api"
});2
3
4
Good for:
health checks
simple errors
small success payloads
quick examples2
3
4
2. Build JSON with o() and a()
Use vix::json::o() and vix::json::a() when the response is bigger or nested.
using namespace vix::json;
Json payload = o(
"ok", true,
"user", o(
"id", 1,
"name", "Ada"
),
"roles", a("admin", "editor")
);
res.json(payload);2
3
4
5
6
7
8
9
10
11
12
Good for:
nested responses
response helper functions
test fixtures
generated metadata
configuration JSON
readable C++ JSON construction2
3
4
5
6
3. Build progressively with obj() and arr()
Use obj() and arr() when fields are added in steps.
using namespace vix::json;
Json payload = obj();
payload["ok"] = true;
payload["count"] = users.size();
Json items = arr();
for (const auto &user : users)
{
items.push_back(user_to_json(user));
}
payload["users"] = items;
res.json(payload);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Good for:
loops
conditional fields
computed fields
response building across multiple steps2
3
4
4. Parse with middleware
Use parser middleware for normal request bodies.
app.use("/api/users", middleware::app::json_strict_dev(4096));Then read:
auto &body = req.state<middleware::parsers::JsonBody>();Good for:
public API routes
POST routes
PUT routes
PATCH routes
routes where invalid JSON should stop before handler logic2
3
4
5
5. Parse manually with try_loads()
Use manual parsing when you need full control.
using namespace vix::json;
auto body = try_loads(req.body());
if (!body)
{
res.status(400).json({
"ok", false,
"error", "Invalid JSON"
});
return;
}2
3
4
5
6
7
8
9
10
11
12
Good for:
custom parse errors
optional JSON body
mixed content routes
small isolated examples
manual validation flows2
3
4
5
6. Read fields safely
Use get_opt<T>() when a required field must be validated.
auto name = get_opt<std::string>(body, "name");
if (!name || name->empty())
{
res.status(422).json({
"ok", false,
"error", "Missing required field",
"field", "name"
});
return;
}2
3
4
5
6
7
8
9
10
11
Use get_or<T>() when an optional field has a default value.
const int page = get_or<int>(body, "page", 1);
const int limit = get_or<int>(body, "limit", 20);2
Good for:
validation
pagination defaults
optional filters
safe field access from external input2
3
4
7. Request JSON cache
Some Vix request APIs can expose JSON-oriented helpers such as:
const vix::json::Json &data = req.json();and:
auto value = req.json_as<MyType>();Use this only when the route or middleware design already guarantees that the body is valid JSON, or when exception-based behavior is intentional.
For public endpoints, prefer one of these two patterns:
middleware::app::json_strict_dev(...)or:
vix::json::try_loads(req.body())They make bad client input easier to handle cleanly.
Which JSON style should I use?
Use this rule:
| Need | Best style |
|---|---|
| Small response | res.json({...}) |
| Nested response | vix::json::o() and vix::json::a() |
| Conditional response | vix::json::obj() and vix::json::arr() |
| Normal JSON request body | middleware::app::json_strict_dev(...) |
| Custom parsing behavior | vix::json::try_loads(req.body()) |
| Required field validation | get_opt<T>() |
| Optional field defaults | get_or<T>() |
| Internal already-validated request body | req.json() or req.json_as<T>() |
Middleware parsing vs manual parsing
Use middleware parsing for clean APIs:
app.use("/api/users", middleware::app::json_strict_dev(4096));
app.post("/api/users", [](Request &req, Response &res)
{
auto &body = req.state<middleware::parsers::JsonBody>();
// business validation here
});2
3
4
5
6
7
8
Use manual parsing for route-level control:
app.post("/api/echo", [](Request &req, Response &res)
{
auto body = vix::json::try_loads(req.body());
if (!body)
{
res.status(400).json({
"ok", false,
"error", "Invalid JSON"
});
return;
}
res.json({
"ok", true
});
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
The recommended default for backend APIs is middleware parsing.
Complete test flow
Run the server:
vix run json_api.cppHealth:
curl -i http://127.0.0.1:8080/api/healthList users:
curl -i http://127.0.0.1:8080/api/usersGet one user:
curl -i http://127.0.0.1:8080/api/users/1Create user:
curl -i \
-X POST http://127.0.0.1:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"Grace","email":"grace@example.com"}'2
3
4
Invalid body:
curl -i \
-X POST http://127.0.0.1:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":}'2
3
4
Manual echo:
curl -i \
-X POST http://127.0.0.1:8080/api/echo-manual \
-H "Content-Type: application/json" \
-d '{"message":"Hello"}'2
3
4
Summary
A Vix JSON API usually follows this shape:
middleware validates and parses the request body
handler reads JsonBody
handler validates business fields
handler builds response JSON
handler sends res.json(...)2
3
4
5
Use direct JSON for small responses:
res.json({
"ok", true
});2
3
Use builders for structured responses:
using namespace vix::json;
res.json(o(
"ok", true,
"items", a("one", "two")
));2
3
4
5
6
Use parser middleware for normal request bodies:
app.use("/api/users", middleware::app::json_strict_dev(4096));Use manual parsing when the route needs custom control:
auto body = vix::json::try_loads(req.body());The recommended default is:
json_strict_dev for request bodies
get_opt for required fields
get_or for optional fields
o/a for structured responses
res.json for sending2
3
4
5