C++ REST API Server Complete Guide | Routing· Middleware
Introduction: “Express-like ergonomics in C++“
REST API Server Essentials
Like Node.js Express or Python Flask, concise routing, middleware chains, and auto-documentation in C++ deliver both high performance and productivity. Goals:
- Express-style routing (GET/POST/PUT/DELETE)
- Middleware chain (logging, auth, CORS)
- JSON request/response auto-parsing
- Swagger/OpenAPI doc generation
- Request validation and error handling Requirements: C++17+, Boost.Beast, nlohmann/json What you’ll learn:
- Build Express-style REST API servers
- Implement middleware patterns
- Integrate JWT authentication
- Auto-generate API documentation
Problem Scenarios: When C++ REST API is Needed
Scenario 1: High-Performance Backend API
Node.js/Python are convenient, but for game servers, financial trading APIs, real-time data feeds handling tens of thousands of requests per second, latency is the bottleneck. C++ REST APIs achieve microsecond response times.
Scenario 2: Add HTTP API to Existing C++ Systems
Game engines, trading systems, IoT gateways already in C++ need web/mobile client integration. Separate Node.js server adds IPC overhead. Direct REST API in C++ process simplifies integration.
Scenario 3: Resource-Constrained Environments
Embedded/edge servers have strict memory limits. Node.js runtime consumes tens of MB. Lightweight C++ REST server operates in megabytes with minimal dependencies.
Scenario 4: Learning Routing/Middleware Patterns
To understand how Express’s app.get(), app.use(), next() work, implementing from scratch is most effective. C++ gives full control over memory and runtime behavior.
Scenario 5: Swagger Auto-Generation
As APIs grow, manual docs fall behind. Code-synced OpenAPI docs ease frontend/mobile collaboration.
Scenario 6: “CORS error in browser”
Frontend (React, Vue) on http://localhost:3000, API on http://localhost:8080—browser blocks cross-origin requests. Access to fetch has been blocked by CORS policy error. Fix: CORS middleware sets Access-Control-Allow-Origin/Methods/Headers, answers OPTIONS preflight with 204.
Scenario 7: “POST request body is empty”
req.json_body() returns empty {}. Cause: Missing Content-Type: application/json header or response sent before parsing. Fix: Check Content-Type, parse, return 400 on failure.
Scenario 8: “Protected API returns 200 without auth”
/users endpoint has auth middleware, but requests without Authorization header return 200 OK. Cause: Middleware registration order error or route-specific middleware not applied. Fix: Bind auth middleware explicitly to route, return immediately on auth failure without calling next().
Conceptual Analogy
Sockets and async I/O are like mailbox addresses and delivery routes. With correct address (IP·port), data arrives, and Asio is like one post office with multiple carriers (threads·handlers) dividing work.
Table of Contents
- Router Design
- Middleware Chain
- Request/Response Handling
- Authentication Middleware
- Validation and Error Handling
- Swagger Documentation
- Complete REST API Example
- Common Errors and Solutions
- Best Practices
- Performance Optimization
- Production Patterns
1. Router Design
Architecture Diagram
flowchart TB
subgraph Client[Client]
C1[HTTP Request]
end
subgraph Server[Server]
subgraph Middleware[Middleware Chain]
M1[Logging]
M2[CORS]
M3[Auth]
M4[Error Handler]
end
subgraph Router[Router]
R1["GET /users"]
R2["POST /users"]
R3["GET /users/:id"]
end
M1 --> M2 --> M3 --> M4 --> Router
end
C1 --> Client
Client --> Server
Express-Style API
// Usage example
RestServer server;
server.get("/users", [](const Request& req, Response& res) {
res.json({{"users", get_all_users()}});
});
server.post("/users", [](const Request& req, Response& res) {
auto user = req.body<User>();
auto id = create_user(user);
res.status(201).json({{"id", id}});
});
server.get("/users/:id", [](const Request& req, Response& res) {
auto id = req.params("id");
auto user = get_user(id);
if (!user) {
res.status(404).json({{"error", "User not found"}});
return;
}
res.json(*user);
});
server.listen(8080);
Router Implementation
#include <functional>
#include <vector>
#include <regex>
#include <optional>
class Router {
public:
using Handler = std::function<void(const Request&, Response&)>;
using Middleware = std::function<void(const Request&, Response&, std::function<void()>)>;
private:
struct Route {
std::string method;
std::regex path_regex;
std::vector<std::string> param_names;
Handler handler;
std::vector<Middleware> middlewares;
};
std::vector<Route> routes_;
std::vector<Middleware> global_middlewares_;
public:
void get(const std::string& path, Handler handler) {
add_route("GET", path, handler);
}
void post(const std::string& path, Handler handler) {
add_route("POST", path, handler);
}
void put(const std::string& path, Handler handler) {
add_route("PUT", path, handler);
}
void del(const std::string& path, Handler handler) {
add_route("DELETE", path, handler);
}
void use(Middleware middleware) {
global_middlewares_.push_back(middleware);
}
private:
void add_route(const std::string& method, const std::string& path, Handler handler) {
Route route;
route.method = method;
route.handler = handler;
// Parse path parameters: /users/:id -> /users/([^/]+)
std::string regex_path = path;
std::regex param_regex(R"(:([a-zA-Z_][a-zA-Z0-9_]*))");
std::smatch match;
while (std::regex_search(regex_path, match, param_regex)) {
route.param_names.push_back(match[1]);
regex_path = match.prefix().str() + "([^/]+)" + match.suffix().str();
}
route.path_regex = std::regex("^" + regex_path + "$");
routes_.push_back(route);
}
public:
std::optional<Route> match(const std::string& method, const std::string& path) {
for (auto& route : routes_) {
if (route.method != method) continue;
std::smatch match;
if (std::regex_match(path, match, route.path_regex)) {
// Extract parameters (match[1], match[2], ...)
return route;
}
}
return std::nullopt;
}
};
Key: Path parameters (:id) are converted to regex capture groups for extraction.
2. Middleware Chain
Middleware Pattern
sequenceDiagram
participant C as Client
participant M1 as Logging
participant M2 as CORS
participant M3 as Auth
participant H as Handler
C->>M1: Request
M1->>M2: next()
M2->>M3: next()
M3->>H: next()
H-->>M3: Response
M3-->>M2: Return
M2-->>M1: Return
M1-->>C: Response
// 타입 정의
class MiddlewareChain {
std::vector<Router::Middleware> middlewares_;
Router::Handler final_handler_;
public:
void add(Router::Middleware middleware) {
middlewares_.push_back(middleware);
}
void execute(const Request& req, Response& res) {
execute_at(0, req, res);
}
private:
void execute_at(size_t index, const Request& req, Response& res) {
if (index >= middlewares_.size()) {
final_handler_(req, res);
return;
}
middlewares_[index](req, res, [this, index, &req, &res]() {
execute_at(index + 1, req, res);
});
}
};
Key: Each middleware calls next() to continue chain or returns early to short-circuit.
Logging Middleware
auto logging_middleware = [](const Request& req, Response& res, auto next) {
auto start = std::chrono::steady_clock::now();
std::cout << req.method() << " " << req.path() << std::endl;
next(); // Execute next middleware
auto end = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << " -> " << res.status_code()
<< " (" << duration.count() << "ms)" << std::endl;
};
server.use(logging_middleware);
Output:
GET /users
-> 200 (15ms)
CORS Middleware
auto cors_middleware = [](const Request& req, Response& res, auto next) {
res.set_header("Access-Control-Allow-Origin", "*");
res.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (req.method() == "OPTIONS") {
res.status(204).send();
return; // Don't call next()
}
next();
};
server.use(cors_middleware);
Key: OPTIONS preflight requests return 204 immediately without calling next().
3. Request/Response Handling
Request Class
#include <boost/beast.hpp>
#include <nlohmann/json.hpp>
#include <unordered_map>
namespace beast = boost::beast;
namespace http = beast::http;
using json = nlohmann::json;
class Request {
http::request<http::string_body> req_;
std::unordered_map<std::string, std::string> params_;
std::unordered_map<std::string, std::string> query_;
json body_json_;
public:
Request(http::request<http::string_body> req)
: req_(std::move(req)) {
parse_query();
parse_body();
}
std::string method() const {
return std::string(req_.method_string());
}
std::string path() const {
auto target = req_.target();
auto pos = target.find('?');
return std::string(target.substr(0, pos));
}
std::string param(const std::string& name) const {
auto it = params_.find(name);
return it != params_.end() ? it->second : "";
}
std::string query(const std::string& name) const {
auto it = query_.find(name);
return it != query_.end() ? it->second : "";
}
template<typename T>
T body() const {
return body_json_.get<T>();
}
const json& json_body() const {
return body_json_;
}
std::string header(const std::string& name) const {
return std::string(req_[name]);
}
void set_param(const std::string& name, const std::string& value) {
params_[name] = value;
}
private:
void parse_query() {
auto target = req_.target();
auto pos = target.find('?');
if (pos == std::string_view::npos) return;
auto query_string = target.substr(pos + 1);
// Parse query_string: key1=value1&key2=value2
std::string qs(query_string);
size_t start = 0;
while (start < qs.size()) {
auto amp = qs.find('&', start);
auto pair = qs.substr(start, amp - start);
auto eq = pair.find('=');
if (eq != std::string::npos) {
query_[pair.substr(0, eq)] = pair.substr(eq + 1);
}
start = (amp == std::string::npos) ? qs.size() : amp + 1;
}
}
void parse_body() {
if (req_.body().empty()) return;
try {
body_json_ = json::parse(req_.body());
} catch (const json::exception&) {
// JSON parse failure
}
}
};
Response Class
class Response {
http::response<http::string_body> res_;
bool sent_ = false;
public:
Response() {
res_.version(11); // HTTP/1.1
res_.result(http::status::ok);
}
Response& status(unsigned code) {
res_.result(static_cast<http::status>(code));
return *this;
}
Response& set_header(const std::string& name, const std::string& value) {
res_.set(name, value);
return *this;
}
void json(const ::json& data) {
res_.set(http::field::content_type, "application/json");
res_.body() = data.dump();
res_.prepare_payload();
sent_ = true;
}
void send(const std::string& body = "") {
res_.body() = body;
res_.prepare_payload();
sent_ = true;
}
void send_file(const std::string& path) {
std::ifstream file(path, std::ios::binary);
if (!file) {
status(404).send("File not found");
return;
}
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
// Set MIME type
auto ext = std::filesystem::path(path).extension().string();
set_header("Content-Type", get_mime_type(ext));
send(content);
}
unsigned status_code() const {
return res_.result_int();
}
const auto& native() const { return res_; }
};
Key: Response provides fluent API (status().json()) for concise handler code.
4. Authentication Middleware
JWT Authentication
#include <jwt-cpp/jwt.h>
class JWTAuth {
std::string secret_;
public:
JWTAuth(const std::string& secret) : secret_(secret) {}
std::string generate(const std::string& user_id) {
return jwt::create()
.set_issuer("my-api")
.set_type("JWT")
.set_payload_claim("user_id", jwt::claim(user_id))
.set_expires_at(std::chrono::system_clock::now() + std::chrono::hours(24))
.sign(jwt::algorithm::hs256{secret_});
}
std::optional<std::string> verify(const std::string& token) {
try {
auto decoded = jwt::decode(token);
auto verifier = jwt::verify()
.allow_algorithm(jwt::algorithm::hs256{secret_})
.with_issuer("my-api");
verifier.verify(decoded);
return decoded.get_payload_claim("user_id").as_string();
} catch (const std::exception&) {
return std::nullopt;
}
}
};
// Auth middleware
auto auth_middleware(JWTAuth& jwt_auth) {
return [&jwt_auth](const Request& req, Response& res, auto next) {
auto auth_header = req.header("Authorization");
if (auth_header.empty() || !auth_header.starts_with("Bearer ")) {
res.status(401).json({{"error", "Unauthorized"}});
return; // Don't call next()
}
auto token = auth_header.substr(7); // Remove "Bearer "
auto user_id = jwt_auth.verify(token);
if (!user_id) {
res.status(401).json({{"error", "Invalid token"}});
return;
}
// Store user_id in request (requires const_cast or mutable context)
const_cast<Request&>(req).set_user_id(*user_id);
next(); // Continue to handler
};
}
// Usage
server.get("/profile", auth_middleware(jwt_auth),
[](const Request& req, Response& res) {
auto user_id = req.user_id();
auto profile = get_user_profile(user_id);
res.json(profile);
});
Key: Auth middleware validates JWT, stores user_id in request, and calls next() only on success.
5. Validation and Error Handling
Request Validation
class Validator {
public:
struct Rule {
std::string field;
std::function<bool(const json&)> check;
std::string message;
};
std::vector<Rule> rules_;
Validator& required(const std::string& field) {
rules_.push_back({
field,
[field](const json& data) { return data.contains(field); },
field + " is required"
});
return *this;
}
Validator& string(const std::string& field) {
rules_.push_back({
field,
[field](const json& data) {
return data.contains(field) && data[field].is_string();
},
field + " must be a string"
});
return *this;
}
Validator& min_length(const std::string& field, size_t len) {
rules_.push_back({
field,
[field, len](const json& data) {
return data.contains(field) &&
data[field].is_string() &&
data[field].get<std::string>().length() >= len;
},
field + " must be at least " + std::to_string(len) + " characters"
});
return *this;
}
Validator& email(const std::string& field) {
rules_.push_back({
field,
[field](const json& data) {
if (!data.contains(field) || !data[field].is_string()) return false;
std::string val = data[field];
std::regex email_regex(R"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})");
return std::regex_match(val, email_regex);
},
field + " must be a valid email"
});
return *this;
}
std::optional<std::vector<std::string>> validate(const json& data) {
std::vector<std::string> errors;
for (const auto& rule : rules_) {
if (!rule.check(data)) {
errors.push_back(rule.message);
}
}
return errors.empty() ? std::nullopt : std::make_optional(errors);
}
};
// Usage
server.post("/users", [](const Request& req, Response& res) {
Validator validator;
validator.required("email")
.string("email")
.email("email")
.required("password")
.min_length("password", 8);
auto errors = validator.validate(req.json_body());
if (errors) {
res.status(400).json({{"errors", *errors}});
return;
}
// Validation passed
auto user = create_user(req.body<User>());
res.status(201).json(user);
});
Global Error Handler
class ErrorHandler {
public:
static void handle(const std::exception& e, Response& res) {
if (auto* ve = dynamic_cast<const ValidationError*>(&e)) {
res.status(400).json({
{"error", "Validation failed"},
{"details", ve->errors()}
});
}
else if (auto* ne = dynamic_cast<const NotFoundException*>(&e)) {
res.status(404).json({
{"error", ne->what()}
});
}
else if (auto* ae = dynamic_cast<const AuthError*>(&e)) {
res.status(401).json({
{"error", ae->what()}
});
}
else {
res.status(500).json({
{"error", "Internal server error"},
{"message", e.what()}
});
}
}
};
// Error handling middleware
auto error_handler_middleware = [](const Request& req, Response& res, auto next) {
try {
next();
} catch (const std::exception& e) {
ErrorHandler::handle(e, res);
}
};
server.use(error_handler_middleware);
Key: Catch exceptions from handlers and convert to appropriate HTTP error responses.
6. Swagger Documentation
API Documentation Definition
struct APIDoc {
std::string path;
std::string method;
std::string summary;
std::string description;
json parameters;
json request_body;
json responses;
};
class SwaggerGenerator {
std::vector<APIDoc> docs_;
public:
void add_doc(const APIDoc& doc) {
docs_.push_back(doc);
}
json generate() {
json swagger = {
{"openapi", "3.0.0"},
{"info", {
{"title", "My API"},
{"version", "1.0.0"}
}},
{"paths", json::object()}
};
for (const auto& doc : docs_) {
if (!swagger[paths].contains(doc.path)) {
swagger[paths][doc.path] = json::object();
}
swagger[paths][doc.path][doc.method] = {
{"summary", doc.summary},
{"description", doc.description},
{"parameters", doc.parameters},
{"requestBody", doc.request_body},
{"responses", doc.responses}
};
}
return swagger;
}
};
// Usage
SwaggerGenerator swagger;
swagger.add_doc({
"/users",
"post",
"Create user",
"Create a new user account",
json::array(),
{
{"content", {
{"application/json", {
{"schema", {
{"type", "object"},
{"properties", {
{"email", {{"type", "string"}}},
{"password", {{"type", "string"}}}
}},
{"required", {"email", "password"}}
}}
}}
}}
},
{
{"201", {
{"description", "User created"},
{"content", {
{"application/json", {
{"schema", {
{"type", "object"},
{"properties", {
{"id", {{"type", "string"}}},
{"email", {{"type", "string"}}}
}}
}}
}}
}}
}},
{"400", {{"description", "Validation error"}}}
}
});
// Swagger UI endpoint
server.get("/api-docs", [&swagger](const Request& req, Response& res) {
res.json(swagger.generate());
});
Key: OpenAPI schema can be generated from code annotations or maintained alongside routes.
7. Complete REST API Example
Request Flow (Middleware → Route)
sequenceDiagram
participant C as Client
participant L as Logging
participant CO as CORS
participant A as Auth
participant H as Handler
C->>L: GET /users
L->>CO: next()
CO->>A: next()
A->>A: JWT verify
alt Auth success
A->>H: next()
H-->>C: 200 JSON
else Auth failure
A-->>C: 401 Unauthorized
end
Complete User Management API
// main.cpp - Complete user CRUD API
int main() {
RestServer server;
JWTAuth jwt_auth("my-secret-key-change-in-production");
SwaggerGenerator swagger;
// Global middlewares
server.use(logging_middleware);
server.use(cors_middleware);
server.use(error_handler_middleware);
// Public: login, register
server.post("/auth/login", [&jwt_auth](const Request& req, Response& res) {
auto body = req.json_body();
auto user = authenticate(body[email], body[password]);
if (!user) {
res.status(401).json({{"error", "Invalid credentials"}});
return;
}
res.json({
{"token", jwt_auth.generate(user->id)},
{"user", *user}
});
});
server.post("/auth/register", [](const Request& req, Response& res) {
Validator v;
v.required("email").email("email")
.required("password").min_length("password", 8);
auto err = v.validate(req.json_body());
if (err) {
res.status(400).json({{"errors", *err}});
return;
}
auto u = create_user(req.json_body());
res.status(201).json({{"id", u.id}, {"email", u.email}});
});
// Protected: CRUD
server.get("/users", auth_middleware(jwt_auth),
[](const Request& req, Response& res) {
auto page = req.query("page");
auto limit = req.query("limit");
res.json({{"users", get_users_paginated(page, limit)}});
});
server.get("/users/:id", auth_middleware(jwt_auth),
[](const Request& req, Response& res) {
auto u = get_user(req.param("id"));
u ? res.json(*u) : res.status(404).json({{"error", "Not found"}});
});
server.put("/users/:id", auth_middleware(jwt_auth),
[](const Request& req, Response& res) {
if (req.user_id() != req.param("id")) {
res.status(403).json({{"error", "Forbidden"}});
return;
}
res.json(update_user(req.param("id"), req.json_body()));
});
server.del("/users/:id", auth_middleware(jwt_auth),
[](const Request& req, Response& res) {
if (req.user_id() != req.param("id")) {
res.status(403).json({{"error", "Forbidden"}});
return;
}
delete_user(req.param("id"))
? res.status(204).send()
: res.status(404).json({{"error", "Not found"}});
});
// API docs
server.get("/api-docs", [&swagger](const Request&, Response& res) {
res.json(swagger.generate());
});
server.listen(8080);
return 0;
}
cURL Testing
# 1. Register
curl -X POST http://localhost:8080/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"secret1234","name":"John Doe"}'
# 2. Login
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"secret1234"}'
# Response: {"token":"eyJ...", "user":{"id":"...","email":"..."}}
# 3. List users (auth required)
curl -X GET http://localhost:8080/users?page=1&limit=10 \
-H "Authorization: Bearer eyJ..."
# 4. Get user
curl -X GET http://localhost:8080/users/123 \
-H "Authorization: Bearer eyJ..."
# 5. Update user
curl -X PUT http://localhost:8080/users/123 \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{"name":"John Doe 2","email":"new@example.com"}'
# 6. Delete user
curl -X DELETE http://localhost:8080/users/123 \
-H "Authorization: Bearer eyJ..."
8. Common Errors and Solutions
Problem 1: “Connection reset” / Request body too large
Cause: Request body too large or client early termination.
Solution: Set Beast parser.body_limit(1024*1024), return 413 Request Entity Too Large on exceed.
http::request_parser<http::string_body> parser;
parser.body_limit(1024 * 1024); // 1MB
if (/* body limit exceeded */) {
res.status(413).json({{"error", "Request entity too large"}});
}
Problem 2: JSON Parsing Failure (400)
Cause: Missing Content-Type: application/json or malformed JSON.
Solution: Check Content-Type, catch json::parse() exception, return 400.
void parse_body() {
if (req_.body().empty()) return;
auto content_type = req_[http::field::content_type];
if (content_type != "application/json") {
throw BadRequestException("Content-Type must be application/json");
}
try {
body_json_ = json::parse(req_.body());
} catch (const json::exception& e) {
throw BadRequestException("Invalid JSON: " + std::string(e.what()));
}
}
Problem 3: JWT Token Expired (401)
Cause: exp claim expired.
Solution: Implement POST /auth/refresh endpoint for refresh token exchange.
server.post("/auth/refresh", [&jwt_auth](const Request& req, Response& res) {
auto refresh_token = req.json_body()[refresh_token];
auto user_id = jwt_auth.verify_refresh(refresh_token);
if (!user_id) {
res.status(401).json({{"error", "Invalid refresh token"}});
return;
}
res.json({{"token", jwt_auth.generate(*user_id)}});
});
Problem 4: CORS Preflight Failure
Cause: OPTIONS request not handled or missing Access-Control-Allow-* headers.
Solution:
if (req.method() == "OPTIONS") {
res.set_header("Access-Control-Allow-Origin", "*");
res.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.status(204).send();
return; // Don't call next()!
}
Problem 5: Missing next() Call
Cause: Middleware doesn’t call next() on success.
Solution: Always call next() after successful validation.
// ✅ Correct pattern
if (!user_id) {
res.status(401).json({{"error", "Unauthorized"}});
return; // Don't call next()
}
const_cast<Request&>(req).set_user_id(*user_id);
next(); // Required!
Problem 6: Path Parameter Matching Failure
Cause: Special character escaping missing in :id → ([^/]+) conversion.
Solution: Use std::regex_replace to convert :param to ([^/]+), match with ^...$.
Problem 7: Data Race on Concurrent Requests
Cause: const_cast to store user_id in Request causes race when requests overlap.
Solution: Use per-request RequestContext struct for independent storage.
struct RequestContext {
std::string user_id;
std::string request_id;
};
// Store in thread-local or request-scoped shared_ptr
Problem 8: Missing 404 Handler
Cause: No route match, unhandled.
Solution: Return 404 when router.match() returns nullopt.
auto route = router.match(req.method(), req.path());
if (!route) {
res.status(404).json({{"error", "Not found"}});
return;
}
9. Best Practices
1. Middleware Registration Order
Middlewares execute in registration order. Register logging → CORS → error handler first, apply auth per-route.
server.use(logging_middleware);
server.use(cors_middleware);
server.use(error_handler_middleware);
server.get("/users", auth_middleware(jwt_auth), handler);
2. Unified JSON Response Format
Standardize success/failure with data or error keys. Include request_id for debugging.
// Success
{"data": {...}, "request_id": "..."}
// Error
{"error": "...", "details": [...], "request_id": "..."}
3. HTTP Method and Status Code Compliance
| Operation | Method | Success | Failure |
|---|---|---|---|
| Create | POST | 201 | 400, 409 |
| Read | GET | 200 | 404 |
| Update | PUT/PATCH | 200 | 400, 404 |
| Delete | DELETE | 204 | 404 |
4. Environment Variables for Config
Never hardcode JWT secrets. Use std::getenv("JWT_SECRET").
auto secret = std::getenv("JWT_SECRET");
if (!secret) {
throw std::runtime_error("JWT_SECRET not set");
}
JWTAuth jwt_auth(secret);
5. Async I/O and Thread Model
Run io_context on std::thread::hardware_concurrency() threads.
net::io_context ioc;
std::vector<std::thread> threads;
for (int i = 0; i < std::thread::hardware_concurrency(); ++i) {
threads.emplace_back([&ioc]() { ioc.run(); });
}
for (auto& t : threads) t.join();
6. API Versioning
Include version in URL path: /v1/users, /v2/users.
server.get("/v1/users", handler_v1);
server.get("/v2/users", handler_v2);
10. Performance Optimization
1. JSON Serialization Optimization
// Remove whitespace in production (smaller payload)
void json_response(const json& data) {
res_.set(http::field::content_type, "application/json");
res_.body() = data.dump(-1); // -1: no indentation, minimal size
res_.prepare_payload();
}
2. Connection Pooling (Keep-Alive)
// HTTP Keep-Alive for connection reuse
void prepare_response() {
res_.set(http::field::connection, "keep-alive");
res_.set(http::field::keep_alive, "timeout=60");
}
3. Response Buffer Pre-Allocation
// Pre-allocate buffer for large responses
void json(const json& data) {
std::string body = data.dump(-1);
res_.body().reserve(body.size() + 256); // Header overhead
res_.body() = std::move(body);
res_.prepare_payload();
}
4. Regex Caching
// Regex compilation is expensive
class Router {
std::unordered_map<std::string, std::regex> path_cache_;
std::regex get_or_compile(const std::string& path) {
auto it = path_cache_.find(path);
if (it != path_cache_.end()) return it->second;
auto compiled = compile_path_to_regex(path);
path_cache_[path] = compiled;
return compiled;
}
};
5. Async I/O Utilization
// Use Beast async instead of sync I/O
void handle_request() {
http::async_read(socket_, buffer_, req_,
[this](beast::error_code ec, std::size_t) {
if (!ec) {
process_request();
}
});
}
6. Performance Benchmarks
| Configuration | Sync (1 thread) | Async (1 thread) | Async (4 threads) |
|---|---|---|---|
| 1K req/s | ~800 | ~1,500 | ~5,000 |
| Avg latency | 1.2ms | 0.6ms | 0.2ms |
| Memory | 50MB | 80MB | 120MB |
| Key: Async + multi-thread provides 6x throughput improvement. |
11. Production Patterns
Pattern 1: Health Check Endpoint
// For Kubernetes / load balancers
server.get("/health", [](const Request&, Response& res) {
res.json({
{"status", "ok"},
{"version", "1.0.0"},
{"timestamp", std::time(nullptr)}
});
});
server.get("/health/ready", [](const Request&, Response& res) {
if (!check_database_connection()) {
res.status(503).json({{"status", "unhealthy"}});
} else {
res.json({{"status", "ready"}});
}
});
Pattern 2: Request ID Tracking
auto request_id_middleware = [](const Request& req, Response& res, auto next) {
auto req_id = req.header("X-Request-ID").empty()
? generate_uuid()
: req.header("X-Request-ID");
res.set_header("X-Request-ID", req_id);
const_cast<Request&>(req).set_request_id(req_id);
next();
};
Pattern 3: Rate Limiting
// Token bucket: 100 requests per minute per client
class RateLimiter {
std::unordered_map<std::string, std::pair<int, std::chrono::steady_clock::time_point>> limits_;
int max_ = 100;
std::chrono::seconds window_{60};
public:
bool allow(const std::string& client_id) {
auto now = std::chrono::steady_clock::now();
auto& [count, start] = limits_[client_id];
if (now - start > window_) {
count = 0;
start = now;
}
return count++ < max_;
}
};
// Return 429 Too Many Requests
auto rate_limit_middleware = [&limiter](const Request& req, Response& res, auto next) {
auto client_id = req.header("X-Client-ID");
if (!limiter.allow(client_id)) {
res.status(429).json({{"error", "Too many requests"}});
return;
}
next();
};
Pattern 4: Structured Logging
// JSON logs for ELK/CloudWatch
void log_request(const Request& req, const Response& res, auto duration) {
std::cerr << json{
{"method", req.method()},
{"path", req.path()},
{"status", res.status_code()},
{"duration_ms", duration.count()},
{"request_id", req.request_id()}
}.dump() << std::endl;
}
Pattern 5: Graceful Shutdown
std::atomic<bool> running{true};
std::signal(SIGTERM, [](int) { running = false; });
std::signal(SIGINT, [](int) { running = false; });
while (running) {
server.poll();
}
server.stop();
Pattern 6: Production Checklist
- [ ] JWT secret in environment variable (never hardcode)
- [ ] CORS Allow-Origin restricted to allowed domains (not "*")
- [ ] HTTPS only (HTTP redirect)
- [ ] Request body size limit (1MB-10MB)
- [ ] Rate limiting applied
- [ ] Health check endpoint exposed
- [ ] Structured logging (JSON)
- [ ] Graceful shutdown implemented
- [ ] Swagger UI only in dev environment
- [ ] Error responses don't leak stack traces
Summary
Key Points
- Routing: Regex + parameter extraction
- Middleware: Function chain + next() callback
- Authentication: JWT + Bearer token
- Validation: Rule-based Validator
- Documentation: Swagger/OpenAPI auto-generation
- Performance: Async I/O + multi-threading
Feature Comparison
| Feature | Implementation |
|---|---|
| Routing | Regex + parameter extraction |
| Middleware | Function chain + next() |
| Auth | JWT + Bearer token |
| Validation | Rule-based Validator |
| Docs | Swagger/OpenAPI generation |
Core Principles
- Express-style API for productivity
- Middleware for cross-cutting concerns
- JSON auto-parsing for convenience
- Type safety maintained
- Auto-documentation for maintainability
Implementation Checklist
Before Writing Code
- Is this the best approach for the problem?
- Can team members understand and maintain this?
- Does it meet performance requirements?
While Writing Code
- All compiler warnings resolved?
- Edge cases considered?
- Error handling appropriate?
During Code Review
- Intent clear?
- Test cases sufficient?
- Documentation present?
Related Articles
- C++ HTTP Framework from Scratch #48-2
- C++ REST API Server Complete Guide #31-2
- C++ REST API Client Guide #21-3
- C++ JSON Parsing Guide #21-2
Keywords
C++ REST API, Boost.Beast, routing, middleware, JWT authentication, Swagger, OpenAPI, Express-style, HTTP server One-line summary: Build Express-style REST API servers in C++ with routing, middleware, JWT auth, and Swagger docs for high-performance backends with microsecond response times.
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Build Express-style REST API servers in C++: routing with path parameters, middleware chain (logging, CORS, auth).
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ REST API 클라이언트 완벽 가이드 | CRUD·인증·에러 처리·프로덕션 패턴 [#21-3]
- C++ JSON 파싱 완벽 가이드 | nlohmann·RapidJSON·커스텀 타입·에러 처리·프로덕션 패턴
- C++ REST API 서버 완벽 가이드 | Beast 라우팅·JSON·미들웨어 [#31-2]
Keywords Covered in This Article (Related Search Terms)
This article covers C++, REST API, HTTP, routing, middleware, JWT, Swagger, Beast, authentication.