This example shows how to cache dynamic GET responses with Vix middleware.
HTTP cache is useful when a route is safe, repeatable, and expensive enough that you do not want to run the handler every time.
Examples:
GET /api/products
GET /api/categories
GET /api/public-feed
GET /api/search?q=phone
GET /api/users?page=1This is not static file caching.
Static files are handled by:
app.static_dir(...);HTTP cache middleware is for dynamic route responses generated by handlers.
The app exposes:
GET /api/health
GET /api/products
GET /api/slow-products
GET /api/products-by-languageThe cache is installed on:
/apiOnly GET responses are cached.
The example demonstrates:
cache miss
cache hit
cache bypass
query-aware cache keys
Vary headers
slow handler skipped on cache hitCreate a file:
http_cache_demo.cppAdd this code:
#include <chrono>
#include <string>
#include <thread>
#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;
};
static std::vector<Product> products{
{1, "Laptop", 999.99},
{2, "Phone", 499.50},
{3, "Tablet", 299.99}
};
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
);
}
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_middleware(App &app)
{
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::http_cache({
.ttl_ms = 30'000,
.allow_bypass = true,
.bypass_header = "x-vix-cache",
.bypass_value = "bypass",
.vary_headers = {"accept-language"}
}));
}
static void register_routes(App &app)
{
app.get("/", [](Request &, Response &res)
{
res.text("HTTP cache example. Try /api/products or /api/slow-products.");
});
app.get("/api/health", [](Request &, Response &res)
{
res.json({
"ok", true,
"service", "http-cache"
});
});
app.get("/api/products", [](Request &req, Response &res)
{
using namespace vix::json;
const std::string page = req.query_value("page", "1");
res.json(o(
"ok", true,
"source", "origin",
"page", page,
"products", products_to_json()
));
});
app.get("/api/slow-products", [](Request &, Response &res)
{
using namespace vix::json;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
res.json(o(
"ok", true,
"source", "origin",
"delay_ms", 500,
"products", products_to_json()
));
});
app.get("/api/products-by-language", [](Request &req, Response &res)
{
using namespace vix::json;
const std::string language =
req.has_header("accept-language")
? req.header("accept-language")
: "none";
res.json(o(
"ok", true,
"source", "origin",
"accept_language", language,
"products", products_to_json()
));
});
}
int main()
{
App app;
install_middleware(app);
register_routes(app);
app.run(8080);
return 0;
}vix run http_cache_demo.cppThe server listens on:
http://127.0.0.1:8080curl -i http://127.0.0.1:8080/Expected body:
HTTP cache example. Try /api/products or /api/slow-products.This route is not under /api, so the HTTP cache middleware does not apply to it.
curl -i http://127.0.0.1:8080/api/productsExpected cache status:
x-vix-cache-status: missExpected body shape:
{
"ok": true,
"source": "origin",
"page": "1",
"products": [
{
"id": 1,
"name": "Laptop",
"price": 999.99
}
]
}A miss means:
the cache did not have a valid response
the route handler ran
the response was storedCall the same route again:
curl -i http://127.0.0.1:8080/api/productsExpected cache status:
x-vix-cache-status: hitA hit means:
the cached response was replayed
the route handler was skippedThis is the main value of HTTP cache middleware.
Send the bypass header:
curl -i \
http://127.0.0.1:8080/api/products \
-H "x-vix-cache: bypass"Expected cache status:
x-vix-cache-status: bypassA bypass means:
the request asked the middleware to skip the cache
the route handler ranThis is useful for debugging and manual refresh.
The cache key includes the query string.
First request:
curl -i "http://127.0.0.1:8080/api/products?page=1"Second request:
curl -i "http://127.0.0.1:8080/api/products?page=1"Expected second response:
x-vix-cache-status: hitDifferent page:
curl -i "http://127.0.0.1:8080/api/products?page=2"Expected behavior:
different query string
different cache key
new cache missThe response for page=1 and page=2 should not be shared.
The route /api/slow-products sleeps for 500 ms before responding.
First request:
time curl -i http://127.0.0.1:8080/api/slow-productsExpected cache status:
x-vix-cache-status: missSecond request:
time curl -i http://127.0.0.1:8080/api/slow-productsExpected cache status:
x-vix-cache-status: hitThe second request should be faster because the handler is skipped.
Bypass the slow route:
time curl -i \
http://127.0.0.1:8080/api/slow-products \
-H "x-vix-cache: bypass"Expected cache status:
x-vix-cache-status: bypassThe bypassed request runs the slow handler again.
The middleware is configured with:
.vary_headers = {"accept-language"}This means the Accept-Language header becomes part of the cache key.
French request:
curl -i \
http://127.0.0.1:8080/api/products-by-language \
-H "Accept-Language: fr"Call it again:
curl -i \
http://127.0.0.1:8080/api/products-by-language \
-H "Accept-Language: fr"Expected second response:
x-vix-cache-status: hitEnglish request:
curl -i \
http://127.0.0.1:8080/api/products-by-language \
-H "Accept-Language: en"Expected behavior:
different Accept-Language
different cache key
new cache missUse vary_headers when the same path can return different content depending on request headers.
Without vary_headers, this route could be wrong:
app.get("/api/products-by-language", [](Request &req, Response &res)
{
const std::string language =
req.has_header("accept-language")
? req.header("accept-language")
: "none";
res.json({
"accept_language", language
});
});If the first cached response was French, an English request could receive the French response.
vary_headers prevents that by separating cache entries.
Good vary headers:
accept-language
accept
x-tenant-id
x-currencyBad vary headers:
headers that do not change the response
high-cardinality headers with no clear value
random request ids
user agents unless truly neededA vary header should only be used if it changes the response.
The example uses:
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000,
.allow_bypass = true,
.bypass_header = "x-vix-cache",
.bypass_value = "bypass",
.vary_headers = {"accept-language"}
}));Meaning:
ttl_ms = 30 seconds
allow_bypass = clients can force origin
bypass_header = x-vix-cache
bypass_value = bypass
vary_headers = accept-language participates in the cache keyYou can share a cache instance explicitly.
auto cache = middleware::app::make_default_cache({
.ttl_ms = 30'000
});
app.use("/api", middleware::app::http_cache_mw({
.prefix = "/api",
.only_get = true,
.ttl_ms = 30'000,
.allow_bypass = true,
.bypass_header = "x-vix-cache",
.bypass_value = "bypass",
.vary_headers = {"accept-language"},
.cache = cache,
.add_debug_header = true,
.debug_header = "x-vix-cache-status"
}));Use this when:
multiple middleware instances should share one cache
you want to build the cache object yourself
you want a clearer bootstrap structureFor most examples, middleware::app::http_cache(...) is enough.
Install HTTP cache on route groups that contain safe GET routes.
Good:
app.use("/api/products", middleware::app::http_cache({
.ttl_ms = 30'000
}));Also good:
app.use("/api/public", middleware::app::http_cache({
.ttl_ms = 30'000
}));Be careful with:
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000
}));This is fine when the middleware only applies to GET, but you should still think about which GET routes under /api are safe to cache.
Good candidates:
public product lists
public categories
public blog posts
public search results
public metadata
slow read-only endpointsBad candidates:
current user profile
cart
orders
notifications
private messages
admin dashboard
routes that depend on cookies or hidden session stateThe safe rule is:
cache public GET routes first
avoid private user-specific routes until the cache key is designed carefullyBe careful with authenticated routes.
If the response depends on the user, the cache key must separate users.
Possible vary inputs:
authorization
x-user-id
x-tenant-idBut those can create many cache entries and can be dangerous if misused.
The recommended starting point is:
do not cache private user-specific routes
cache public GET routes firstStatic file cache:
app.static_dir(
"public",
"/",
"index.html",
true,
"public, max-age=3600",
true,
true
);Dynamic HTTP cache:
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000
}));The difference:
static file cache
browser/proxy cache policy for files served from public/
dynamic HTTP cache
server-side response cache for route handlersDo not use http_cache to explain static files.
Do not use app.static_dir(...) to explain dynamic response caching.
They are different parts of Vix.
HTTP cache and ETag solve different problems.
HTTP cache
server stores response
cache hit can skip handler
ETag
response gets a validation tag
client can revalidate
server may return 304Use HTTP cache when the server should avoid repeated work.
Use ETag when the client should avoid downloading the same body again.
They can be combined, but start with one feature and test the headers.
When HTTP cache is active, you may see:
x-vix-cache-status: miss
x-vix-cache-status: hit
x-vix-cache-status: bypassWhen timing middleware is active, you may see:
x-response-time: ...
server-timing: total;dur=...When request id middleware is active, you may see:
x-request-id: ...These headers make the example easier to test.
Run:
vix run http_cache_demo.cppFirst products request:
curl -i http://127.0.0.1:8080/api/productsSecond products request:
curl -i http://127.0.0.1:8080/api/productsBypass:
curl -i \
http://127.0.0.1:8080/api/products \
-H "x-vix-cache: bypass"Query key:
curl -i "http://127.0.0.1:8080/api/products?page=1"
curl -i "http://127.0.0.1:8080/api/products?page=2"Slow route:
time curl -i http://127.0.0.1:8080/api/slow-products
time curl -i http://127.0.0.1:8080/api/slow-productsVary header:
curl -i \
http://127.0.0.1:8080/api/products-by-language \
-H "Accept-Language: fr"
curl -i \
http://127.0.0.1:8080/api/products-by-language \
-H "Accept-Language: en"Use HTTP cache middleware for dynamic GET routes that are safe to replay.
Start with:
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000,
.allow_bypass = true,
.bypass_header = "x-vix-cache",
.bypass_value = "bypass"
}));Remember:
first request
miss, handler runs
same request again
hit, handler skipped
bypass header
bypass, handler runs
different query string
different cache key
vary header
separates responses that depend on request headersKeep the separation clear:
HTTP cache middleware
dynamic GET response cache
app.static_dir(...)
static file serving and static cache headers