Core Concepts
This page explains the model behind vix::middleware.
The quick start shows how to install middleware on vix::App.
This page explains what happens behind that API:
how middleware runs
what next() means
how a request can be stopped early
how middleware can modify a response
how typed request state works
how lower-level middleware integrates with vix::App
when to use HttpPipeline
why middleware order mattersThe goal is to make middleware predictable.
Once the flow is clear, the module becomes simple to reason about.
The simplest definition
A middleware is code that runs around a route handler.
It can:
inspect the request
modify the response
store data on the request
call the next middleware
stop the request earlyA normal vix::App middleware has this shape:
app.use([](vix::Request &req, vix::Response &res, vix::App::Next next)
{
(void)req;
(void)res;
next();
});The important part is next.
Calling next() means:
continue to the next middleware or route handlerNot calling next() means:
stop the request hereThis is the central rule.
Request flow
When several middleware functions are installed, they run in order.
app.use(middleware_a);
app.use(middleware_b);
app.use(middleware_c);The request flow is:
request
-> middleware_a
-> middleware_b
-> middleware_c
-> route handler
-> responseIf every middleware calls next(), the request eventually reaches the route handler.
If one middleware does not call next(), the chain stops.
request
-> middleware_a
-> middleware_b
stops here
-> responseThe route handler is not called.
This is how rate limits, API key checks, JWT checks, body limits, CORS preflight, CSRF, IP filtering, and cache hits work.
Before middleware
Some middleware runs before the handler and decides whether the request can continue.
app.use("/admin", [](vix::Request &req, vix::Response &res, vix::App::Next next)
{
if (req.header("x-api-key") != "secret")
{
res.status(401).json({
"ok", false,
"error", "unauthorized"
});
return;
}
next();
});If the header is missing or invalid, the middleware sends a response and returns.
The handler is skipped.
This pattern is called short-circuiting.
After middleware
Some middleware calls next() first, then modifies the response after the handler has run.
app.use([](vix::Request &req, vix::Response &res, vix::App::Next next)
{
(void)req;
next();
res.header("X-Content-Type-Options", "nosniff");
});This pattern is useful when the middleware needs to see the final response.
Examples:
security headers
timing
logging
compression
ETag
metricsThese middleware functions usually need the handler to produce a response first.
Before and after middleware
A middleware can also do work before and after next().
app.use([](vix::Request &req, vix::Response &res, vix::App::Next next)
{
const auto path = req.path();
next();
res.header("X-Handled-Path", path);
});The middleware reads the request before the handler, then updates the response after the handler.
This is the wrapping model.
middleware begins
-> next middleware
-> route handler
<- response returns
middleware endsWhy order matters
Middleware order controls behavior.
This order is good:
app.use("/api", middleware::app::body_limit_write_dev(1024 * 1024));
app.use("/api/users", middleware::app::json_strict_dev(4096));The body limit runs before the JSON parser.
That means oversized bodies are rejected before parsing.
This order is usually worse:
app.use("/api/users", middleware::app::json_strict_dev(4096));
app.use("/api", middleware::app::body_limit_write_dev(1024 * 1024));The parser can run before the global body limit.
A practical order is:
recovery
request id
timing
security headers
CORS
rate limit
body limit
authentication
parser
handler
compression
ETag
logging
metricsThe exact order depends on the application.
The principle is stable:
reject bad requests early
parse only after size rules
authenticate before protected handlers
modify responses after handlers
observe the whole request flowShort-circuiting
Short-circuiting means a middleware sends a response and does not call next().
app.use("/api", [](vix::Request &req, vix::Response &res, vix::App::Next next)
{
if (req.method() == "POST" && req.body().size() > 1024)
{
res.status(413).json({
"ok", false,
"error", "payload_too_large"
});
return;
}
next();
});Short-circuiting is normal for:
CORS preflight
body limit
rate limit
API key authentication
JWT authentication
RBAC authorization
CSRF protection
IP filtering
HTTP cache hits
parser errorsA middleware should short-circuit only when it has enough information to produce the response itself.
App middleware and module middleware
Core already supports middleware through vix::App.
app.use([](vix::Request &req, vix::Response &res, vix::App::Next next)
{
(void)req;
(void)res;
next();
});The vix::middleware module provides reusable middleware implementations.
app.use(vix::middleware::app::cors_dev());
app.use(vix::middleware::app::security_headers_dev());
app.use(vix::middleware::app::rate_limit_dev());The difference is:
Core middleware
custom function installed directly on vix::App
vix::middleware
reusable middleware for common backend concernsUse direct App middleware for one-off behavior.
Use vix::middleware when you need a reusable component such as CORS, rate limiting, request IDs, JSON parsing, sessions, JWT, HTTP cache, or compression.
Context-based middleware
Inside the middleware module, the main lower-level type is:
vix::middleware::MiddlewareFnIt has this shape:
using MiddlewareFn = std::function<void(Context &, Next)>;A context-based middleware receives a Context.
vix::middleware::MiddlewareFn mw =
[](vix::middleware::Context &ctx, vix::middleware::Next next)
{
ctx.res().header("X-Example", "yes");
next();
};Context gives middleware a consistent place to access:
request
response
services
typed state helpers
error helpersThe request and response are still the normal Vix HTTP objects.
The context just groups middleware-specific tools around them.
Request and response access
A context-based middleware reads the request with:
ctx.req()Example:
const std::string method = ctx.req().method();
const std::string path = ctx.req().path();
const std::string content_type = ctx.req().header("Content-Type");It writes to the response with:
ctx.res()Example:
ctx.res().status(200);
ctx.res().header("X-Example", "ok");
ctx.res().text("OK");Most middleware either:
reads the request before next()
writes the response after next()
does bothTyped request state
Middleware often needs to attach data to the current request.
Examples:
request_id()
stores RequestId
timing()
stores Timing
json()
stores JsonBody
form()
stores FormBody
jwt()
stores JwtClaims
api_key()
stores ApiKey
rbac_context()
stores Authz
session()
stores SessionA handler can read typed state:
app.post("/api/echo", [](vix::Request &req, vix::Response &res)
{
auto &body = req.state<vix::middleware::parsers::JsonBody>();
res.json({
"received", body.value.dump()
});
});The type is the key.
You do not need string keys.
You ask for the state by type.
state<T>() and try_state<T>()
Use state<T>() when the value must exist.
auto &body = req.state<vix::middleware::parsers::JsonBody>();This is appropriate when the route is protected by the middleware that creates the state.
Example:
app.use("/api/users", vix::middleware::app::json_strict_dev());
app.post("/api/users", [](vix::Request &req, vix::Response &res)
{
auto &body = req.state<vix::middleware::parsers::JsonBody>();
res.json({
"ok", true,
"body", body.value.dump()
});
});Use try_state<T>() when the value may be missing.
auto *session = req.try_state<vix::middleware::auth::Session>();
if (!session)
{
res.status(500).json({
"ok", false,
"error", "session_missing"
});
return;
}This is safer when middleware is optional, conditional, or installed only on some routes.
Why typed state matters
Typed state keeps handlers clean.
Without middleware, a handler may need to:
read raw body
check content type
parse JSON
handle parse errors
validate auth headers
decode token
extract roles
load sessionWith middleware, reusable work happens before the handler.
The handler reads the typed result.
auto &claims = req.state<vix::middleware::auth::JwtClaims>();
auto &body = req.state<vix::middleware::parsers::JsonBody>();This keeps application logic focused.
Services
Context also gives access to a services container.
ctx.services()Services let middleware share objects without global variables.
Examples:
loggers
metrics sinks
permission resolvers
rate limiter state
custom application servicesA middleware can look up a service:
auto resolver = ctx.services().get<MyService>();If the service exists, the middleware can use it.
If it does not, the middleware can fall back, skip optional behavior, or return an error depending on its design.
Services are most useful in lower-level middleware and HttpPipeline.
Normal vix::App applications often use presets first and only use services for custom integrations.
Normalized errors
Middleware should return predictable errors.
The module exposes a normalized error model through:
vix::middleware::Error
vix::middleware::normalize(...)
ctx.send_error(...)Example:
vix::middleware::Error err;
err.status = 401;
err.code = "unauthorized";
err.message = "Missing token";
err.details["hint"] = "Use Authorization header";
ctx.send_error(vix::middleware::normalize(std::move(err)));A normalized error lets middleware produce consistent responses.
Common middleware errors include:
400 invalid_json
401 missing_api_key
401 missing_auth
403 forbidden
403 csrf_failed
411 length_required
413 payload_too_large
415 unsupported_media_type
429 rate_limited
500 internal_server_errorApp integration
Most reusable middleware is context-based.
To use it inside vix::App, adapt it:
app.use(vix::middleware::app::adapt_ctx(
vix::middleware::basics::request_id()
));This converts:
MiddlewareFn
-> vix::App::MiddlewareFor legacy HTTP middleware, use adapt().
app.use(vix::middleware::app::adapt(my_http_middleware));For normal applications, prefer App presets:
app.use("/api", vix::middleware::app::cors_dev());
app.use("/api", vix::middleware::app::rate_limit_dev());
app.use("/api/users", vix::middleware::app::json_strict_dev());Presets are already adapted for vix::App.
Prefix matching
vix::App can install middleware globally:
app.use(middleware);Or on a prefix:
app.use("/api", middleware);A prefix middleware applies to matching routes.
app.use("/api", middleware::app::rate_limit_dev());This applies to:
/api
/api/users
/api/admin/statusIt does not apply to:
/admin
/publicA more specific prefix limits the middleware to a smaller route group.
app.use("/api/users", middleware::app::json_strict_dev());This applies to:
/api/users
/api/users/123It does not apply to:
/api/health
/api/admin/statusUse prefixes intentionally.
Broad middleware should go on broad prefixes.
Route-specific middleware should go on narrow prefixes.
Exact path protection
Sometimes a middleware should apply to one exact route.
Use:
vix::middleware::app::protect(...)Example:
vix::middleware::app::protect(
app,
"/admin",
vix::middleware::app::api_key_dev("secret")
);This protects:
/adminIt does not protect:
/admin/settingsFor route groups, use prefix protection instead.
vix::middleware::app::protect_prefix(
app,
"/admin",
vix::middleware::app::api_key_dev("secret")
);Chaining middleware
A chain combines several App middlewares into one.
auto users_stack = vix::middleware::app::chain(
vix::middleware::app::body_limit_write_dev(4096),
vix::middleware::app::json_strict_dev(4096)
);
app.use("/api/users", std::move(users_stack));The chain runs in order.
body_limit_write_dev
-> json_strict_dev
-> handlerUse chain() when a route or route group has a clear middleware stack.
Conditional middleware
Use when() when prefix matching is not enough.
auto only_post = vix::middleware::app::when(
[](const vix::Request &req)
{
return req.method() == "POST";
},
vix::middleware::app::body_limit_write_dev(1024)
);
app.use("/api", std::move(only_post));If the predicate returns false, the middleware is skipped.
Use when() for conditions such as:
only POST requests
only a specific content type
only a specific header
custom path matching
custom tenant logicHttpPipeline
Most applications should use vix::App.
HttpPipeline exists for tests, examples, and lower-level integrations.
vix::middleware::HttpPipeline pipeline;
pipeline.use(vix::middleware::security::csrf());
pipeline.use(vix::middleware::parsers::json());
pipeline.run(req, res, [](auto &req, auto &res)
{
res.ok().text("OK");
});Use HttpPipeline when you want to run middleware without starting a server.
It is useful for:
unit tests
middleware tests
custom HTTP integration
manual pipeline composition
observability hooksIt is not the default application path.
The default path is still:
vix::App
-> app.use(...)
-> route handlerPipeline hooks
HttpPipeline supports hooks.
Common hook points are:
on_begin
on_end
on_errorHooks are useful for observability.
Examples:
tracing
metrics
debug traceA pipeline can install hooks:
vix::middleware::HttpPipeline pipeline;
pipeline.set_hooks(
vix::middleware::observability::tracing_hooks()
);Hooks are lower-level than normal App middleware.
Use them when you need pipeline-level control.
Static files are outside the middleware chain
Static files are served by vix::App.
app.static_dir("public", "/", "index.html");That is a Core feature.
The middleware module can provide an optional static response hook, for example compression:
vix::App::set_static_response_hook(
vix::middleware::performance::compressed_static_response_hook()
);The distinction is important:
app.use(...)
route middleware
app.static_dir(...)
static file serving
App::set_static_response_hook(...)
optional response enhancement for static filesDo not think of static files as middleware.
Core serves them.
Middleware can enhance the response after Core writes it.
A complete mental model
For a dynamic API route:
request
-> App prefix matching
-> middleware chain
-> route handler
-> after-next middleware work
-> responseFor a blocked request:
request
-> middleware chain
-> middleware sends error
-> responseFor a cached GET response:
request
-> HTTP cache middleware
-> cache hit
-> cached responseFor a static file:
request
-> Core static file resolution
-> file response
-> optional static response hook
-> responseThis is the separation to remember:
Core owns the app and file serving.
Middleware owns reusable HTTP behavior.
App integration connects them.Summary
The middleware model is built on a few simple ideas:
middleware wraps handlers
next() continues the request
not calling next() stops the request
middleware order matters
typed state carries parsed/authenticated data
App presets are the normal user-facing API
adapt_ctx() connects lower-level middleware to vix::App
HttpPipeline is for tests and custom integrations
static files belong to CoreOnce these ideas are clear, the rest of the module is just a set of reusable backend components.