API Key Auth
This example shows how to protect API routes with an API key.
It demonstrates:
public routes
protected routes
missing key response
invalid key response
valid key response
reading ApiKey state inside a handler
custom API key optionsAPI key authentication is useful for simple private routes, internal APIs, admin endpoints, service-to-service calls, and early prototypes.
For user login systems, JWT or sessions are usually a better fit.
What this example builds
The app exposes:
GET /api/health
GET /api/admin/status
GET /api/admin/metricsThe route /api/health is public.
The routes under /api/admin require:
x-api-key: secretSource
Create a file:
auth_api_key.cppAdd this code:
#include <string>
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
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/admin", middleware::app::api_key_dev("secret"));
}
static void register_routes(App &app)
{
app.get("/", [](Request &, Response &res)
{
res.text("API Key Auth example. Try /api/health or /api/admin/status.");
});
app.get("/api/health", [](Request &, Response &res)
{
res.json({
"ok", true,
"service", "auth-api-key"
});
});
app.get("/api/admin/status", [](Request &req, Response &res)
{
auto &api_key = req.state<middleware::auth::ApiKey>();
res.json({
"ok", true,
"admin", true,
"message", "API key accepted",
"key_size", static_cast<long long>(api_key.value.size())
});
});
app.get("/api/admin/metrics", [](Request &, Response &res)
{
res.json({
"ok", true,
"requests", 42,
"errors", 0
});
});
}
int main()
{
App app;
install_middleware(app);
register_routes(app);
app.run(8080);
return 0;
}Run it
vix run auth_api_key.cppThe server listens on:
http://127.0.0.1:8080Test the public route
curl -i http://127.0.0.1:8080/api/healthExpected status:
200 OKExpected body shape:
{
"ok": true,
"service": "auth-api-key"
}This route is public because the API key middleware is installed only on:
app.use("/api/admin", middleware::app::api_key_dev("secret"));So it applies to:
/api/admin/status
/api/admin/metricsIt does not apply to:
/api/health
/Test missing API key
curl -i http://127.0.0.1:8080/api/admin/statusExpected status:
401 UnauthorizedExpected error code:
missing_api_keyThe handler is not called.
The middleware stops the request before the route logic runs.
Test invalid API key
curl -i \
http://127.0.0.1:8080/api/admin/status \
-H "x-api-key: wrong"Expected status:
403 ForbiddenExpected error code:
invalid_api_keyThe request provided a key, but the key was not accepted.
Test valid API key
curl -i \
http://127.0.0.1:8080/api/admin/status \
-H "x-api-key: secret"Expected status:
200 OKExpected body shape:
{
"ok": true,
"admin": true,
"message": "API key accepted",
"key_size": 6
}The route can access the key through typed request state:
auto &api_key = req.state<middleware::auth::ApiKey>();In a real application, do not return the API key value to the client.
This example returns only the key size to show that the state exists.
Test another protected route
curl -i \
http://127.0.0.1:8080/api/admin/metrics \
-H "x-api-key: secret"Expected body shape:
{
"ok": true,
"requests": 42,
"errors": 0
}Because the middleware is installed on /api/admin, all routes under that prefix are protected.
How it works
The important line is:
app.use("/api/admin", middleware::app::api_key_dev("secret"));This installs API key authentication only for the /api/admin prefix.
The request flow for /api/admin/status is:
request
-> request id
-> timing
-> security headers
-> API key middleware
-> route handler
-> responseIf the key is missing:
request
-> API key middleware
-> 401 responseIf the key is invalid:
request
-> API key middleware
-> 403 responseIf the key is valid:
request
-> API key middleware
-> route handler
-> 200 responseWhy 401 and 403 are different
A missing key returns:
401 UnauthorizedThat means the request is not authenticated.
An invalid key returns:
403 ForbiddenThat means credentials were provided but rejected.
This distinction makes debugging and client behavior clearer.
Custom API key configuration
The preset is good for simple examples.
For custom behavior, use ApiKeyOptions.
This version accepts:
x-api-key: secretand also:
?api_key=secret#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
middleware::auth::ApiKeyOptions opt;
opt.header = "x-api-key";
opt.query_param = "api_key";
opt.required = true;
opt.allowed_keys = {"secret"};
app.use("/api/admin", middleware::app::adapt_ctx(
middleware::auth::api_key(opt)
));
app.get("/api/admin/status", [](Request &req, Response &res)
{
auto &api_key = req.state<middleware::auth::ApiKey>();
res.json({
"ok", true,
"admin", true,
"key_size", static_cast<long long>(api_key.value.size())
});
});
app.run(8080);
}Test with header:
curl -i \
http://127.0.0.1:8080/api/admin/status \
-H "x-api-key: secret"Test with query parameter:
curl -i \
"http://127.0.0.1:8080/api/admin/status?api_key=secret"Prefer headers for production APIs.
Query parameters can be stored in logs, browser history, reverse proxy logs, and analytics systems.
Custom validation function
You can validate keys dynamically.
middleware::auth::ApiKeyOptions opt;
opt.header = "x-api-key";
opt.required = true;
opt.validate = [](const std::string &key)
{
return key == "secret";
};
app.use("/api/admin", middleware::app::adapt_ctx(
middleware::auth::api_key(opt)
));Use this pattern when the key must be checked against:
a database
a cache
a configuration file
a tenant table
an internal serviceThe middleware accepts the key only if validate(...) returns true.
Custom extraction function
You can also control how the key is extracted from the request.
middleware::auth::ApiKeyOptions opt;
opt.required = true;
opt.extract = [](const middleware::Request &req)
{
std::string key = req.header("x-api-key");
if (!key.empty())
return key;
return req.header("x-admin-key");
};
opt.validate = [](const std::string &key)
{
return key == "secret";
};
app.use("/api/admin", middleware::app::adapt_ctx(
middleware::auth::api_key(opt)
));Use extract when the API key can come from a custom header or a special gateway convention.
Optional API key
Sometimes a route can accept an API key when present but still allow anonymous requests.
Use:
middleware::auth::ApiKeyOptions opt;
opt.header = "x-api-key";
opt.required = false;
opt.allowed_keys = {"secret"};
app.use("/api", middleware::app::adapt_ctx(
middleware::auth::api_key(opt)
));When required is false:
missing key
request continues
invalid key
request is rejected
valid key
ApiKey state is storedIn the handler, use try_state:
auto *api_key = req.try_state<middleware::auth::ApiKey>();
if (api_key)
{
res.json({
"ok", true,
"authenticated", true
});
return;
}
res.json({
"ok", true,
"authenticated", false
});Use optional API keys for routes that can behave differently for trusted clients but still allow public access.
API key with other middleware
API key auth should usually run after broad security middleware and before protected handlers.
A practical order is:
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({"https://example.com"}));
app.use("/api", middleware::app::rate_limit_dev());
app.use("/api/admin", middleware::app::api_key_dev("secret"));The idea is:
request id
identify the request
timing
measure the request
security headers
harden responses
CORS
handle browser access
rate limit
block abusive clients
API key
protect private routesFor routes with JSON bodies, add the parser after the broad safety middleware:
app.use("/api/admin/products", middleware::app::api_key_dev("secret"));
app.use("/api/admin/products", middleware::app::json_strict_dev(4096));API key vs JWT
Use API keys when the caller is usually a system, script, service, or admin tool.
internal service
deployment tool
private admin endpoint
simple automationUse JWT when the caller is usually a user or sessionless client identity.
user login
mobile app
frontend app
role-based API
permission-based APIA simple rule:
API key
simple service access
JWT
user identity and claims
RBAC
roles and permissions after JWTProduction notes
For production:
do not hardcode secrets in source code
read API keys from environment or config
do not log full API keys
prefer headers over query parameters
rotate keys when needed
use HTTPS
combine with rate limitingExample using a configuration value:
const std::string admin_key = cfg.getString("security.admin_api_key", "");
app.use("/api/admin", middleware::app::api_key_dev(admin_key));Make sure the key is not empty before starting the server.
Complete test flow
Run:
vix run auth_api_key.cppPublic route:
curl -i http://127.0.0.1:8080/api/healthMissing key:
curl -i http://127.0.0.1:8080/api/admin/statusInvalid key:
curl -i \
http://127.0.0.1:8080/api/admin/status \
-H "x-api-key: wrong"Valid key:
curl -i \
http://127.0.0.1:8080/api/admin/status \
-H "x-api-key: secret"Another protected route:
curl -i \
http://127.0.0.1:8080/api/admin/metrics \
-H "x-api-key: secret"Summary
API key authentication is the simplest way to protect a route group.
Use:
app.use("/api/admin", middleware::app::api_key_dev("secret"));Then read the authenticated key state when needed:
auto &api_key = req.state<middleware::auth::ApiKey>();The behavior is:
missing key
401 missing_api_key
invalid key
403 invalid_api_key
valid key
handler runsUse API keys for simple private access.
Use JWT and RBAC when user identity, claims, roles, and permissions matter.