Vix.cpp v2.6.0 is here Read the blog
Skip to content

JSON API

This example shows how to build a small JSON API with Vix.

It covers the most common JSON patterns:

txt
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 middleware
1
2
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:

txt
GET  /api/health
GET  /api/users
GET  /api/users/{id}
POST /api/users
POST /api/echo-manual
1
2
3
4
5

The example demonstrates two JSON request-body styles:

txt
middleware parsing
  app.use("/api/users", middleware::app::json_strict_dev(...))

manual parsing
  vix::json::try_loads(req.body())
1
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:

txt
json_api.cpp
1

Add this code:

cpp
#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;
}
1
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

bash
vix run json_api.cpp
1

The server listens on:

txt
http://127.0.0.1:8080
1

Test the health route

bash
curl -i http://127.0.0.1:8080/api/health
1

Expected body shape:

json
{
  "ok": true,
  "service": "json-api"
}
1
2
3
4

This route uses the simplest JSON response style:

cpp
res.json({
  "ok", true,
  "service", "json-api"
});
1
2
3
4

Use this style for small responses.

Test the users list

bash
curl -i http://127.0.0.1:8080/api/users
1

Expected body shape:

json
{
  "ok": true,
  "users": [
    {
      "id": 1,
      "name": "Ada",
      "email": "ada@example.com",
      "active": true
    },
    {
      "id": 2,
      "name": "Linus",
      "email": "linus@example.com",
      "active": true
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

This route builds a payload first:

cpp
using namespace vix::json;

Json payload = o(
  "ok", true,
  "users", users_to_json()
);

res.json(payload);
1
2
3
4
5
6
7
8

Use this style when the JSON response is larger or reused by helper functions.

Test one user

bash
curl -i http://127.0.0.1:8080/api/users/1
1

Expected body shape:

json
{
  "ok": true,
  "user": {
    "id": 1,
    "name": "Ada",
    "email": "ada@example.com",
    "active": true
  }
}
1
2
3
4
5
6
7
8
9

Request a missing user:

bash
curl -i http://127.0.0.1:8080/api/users/999
1

Expected status:

txt
404 Not Found
1

Expected body shape:

json
{
  "ok": false,
  "error": "User not found"
}
1
2
3
4

Create a user

bash
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}'
1
2
3
4

Expected status:

txt
201 Created
1

Expected body shape:

json
{
  "ok": true,
  "user": {
    "id": 3,
    "name": "Grace",
    "email": "grace@example.com",
    "active": true
  }
}
1
2
3
4
5
6
7
8
9

The route reads parsed JSON from request state:

cpp
auto &body = req.state<middleware::parsers::JsonBody>();
1

That state exists because the route prefix installed the JSON parser middleware:

cpp
app.use("/api/users", middleware::app::json_strict_dev(
  4096,
  false,
  true
));
1
2
3
4
5

The route then validates required fields:

cpp
auto name = get_opt<std::string>(body.value, "name");
auto email = get_opt<std::string>(body.value, "email");
1
2

And reads an optional field with a default:

cpp
const bool active = get_or<bool>(body.value, "active", true);
1

Test validation

Missing name:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"email":"missing-name@example.com"}'
1
2
3
4

Expected status:

txt
422 Unprocessable Entity
1

Expected body shape:

json
{
  "ok": false,
  "error": "Missing required field",
  "field": "name"
}
1
2
3
4
5

Missing email:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Grace"}'
1
2
3
4

Expected status:

txt
422 Unprocessable Entity
1

Expected body shape:

json
{
  "ok": false,
  "error": "Missing required field",
  "field": "email"
}
1
2
3
4
5

Invalid JSON:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":}'
1
2
3
4

Expected status:

txt
400 Bad Request
1

Wrong content type:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/users \
  -H "Content-Type: text/plain" \
  -d '{"name":"Grace","email":"grace@example.com"}'
1
2
3
4

Expected status:

txt
415 Unsupported Media Type
1

The 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:

cpp
auto body = try_loads(req.body());

if (!body)
{
  res.status(400).json({
    "ok", false,
    "error", "Invalid JSON"
  });
  return;
}
1
2
3
4
5
6
7
8
9
10

Test it:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/echo-manual \
  -H "Content-Type: application/json" \
  -d '{"message":"Hello"}'
1
2
3
4

Expected body shape:

json
{
  "ok": true,
  "body": {
    "message": "Hello"
  }
}
1
2
3
4
5
6

Invalid JSON:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/echo-manual \
  -H "Content-Type: application/json" \
  -d 'not-json'
1
2
3
4

Expected status:

txt
400 Bad Request
1

Use manual parsing when:

txt
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 formats
1
2
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.

cpp
res.json({
  "ok", true,
  "service", "api"
});
1
2
3
4

Good for:

txt
health checks
simple errors
small success payloads
quick examples
1
2
3
4

2. Build JSON with o() and a()

Use vix::json::o() and vix::json::a() when the response is bigger or nested.

cpp
using namespace vix::json;

Json payload = o(
  "ok", true,
  "user", o(
    "id", 1,
    "name", "Ada"
  ),
  "roles", a("admin", "editor")
);

res.json(payload);
1
2
3
4
5
6
7
8
9
10
11
12

Good for:

txt
nested responses
response helper functions
test fixtures
generated metadata
configuration JSON
readable C++ JSON construction
1
2
3
4
5
6

3. Build progressively with obj() and arr()

Use obj() and arr() when fields are added in steps.

cpp
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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Good for:

txt
loops
conditional fields
computed fields
response building across multiple steps
1
2
3
4

4. Parse with middleware

Use parser middleware for normal request bodies.

cpp
app.use("/api/users", middleware::app::json_strict_dev(4096));
1

Then read:

cpp
auto &body = req.state<middleware::parsers::JsonBody>();
1

Good for:

txt
public API routes
POST routes
PUT routes
PATCH routes
routes where invalid JSON should stop before handler logic
1
2
3
4
5

5. Parse manually with try_loads()

Use manual parsing when you need full control.

cpp
using namespace vix::json;

auto body = try_loads(req.body());

if (!body)
{
  res.status(400).json({
    "ok", false,
    "error", "Invalid JSON"
  });
  return;
}
1
2
3
4
5
6
7
8
9
10
11
12

Good for:

txt
custom parse errors
optional JSON body
mixed content routes
small isolated examples
manual validation flows
1
2
3
4
5

6. Read fields safely

Use get_opt<T>() when a required field must be validated.

cpp
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;
}
1
2
3
4
5
6
7
8
9
10
11

