Vix.cpp v2.7.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

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

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())

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

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;
}

Run it

bash
vix run json_api.cpp

The server listens on:

txt
http://127.0.0.1:8080

Test the health route

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

Expected body shape:

json
{
  "ok": true,
  "service": "json-api"
}

This route uses the simplest JSON response style:

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

Use this style for small responses.

Test the users list

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

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
    }
  ]
}

This route builds a payload first:

cpp
using namespace vix::json;

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

res.json(payload);

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

Expected body shape:

json
{
  "ok": true,
  "user": {
    "id": 1,
    "name": "Ada",
    "email": "ada@example.com",
    "active": true
  }
}

Request a missing user:

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

Expected status:

txt
404 Not Found

Expected body shape:

json
{
  "ok": false,
  "error": "User not found"
}

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}'

Expected status:

txt
201 Created

Expected body shape:

json
{
  "ok": true,
  "user": {
    "id": 3,
    "name": "Grace",
    "email": "grace@example.com",
    "active": true
  }
}

The route reads parsed JSON from request state:

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

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
));

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");

And reads an optional field with a default:

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

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"}'

Expected status:

txt
422 Unprocessable Entity

Expected body shape:

json
{
  "ok": false,
  "error": "Missing required field",
  "field": "name"
}

Missing email:

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

Expected status:

txt
422 Unprocessable Entity

Expected body shape:

json
{
  "ok": false,
  "error": "Missing required field",
  "field": "email"
}

Invalid JSON:

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

Expected status:

txt
400 Bad Request

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"}'

Expected status:

txt
415 Unsupported Media Type

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;
}

Test it:

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

Expected body shape:

json
{
  "ok": true,
  "body": {
    "message": "Hello"
  }
}

Invalid JSON:

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

Expected status:

txt
400 Bad Request

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

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"
});

Good for:

txt
health checks
simple errors
small success payloads
quick examples

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);

Good for:

txt
nested responses
response helper functions
test fixtures
generated metadata
configuration JSON
readable C++ JSON construction

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);

Good for:

txt
loops
conditional fields
computed fields
response building across multiple steps

4. Parse with middleware

Use parser middleware for normal request bodies.

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

Then read:

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

Good for:

txt
public API routes
POST routes
PUT routes
PATCH routes
routes where invalid JSON should stop before handler logic

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;
}

Good for:

txt
custom parse errors
optional JSON body
mixed content routes
small isolated examples
manual validation flows

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;
}

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);

Good for:

txt
validation
pagination defaults
optional filters
safe field access from external input

7. Request JSON cache

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

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

and:

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

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

or:

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

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
});

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
  });
});

The recommended default for backend APIs is middleware parsing.

Complete test flow

Run the server:

bash
vix run json_api.cpp

Health:

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

List users:

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

Get one user:

bash
curl -i http://127.0.0.1:8080/api/users/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"}'

Invalid body:

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

Manual echo:

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

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(...)

Use direct JSON for small responses:

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

Use builders for structured responses:

cpp
using namespace vix::json;

res.json(o(
  "ok", true,
  "items", a("one", "two")
));

Use parser middleware for normal request bodies:

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

Use manual parsing when the route needs custom control:

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

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

Released under the MIT License.