Create a Durable POST Route
This guide shows how to create a durable POST route with Cnerium.
A durable route is used when a backend operation must be safe under retries. The common case is a route that creates or changes important state, such as an order, payment, invoice, registration, stock reservation, or workflow command.
The route is still hosted by Vix. Cnerium only adds the reliability layer around the selected handler.
Starting point
Start with a normal Vix application and attach Cnerium.
#include <vix.hpp>
#include <cnerium/cnerium.hpp>
int main()
{
vix::App app;
auto cnerium = cnerium::attach(app);
app.get("/health", [](vix::Request &req, vix::Response &res)
{
(void)req;
res.json({
{"ok", true}
});
});
if (!cnerium.start())
{
return 1;
}
app.run();
return 0;
}The application is still a Vix backend. Cnerium is attached to the vix::App so it can register durable routes into the same application.
Add the route
A durable POST route is registered with durable_post:
cnerium.durable_post(
"/orders",
"orders.create",
[](cnerium::DurableRequest &request)
{
(void)request;
return cnerium::created({
{"ok", true}
});
});The route has three parts.
The first argument is the HTTP path:
/ordersThe second argument is the stable operation name:
orders.createThe third argument is the durable handler.
The path belongs to the HTTP API. The operation name belongs to Cnerium’s reliability model. The handler contains the application logic that should run only for a new safe request.
Why the operation name matters
The operation name scopes the idempotency state for the route.
cnerium.durable_post(
"/orders",
"orders.create",
handler);Cnerium uses the operation name together with the Idempotency-Key and the request body hash.
A good operation name is explicit and stable:
orders.create
payments.create
invoices.create
users.register
workflows.startAvoid names that are too generic:
create
submit
post
action
handlerChanging the operation name changes the idempotency namespace for the route. Treat it as part of the backend contract.
Read the JSON body
Inside the durable handler, use request.json() to parse the body.
const auto body = request.json();Then extract fields from the JSON object:
const std::string product_id = cnerium::support::string_or(body, "product_id", "");
const int quantity = cnerium::support::int_or(body, "quantity", 0);This keeps the example simple and explicit.
Validate input
A durable route still needs normal application validation.
if (product_id.empty())
{
return cnerium::DurableResponse::bad_request(
"Missing required field: product_id");
}
if (quantity <= 0)
{
return cnerium::DurableResponse::bad_request(
"Field quantity must be greater than zero");
}Cnerium protects the retry behavior of the route, but it does not replace domain validation. The application is still responsible for validating the submitted data before creating or changing state.
Return a durable response
A durable handler returns cnerium::DurableResponse.
return cnerium::created({
{"ok", true},
{"order_id", order_id},
{"product_id", product_id},
{"quantity", quantity}
});The response is durable because Cnerium can store it and replay it later.
A normal Vix route writes directly to vix::Response. A Cnerium durable route returns a response object so Cnerium can preserve the result of the completed operation.
Complete route
Here is the full route:
cnerium.durable_post(
"/orders",
"orders.create",
[](cnerium::DurableRequest &request)
{
const auto body = request.json();
const std::string product_id = cnerium::support::string_or(body, "product_id", "");
const int quantity = cnerium::support::int_or(body, "quantity", 0);
if (product_id.empty())
{
return cnerium::DurableResponse::bad_request(
"Missing required field: product_id");
}
if (quantity <= 0)
{
return cnerium::DurableResponse::bad_request(
"Field quantity must be greater than zero");
}
const std::string order_id =
"ord_" + request.idempotency_key_value();
return cnerium::created({
{"ok", true},
{"order_id", order_id},
{"product_id", product_id},
{"quantity", quantity}
});
});This route is intentionally small. In a real application, the handler might call a service, insert a database row, reserve stock, write an audit record, or call an external provider. The important rule is that those critical side effects should happen inside the durable handler, after Cnerium has decided that the request is safe to execute.
Complete application
#include <vix.hpp>
#include <cnerium/cnerium.hpp>
#include <string>
int main()
{
vix::App app;
auto cnerium = cnerium::attach(app);
app.get("/health", [](vix::Request &req, vix::Response &res)
{
(void)req;
res.json({
{"ok", true},
{"service", "orders"}
});
});
cnerium.durable_post(
"/orders",
"orders.create",
[](cnerium::DurableRequest &request)
{
const auto body = request.json();
const std::string product_id = cnerium::support::string_or(body, "product_id", "");
const int quantity = cnerium::support::int_or(body, "quantity", 0);
if (product_id.empty())
{
return cnerium::DurableResponse::bad_request(
"Missing required field: product_id");
}
if (quantity <= 0)
{
return cnerium::DurableResponse::bad_request(
"Field quantity must be greater than zero");
}
const std::string order_id = "ord_" + request.idempotency_key_value();
return cnerium::created({
{"ok", true},
{"order_id", order_id},
{"product_id", product_id},
{"quantity", quantity}
});
});
if (!cnerium.start())
{
return 1;
}
app.run();
return 0;
}This application has one normal Vix route and one durable Cnerium route.
The health route is ordinary HTTP. The order route is protected against unsafe retries.
Send a valid request
A durable route requires an Idempotency-Key.
curl -i -X POST http://127.0.0.1:8080/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-123" \
-d '{"product_id":"p1","quantity":2}'Expected response:
HTTP/1.1 201 CreatedExample body:
{
"ok": true,
"order_id": "ord_order-123",
"product_id": "p1",
"quantity": 2
}Because this is the first request for orders.create with order-123, Cnerium executes the handler and stores the response.
Retry the same request
Send the same request again:
curl -i -X POST http://127.0.0.1:8080/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-123" \
-d '{"product_id":"p1","quantity":2}'Expected response:
HTTP/1.1 201 CreatedThe body should match the first response.
This time, Cnerium returns the stored response. The durable handler is not executed again.
Reuse the same key with a different body
Now change the request body but keep the same idempotency key:
curl -i -X POST http://127.0.0.1:8080/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-123" \
-d '{"product_id":"p2","quantity":1}'Expected response:
HTTP/1.1 409 ConflictThis is not a safe retry. The same key is being used for a different request body, so Cnerium rejects it.
Missing key
If the client omits the idempotency key:
curl -i -X POST http://127.0.0.1:8080/orders \
-H "Content-Type: application/json" \
-d '{"product_id":"p1","quantity":2}'The route should reject the request.
A durable route cannot evaluate retry safety without a stable key for the logical operation.
Add a service function
For real applications, keep the durable route handler small and move domain work into a service function.
struct Order
{
std::string id;
std::string product_id;
int quantity{};
};
Order create_order(
const std::string &idempotency_key,
const std::string &product_id,
int quantity)
{
return Order{
"ord_" + idempotency_key,
product_id,
quantity};
}Then call it from the durable handler:
const Order order = create_order(request.idempotency_key_value(),
product_id,
quantity);
return cnerium::created({
{"ok", true},
{"order_id", order.id},
{"product_id", order.product_id},
{"quantity", order.quantity}
});The durable handler remains responsible for request-level validation and response creation. The service owns the domain operation.
Complete application with a service
#include <vix.hpp>
#include <cnerium/cnerium.hpp>
#include <string>
struct Order
{
std::string id;
std::string product_id;
int quantity{};
};
Order create_order(
const std::string &idempotency_key,
const std::string &product_id,
int quantity)
{
return Order{
"ord_" + idempotency_key,
product_id,
quantity};
}
int main()
{
vix::App app;
auto cnerium = cnerium::attach(app);
app.get("/health", [](vix::Request &req, vix::Response &res)
{
(void)req;
res.json({
{"ok", true},
{"service", "orders"}
});
});
cnerium.durable_post(
"/orders",
"orders.create",
[](cnerium::DurableRequest &request)
{
const auto body = request.json();
const std::string product_id = cnerium::support::string_or(body, "product_id", "");
const int quantity = cnerium::support::int_or(body, "quantity", 0);
if (product_id.empty())
{
return cnerium::DurableResponse::bad_request(
"Missing required field: product_id");
}
if (quantity <= 0)
{
return cnerium::DurableResponse::bad_request(
"Field quantity must be greater than zero");
}
const Order order =create_order(request.idempotency_key_value(),
product_id,
quantity);
return cnerium::created({
{"ok", true},
{"order_id", order.id},
{"product_id", order.product_id},
{"quantity", order.quantity}
});
});
if (!cnerium.start())
{
return 1;
}
app.run();
return 0;
}This keeps the example close to a real backend shape without introducing unrelated infrastructure.
Add realtime emission
A durable handler can emit an event after the operation succeeds.
cnerium.emit(
"order.created",
cnerium::support::object({
{"order_id", cnerium::Json(order.id)},
{"product_id", cnerium::Json(order.product_id)},
{"quantity", cnerium::Json(order.quantity)}
})
);To use this inside the handler, capture the attached Cnerium layer by reference:
cnerium.durable_post(
"/orders",
"orders.create",
[&cnerium](cnerium::DurableRequest &request)
{
// create order
cnerium.emit(
"order.created",
cnerium::support::object({
{"order_id", cnerium::Json(order.id)}
})
);
return cnerium::created({
{"ok", true},
{"order_id", order.id}
});
});If the same request is replayed from storage, the handler is not executed again, so the event is not emitted again by that handler.
Common mistakes
Do not use app.post for critical writes that need retry safety.
app.post("/orders", create_order);That is a normal Vix route. It does not provide stored response replay or idempotency conflict detection.
Do not generate a new idempotency key on every retry. The client must reuse the same key for the same logical operation.
Do not reuse the same key with a different request body. Cnerium will reject it as a conflict.
Do not make every route durable. Use durable routes for critical writes, and keep ordinary reads as normal Vix routes.
Do not let critical side effects happen outside the durable handler. Cnerium can only prevent duplicate handler execution for work that happens inside the durable route path.
Summary
A durable POST route is a Vix route protected by Cnerium.
Register it with cnerium.durable_post, give it a stable operation name, require the client to send an Idempotency-Key, validate the request, perform the operation, and return a cnerium::DurableResponse.
On a safe retry, Cnerium returns the stored response instead of running the handler again. On unsafe key reuse, it returns 409 Conflict.