C++ REST API Server Complete Guide | Routing· Middleware
이 글의 핵심
Build Express-style REST API servers in C++: routing, middleware, JWT auth, Swagger docs, and production patterns for high-performance backends.
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":"[email protected]","password":"secret1234","name":"John Doe"}'
# 2. Login
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","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":"[email protected]"}'
# 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.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Build Express-style REST API servers in C++: routing with path parameters, middleware chain (logging, CORS, auth), JSON … 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ REST API 클라이언트 완벽 가이드 | CRUD·인증·에러 처리·프로덕션 패턴 [#21-3]
- C++ JSON 파싱 완벽 가이드 | nlohmann·RapidJSON·커스텀 타입·에러 처리·프로덕션 패턴
- C++ REST API 서버 완벽 가이드 | Beast 라우팅·JSON·미들웨어 [#31-2]
이 글에서 다루는 키워드 (관련 검색어)
C++, REST API, HTTP, routing, middleware, JWT, Swagger, Beast, authentication 등으로 검색하시면 이 글이 도움이 됩니다.