Use get_or<T>() when an optional field has a default value.

cpp
const int page = get_or<int>(body, "page", 1);
const int limit = get_or<int>(body, "limit", 20);
1
2

Good for:

txt
validation
pagination defaults
optional filters
safe field access from external input
1
2
3
4

7. Request JSON cache

Some Vix request APIs can expose JSON-oriented helpers such as:

cpp
const vix::json::Json &data = req.json();
1

and:

cpp
auto value = req.json_as<MyType>();
1

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:

cpp
middleware::app::json_strict_dev(...)
1

or:

cpp
vix::json::try_loads(req.body())
1

They make bad client input easier to handle cleanly.

Which JSON style should I use?

Use this rule:

NeedBest style
Small responseres.json({...})
Nested responsevix::json::o() and vix::json::a()
Conditional responsevix::json::obj() and vix::json::arr()
Normal JSON request bodymiddleware::app::json_strict_dev(...)
Custom parsing behaviorvix::json::try_loads(req.body())
Required field validationget_opt<T>()
Optional field defaultsget_or<T>()
Internal already-validated request bodyreq.json() or req.json_as<T>()

Middleware parsing vs manual parsing

Use middleware parsing for clean APIs:

cpp
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
});
1
2
3
4
5
6
7
8

Use manual parsing for route-level control:

cpp
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
  });
});
1
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:

bash
vix run json_api.cpp
1

Health:

bash
curl -i http://127.0.0.1:8080/api/health
1

List users:

bash
curl -i http://127.0.0.1:8080/api/users
1

Get one user:

bash
curl -i http://127.0.0.1:8080/api/users/1
1

Create user:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Grace","email":"grace@example.com"}'
1
2
3
4

Invalid body:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":}'
1
2
3
4

Manual echo:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/echo-manual \
  -H "Content-Type: application/json" \
  -d '{"message":"Hello"}'
1
2
3
4

Summary

A Vix JSON API usually follows this shape:

txt
middleware validates and parses the request body
handler reads JsonBody
handler validates business fields
handler builds response JSON
handler sends res.json(...)
1
2
3
4
5

Use direct JSON for small responses:

cpp
res.json({
  "ok", true
});
1
2
3

Use builders for structured responses:

cpp
using namespace vix::json;

res.json(o(
  "ok", true,
  "items", a("one", "two")
));
1
2
3
4
5
6

Use parser middleware for normal request bodies:

cpp
app.use("/api/users", middleware::app::json_strict_dev(4096));
1

Use manual parsing when the route needs custom control:

cpp
auto body = vix::json::try_loads(req.body());
1

The recommended default is:

txt
json_strict_dev for request bodies
get_opt for required fields
get_or for optional fields
o/a for structured responses
res.json for sending
1
2
3
4
5

Released under the MIT License.

Morty Proxy This is a proxified and sanitized view of the page, visit original site.