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

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:

txt
recovery
request id
timing
security headers
CORS
rate limit
body limit
strict JSON parsing
route handlers
1
2
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:

txt
GET  /api/health
GET  /api/products
POST /api/products
1
2
3

The middleware stack protects /api and parses JSON for POST /api/products.

txt
/api
  recovery
  request id
  timing
  security headers
  CORS
  rate limit
  body limit

/api/products
  strict JSON parser
1
2
3
4
5
6
7
8
9
10
11

Source

Create a file:

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

Run it

bash
vix run middleware_api.cpp
1

The server listens on:

txt
http://127.0.0.1:8080
1

Test the public route

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

Expected body:

txt
Middleware API example. Try /api/health or /api/products.
1

The public route is not under /api, so the API middleware stack does not apply to it.

Test the health route

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

Expected body shape:

json
{
  "ok": true,
  "service": "middleware-api",
  "request_id": "...",
  "duration_ms": 0
}
1
2
3
4
5
6

Expected response headers may include:

txt
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: ...
1
2
3
4
5
6
7

This shows that the middleware stack is active.

Test the products list

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

Expected body shape:

json
{
  "ok": true,
  "products": [
    {
      "id": 1,
      "name": "Laptop",
      "price": 999.99,
      "available": true
    },
    {
      "id": 2,
      "name": "Phone",
      "price": 499.5,
      "available": true
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

This route uses JSON builders:

cpp
using namespace vix::json;

res.json(o(
  "ok", true,
  "products", products_to_json()
));
1
2
3
4
5
6

Use builders when responses become nested or when helper functions return JSON values.

Create a product

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

Expected status:

txt
201 Created
1

Expected body shape:

json
{
  "ok": true,
  "product": {
    "id": 3,
    "name": "Tablet",
    "price": 299.99,
    "available": true
  }
}
1
2
3
4
5
6
7
8
9

The JSON body is parsed by middleware:

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

The handler reads the parsed body:

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

Then it validates application fields.

Test validation errors

Missing name:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/products \
  -H "Content-Type: application/json" \
  -d '{"price":299.99}'
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

Invalid price:

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

Expected status:

txt
422 Unprocessable Entity
1

Expected body shape:

json
{
  "ok": false,
  "error": "Price must be greater than zero",
  "field": "price"
}
1
2
3
4
5

The parser validates JSON format.

The handler validates business rules.

Test invalid JSON

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

Expected status:

txt
400 Bad Request
1

The handler is not called.

The JSON parser middleware rejects the request before route logic runs.

Test wrong Content-Type

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/products \
  -H "Content-Type: text/plain" \
  -d '{"name":"Tablet","price":299.99}'
1
2
3
4

Expected status:

txt
415 Unsupported Media Type
1

The strict JSON parser requires:

txt
Content-Type: application/json
1

or a compatible JSON content type such as:

txt
application/json; charset=utf-8
1

Test body limit

The API has a broad body limit:

cpp
app.use("/api", middleware::app::body_limit_write_dev(
  1024 * 1024
));
1
2
3

And a smaller JSON parser limit for products:

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

Test a large JSON body:

bash
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"
1
2
3
4
5
6

Expected status:

txt
413 Payload Too Large
1

The broad body limit protects the API group.

The parser limit protects the specific JSON route.

Test CORS preflight

Allowed origin:

bash
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"
1
2
3
4
5

Expected status:

txt
204 No Content
1

Expected CORS header:

txt
Access-Control-Allow-Origin: http://localhost:5173
1

Blocked origin:

bash
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"
1
2
3
4
5

Expected status:

txt
403 Forbidden
1

CORS is installed before the JSON parser so browser preflight requests can be handled before body parsing.

Test rate limit

The demo stack uses:

cpp
app.use("/api", middleware::app::rate_limit_custom_dev(
  60.0,
  1.0,
  "x-forwarded-for"
));
1
2
3
4
5

For normal testing, 60 requests is high.

To see rate limiting quickly, temporarily change it to:

cpp
app.use("/api", middleware::app::rate_limit_custom_dev(
  5.0,
  0.0,
  "x-forwarded-for"
));
1
2
3
4
5

Then run:

bash
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"
done
1
2
3
4
5
6

Expected sixth response:

txt
429 Too Many Requests
1

Rate limiting should happen before expensive parsing and handler work.

Middleware order

The middleware installation function is:

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

The order is intentional.

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

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

not on:

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

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.

cpp
app.use("/api", middleware::app::recovery_dev());
1

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.

cpp
app.use("/api", middleware::app::request_id_dev());
1

It stores:

cpp
middleware::basics::RequestId
1

Read it inside a handler:

cpp
auto *request_id =
  req.try_state<middleware::basics::RequestId>();
1
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.

cpp
app.use("/api", middleware::app::timing_dev());
1

It stores:

cpp
middleware::basics::Timing
1

Read it inside a handler:

cpp
auto *timing =
  req.try_state<middleware::basics::Timing>();
1
2

It can also emit headers such as:

txt
x-response-time
server-timing
1
2

Security headers

Security headers middleware adds browser hardening headers.

cpp
app.use("/api", middleware::app::security_headers_dev());
1

Typical headers include:

txt
X-Content-Type-Options
X-Frame-Options
Referrer-Policy
Permissions-Policy
1
2
3
4

Use it on API and web routes where browser behavior matters.

CORS

CORS controls which browser origins can call the API.

cpp
app.use("/api", middleware::app::cors_dev({
  "http://localhost:5173",
  "http://127.0.0.1:5173",
  "https://example.com"
}));
1
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.

cpp
app.use("/api", middleware::app::rate_limit_custom_dev(
  60.0,
  1.0,
  "x-forwarded-for"
));
1
2
3
4
5

The values mean:

txt
capacity = 60
refill_per_sec = 1
key header = x-forwarded-for
1
2
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.

cpp
app.use("/api", middleware::app::body_limit_write_dev(
  1024 * 1024
));
1
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.

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

It checks:

txt
Content-Type
empty body
maximum size
JSON validity
1
2
3
4

Then it stores:

cpp
middleware::parsers::JsonBody
1

The handler reads it with:

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

Complete request flow

For POST /api/products, the flow is:

txt
request
  -> recovery
  -> request id
  -> timing
  -> security headers
  -> CORS
  -> rate limit
  -> body limit
  -> strict JSON parser
  -> handler
  -> response
1
2
3
4
5
6
7
8
9
10
11

If any middleware rejects the request, the handler is not called.

Examples:

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

txt
handler parses JSON
handler checks body size
handler checks CORS
handler rate limits
handler validates business fields
handler writes response
1
2
3
4
5
6

Better route design:

txt
middleware handles reusable HTTP concerns
handler handles application rules
1
2

That is what this example demonstrates.

Summary

This example shows a practical API stack:

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

The rule is:

txt
middleware protects and prepares the request
handlers implement application behavior
1
2

Use this structure when you want a small Vix API that already looks like a real backend.

Released under the MIT License.

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