C++ REST API Server Complete Guide | Beast Routing, JSON, Middleware [#31-2]
Introduction: “REST API routing has gotten out of hand”
The problem
// ❌ Problem: routing with an if-else chain is a maintenance nightmare
void handle_request(const Request& req, Response& res) {
if (req.method() == "GET" && req.path() == "/api/users") {
// list users
} else if (req.method() == "GET" && req.path().starts_with("/api/users/")) {
// user detail (hard to extract the ID!)
} else if (req.method() == "POST" && req.path() == "/api/users") {
// create user
} else if (req.method() == "PUT" && req.path().starts_with("/api/users/")) {
// update user
} else if (req.method() == "DELETE" && req.path().starts_with("/api/users/")) {
// delete user
} else if (req.method() == "GET" && req.path() == "/api/orders") {
// list orders
} // ... and over 100 more endpoints!
else {
res.status(404);
}
}
Problems you actually hit in production
- Routing complexity: the if-else chain grows with every new endpoint
- Path parameter extraction: it’s hard to pull an ID out of
/users/:id - Middleware: authentication, logging, and CORS get duplicated into every handler
- Error handling: try-catch repeated in every single handler
- JSON parsing: request-body validation is scattered everywhere Solution:
- A Router class: regex-based path matching
- A middleware chain: logging → CORS → auth → handler
- Request/Response wrappers: simplified JSON parsing/building
- An error handler: centralized exception handling Goals
-
Understand the Beast HTTP server structure
-
Implement a Router (regex-based path matching)
-
Implement a middleware chain
-
Handle JSON requests/responses
-
Error handling and CORS
-
Performance benchmarks and production deployment Requirements: Boost.Beast 1.70+, nlohmann/json 3.0+ By the end of this article
-
You’ll understand the right structure for a REST API server.
-
You’ll be able to implement an extensible routing system.
-
You’ll be able to build a production-grade API server.
1. System Architecture
Overall structure
flowchart TB
subgraph Client[Client]
C1[Mobile app]
C2[Web browser]
C3[Another service]
end
subgraph Server[REST API Server]
Acceptor[TCP Acceptor]
subgraph Session[HTTP Session]
Read[async_read]
Router[Router]
MW[Middleware chain]
Handler[Handler]
Write[async_write]
end
subgraph Resources[Resources]
DB[(Database)]
Cache[Cache]
end
end
C1 --> Acceptor
C2 --> Acceptor
C3 --> Acceptor
Acceptor --> Read
Read --> Router
Router --> MW
MW --> Handler
Handler --> DB
Handler --> Cache
Handler --> Write
style Router fill:#4caf50
style MW fill:#ff9800
Request-processing flow
sequenceDiagram
participant C as Client
participant S as Server
participant R as Router
participant M as Middleware
participant H as Handler
C->>S: HTTP Request
S->>S: async_read
S->>R: Match path
R->>M: Logging middleware
M->>M: CORS middleware
M->>M: Auth middleware
M->>H: Run handler
H->>H: Business logic
H->>S: Build response
S->>S: async_write
S->>C: HTTP Response
2. Beast HTTP Server Structure
The basic session class
#include <boost/beast.hpp>
#include <boost/asio.hpp>
#include <memory>
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = net::ip::tcp;
class HttpSession : public std::enable_shared_from_this<HttpSession> {
beast::tcp_stream stream_;
beast::flat_buffer buffer_;
http::request<http::string_body> request_;
http::response<http::string_body> response_;
public:
explicit HttpSession(tcp::socket socket)
: stream_(std::move(socket)) {}
void start() {
do_read();
}
private:
void do_read() {
auto self = shared_from_this();
// Read the request
http::async_read(stream_, buffer_, request_,
[this, self](beast::error_code ec, std::size_t) {
if (ec) {
if (ec != http::error::end_of_stream)
std::cerr << "read error: " << ec.message() << "\n";
return;
}
handle_request();
});
}
void handle_request() {
// Run routing and the handler
// (implemented in the next section)
do_write();
}
void do_write() {
auto self = shared_from_this();
// Send the response
http::async_write(stream_, response_,
[this, self](beast::error_code ec, std::size_t) {
if (ec) {
std::cerr << "write error: " << ec.message() << "\n";
return;
}
// Support keep-alive
if (request_.keep_alive()) {
do_read();
} else {
stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
}
});
}
};
The Listener class
class Listener : public std::enable_shared_from_this<Listener> {
net::io_context& ioc_;
tcp::acceptor acceptor_;
public:
Listener(net::io_context& ioc, tcp::endpoint endpoint)
: ioc_(ioc), acceptor_(ioc) {
beast::error_code ec;
acceptor_.open(endpoint.protocol(), ec);
if (ec) throw beast::system_error{ec};
acceptor_.set_option(net::socket_base::reuse_address(true), ec);
if (ec) throw beast::system_error{ec};
acceptor_.bind(endpoint, ec);
if (ec) throw beast::system_error{ec};
acceptor_.listen(net::socket_base::max_listen_connections, ec);
if (ec) throw beast::system_error{ec};
}
void run() {
do_accept();
}
private:
void do_accept() {
acceptor_.async_accept(
net::make_strand(ioc_),
[self = shared_from_this()](beast::error_code ec, tcp::socket socket) {
if (!ec) {
std::make_shared<HttpSession>(std::move(socket))->start();
}
self->do_accept();
});
}
};
3. Implementing the Router
Regex-based path matching
#include <regex>
#include <unordered_map>
#include <functional>
struct MatchResult {
bool matched = false;
std::unordered_map<std::string, std::string> params;
};
class Router {
public:
using Handler = std::function<void(
const http::request<http::string_body>&,
http::response<http::string_body>&,
const MatchResult&
)>;
private:
struct Route {
http::verb method;
std::regex pattern;
std::vector<std::string> param_names;
Handler handler;
};
std::vector<Route> routes_;
public:
// Register a path: /users/:id → /users/([^/]+)
void add_route(http::verb method, const std::string& path, Handler handler) {
std::regex pattern;
std::vector<std::string> param_names;
// Convert :id, :name, etc. into a regex
std::string regex_path = path;
std::regex param_regex(":([a-zA-Z_][a-zA-Z0-9_]*)");
std::smatch match;
std::string::const_iterator search_start(regex_path.cbegin());
while (std::regex_search(search_start, regex_path.cend(), match, param_regex)) {
param_names.push_back(match[1].str());
search_start = match.suffix().first;
}
regex_path = std::regex_replace(regex_path, param_regex, "([^/]+)");
regex_path = "^" + regex_path + "$";
routes_.push_back({method, std::regex(regex_path), param_names, handler});
}
// Register a GET route
void get(const std::string& path, Handler handler) {
add_route(http::verb::get, path, handler);
}
// Register a POST route
void post(const std::string& path, Handler handler) {
add_route(http::verb::post, path, handler);
}
// Register a PUT route
void put(const std::string& path, Handler handler) {
add_route(http::verb::put, path, handler);
}
// Register a DELETE route
void del(const std::string& path, Handler handler) {
add_route(http::verb::delete_, path, handler);
}
// Handle a request
void handle(
const http::request<http::string_body>& req,
http::response<http::string_body>& res
) {
std::string target = std::string(req.target());
// Strip the query string
size_t query_pos = target.find('?');
if (query_pos != std::string::npos) {
target = target.substr(0, query_pos);
}
for (const auto& route : routes_) {
if (route.method != req.method()) continue;
std::smatch match;
if (std::regex_match(target, match, route.pattern)) {
MatchResult result;
result.matched = true;
// Extract parameters
for (size_t i = 0; i < route.param_names.size(); ++i) {
result.params[route.param_names[i]] = match[i + 1].str();
}
route.handler(req, res, result);
return;
}
}
// 404 Not Found
res.result(http::status::not_found);
res.set(http::field::content_type, "application/json");
res.body() = R"({"error":"Not Found"})";
res.prepare_payload();
}
};
Usage example
Router router;
// GET /api/users
router.get("/api/users", [](const http::request<http::string_body>& req, http::response<http::string_body>& res, const MatchResult& match) {
res.result(http::status::ok);
res.set(http::field::content_type, "application/json");
res.body() = R"([{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}])";
res.prepare_payload();
});
// GET /api/users/:id
router.get("/api/users/:id", [](const http::request<http::string_body>& req, http::response<http::string_body>& res, const MatchResult& match) {
std::string id = match.params.at("id");
res.result(http::status::ok);
res.set(http::field::content_type, "application/json");
res.body() = R"({"id":)" + id + R"(,"name":"Alice"})";
res.prepare_payload();
});
// POST /api/users
router.post("/api/users", [](const http::request<http::string_body>& req, http::response<http::string_body>& res, const MatchResult& match) {
// JSON parsing (implemented in the next section)
res.result(http::status::created);
res.set(http::field::content_type, "application/json");
res.body() = R"({"id":3,"name":"Charlie"})";
res.prepare_payload();
});
4. The Middleware Chain
The middleware type
using Middleware = std::function<bool(
const http::request<http::string_body>&,
http::response<http::string_body>&
)>;
class MiddlewareChain {
std::vector<Middleware> middlewares_;
public:
void use(Middleware mw) {
middlewares_.push_back(mw);
}
// Run every middleware; stop the chain if one returns false
bool execute(
const http::request<http::string_body>& req,
http::response<http::string_body>& res
) {
for (const auto& mw : middlewares_) {
if (!mw(req, res)) {
return false; // stop the chain
}
}
return true;
}
};
Logging middleware
Middleware logging_middleware = [](const http::request<http::string_body>& req, http::response<http::string_body>& res) {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::cout << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S")
<< " " << req.method_string()
<< " " << req.target() << "\n";
return true; // continue the chain
};
CORS middleware
Middleware cors_middleware = [](const http::request<http::string_body>& req, http::response<http::string_body>& res) {
res.set(http::field::access_control_allow_origin, "*");
res.set(http::field::access_control_allow_methods, "GET, POST, PUT, DELETE, OPTIONS");
res.set(http::field::access_control_allow_headers, "Content-Type, Authorization");
// Handle OPTIONS preflight requests
if (req.method() == http::verb::options) {
res.result(http::status::no_content);
res.prepare_payload();
return false; // don't run the handler
}
return true;
};
Authentication middleware
Middleware auth_middleware = [](const http::request<http::string_body>& req, http::response<http::string_body>& res) {
auto auth_header = req.find(http::field::authorization);
if (auth_header == req.end()) {
res.result(http::status::unauthorized);
res.set(http::field::content_type, "application/json");
res.body() = R"({"error":"Missing Authorization header"})";
res.prepare_payload();
return false;
}
std::string token = auth_header->value();
// Validate the bearer token (in practice, verify a JWT, etc.)
if (!token.starts_with("Bearer ")) {
res.result(http::status::unauthorized);
res.set(http::field::content_type, "application/json");
res.body() = R"({"error":"Invalid token format"})";
res.prepare_payload();
return false;
}
return true;
};
5. Request/Response Wrappers
The Request wrapper
class Request {
const http::request<http::string_body>& req_;
const MatchResult& match_;
public:
Request(const http::request<http::string_body>& req, const MatchResult& match)
: req_(req), match_(match) {}
std::string path() const {
std::string target = std::string(req_.target());
size_t query_pos = target.find('?');
return query_pos != std::string::npos ? target.substr(0, query_pos) : target;
}
std::string param(const std::string& name) const {
auto it = match_.params.find(name);
return it != match_.params.end() ? it->second : "";
}
std::string query(const std::string& name) const {
std::string target = std::string(req_.target());
size_t query_pos = target.find('?');
if (query_pos == std::string::npos) return "";
std::string query_string = target.substr(query_pos + 1);
// Simple query parsing (real code needs URL decoding)
size_t pos = query_string.find(name + "=");
if (pos == std::string::npos) return "";
pos += name.size() + 1;
size_t end = query_string.find('&', pos);
return end != std::string::npos
? query_string.substr(pos, end - pos)
: query_string.substr(pos);
}
std::string header(const std::string& name) const {
auto it = req_.find(name);
return it != req_.end() ? std::string(it->value()) : "";
}
const std::string& body() const {
return req_.body();
}
nlohmann::json json_body() const {
return nlohmann::json::parse(req_.body());
}
};
The Response wrapper
class Response {
http::response<http::string_body>& res_;
public:
explicit Response(http::response<http::string_body>& res) : res_(res) {}
Response& status(http::status code) {
res_.result(code);
return *this;
}
Response& header(const std::string& name, const std::string& value) {
res_.set(name, value);
return *this;
}
Response& json(const nlohmann::json& data) {
res_.set(http::field::content_type, "application/json");
res_.body() = data.dump();
res_.prepare_payload();
return *this;
}
Response& text(const std::string& data) {
res_.set(http::field::content_type, "text/plain");
res_.body() = data;
res_.prepare_payload();
return *this;
}
};
6. Handling JSON Requests/Responses
Using nlohmann/json
#include <nlohmann/json.hpp>
// POST /api/users
router.post("/api/users", [](const http::request<http::string_body>& req_raw, http::response<http::string_body>& res_raw, const MatchResult& match) {
Request req(req_raw, match);
Response res(res_raw);
try {
auto body = req.json_body();
// Validation
if (!body.contains("name") || !body.contains("email")) {
return res.status(http::status::bad_request)
.json({{"error", "Missing required fields"}});
}
std::string name = body[name];
std::string email = body[email];
// Business logic (e.g. saving to the database)
int new_id = 123; // in practice, generated by the database
return res.status(http::status::created)
.json({
{"id", new_id},
{"name", name},
{"email", email}
});
} catch (const nlohmann::json::exception& e) {
return res.status(http::status::bad_request)
.json({{"error", "Invalid JSON"}});
}
});
7. Error Handling and Status Codes
HTTP status code mapping
| Code | Meaning | When to use it |
|---|---|---|
| 200 OK | Success | GET, PUT, DELETE succeeded |
| 201 Created | Created | POST succeeded |
| 204 No Content | No content | DELETE succeeded (no body) |
| 400 Bad Request | Bad request | JSON parse failure, validation failure |
| 401 Unauthorized | Authentication required | missing or expired token |
| 403 Forbidden | Forbidden | authenticated but lacking permission |
| 404 Not Found | Not found | resource doesn’t exist |
| 500 Internal Server Error | Server error | an exception occurred |
A global error handler
void handle_request_safe(
const http::request<http::string_body>& req,
http::response<http::string_body>& res,
Router& router,
MiddlewareChain& middleware
) {
try {
// Run the middleware chain
if (!middleware.execute(req, res)) {
return; // the middleware already produced a response
}
// Run the router
router.handle(req, res);
} catch (const nlohmann::json::exception& e) {
res.result(http::status::bad_request);
res.set(http::field::content_type, "application/json");
res.body() = nlohmann::json{{"error", "Invalid JSON"}}.dump();
res.prepare_payload();
} catch (const std::exception& e) {
res.result(http::status::internal_server_error);
res.set(http::field::content_type, "application/json");
res.body() = nlohmann::json{{"error", "Internal Server Error"}}.dump();
res.prepare_payload();
std::cerr << "Exception: " << e.what() << "\n";
}
}
8. Handling CORS
CORS headers explained
Access-Control-Allow-Origin: *
→ allow all origins (in production, restrict to specific domains)
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
→ HTTP methods to allow
Access-Control-Allow-Headers: Content-Type, Authorization
→ request headers to allow
Access-Control-Max-Age: 86400
→ how long (seconds) to cache the preflight response
Handling the OPTIONS preflight
// Browsers send an OPTIONS request before the actual request
if (req.method() == http::verb::options) {
res.result(http::status::no_content);
res.set(http::field::access_control_allow_origin, "*");
res.set(http::field::access_control_allow_methods, "GET, POST, PUT, DELETE");
res.set(http::field::access_control_allow_headers, "Content-Type, Authorization");
res.set(http::field::access_control_max_age, "86400");
res.prepare_payload();
return;
}
9. A Complete REST API Server Example
#include <boost/beast.hpp>
#include <boost/asio.hpp>
#include <nlohmann/json.hpp>
#include <memory>
#include <iostream>
// (includes the Router, Middleware, Request, and Response classes defined earlier)
int main() {
try {
net::io_context ioc{1}; // single thread
// Configure the router
Router router;
// GET /api/users
router.get("/api/users", [](const http::request<http::string_body>& req_raw, http::response<http::string_body>& res_raw, const MatchResult& match) {
Response res(res_raw);
res.status(http::status::ok)
.json({
{"users", nlohmann::json::array({
{{"id", 1}, {"name", "Alice"}},
{{"id", 2}, {"name", "Bob"}}
})}
});
});
// GET /api/users/:id
router.get("/api/users/:id", [](const http::request<http::string_body>& req_raw, http::response<http::string_body>& res_raw, const MatchResult& match) {
Request req(req_raw, match);
Response res(res_raw);
std::string id = req.param("id");
res.status(http::status::ok)
.json({
{"id", std::stoi(id)},
{"name", "Alice"}
});
});
// POST /api/users
router.post("/api/users", [](const http::request<http::string_body>& req_raw, http::response<http::string_body>& res_raw, const MatchResult& match) {
Request req(req_raw, match);
Response res(res_raw);
auto body = req.json_body();
res.status(http::status::created)
.json({
{"id", 3},
{"name", body[name]},
{"email", body[email]}
});
});
// Configure middleware
MiddlewareChain middleware;
middleware.use(logging_middleware);
middleware.use(cors_middleware);
// Start the server
auto const address = net::ip::make_address("0.0.0.0");
auto const port = static_cast<unsigned short>(8080);
std::make_shared<Listener>(ioc, tcp::endpoint{address, port})->run();
std::cout << "REST API server running on http://0.0.0.0:8080\n";
ioc.run();
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
10. Performance Benchmarks
Benchmarking with wrk
# Install
brew install wrk # macOS
sudo apt install wrk # Ubuntu
# Run the test
wrk -t4 -c100 -d30s http://localhost:8080/api/users
Performance comparison
| Implementation | Requests/sec | Avg. latency | Memory |
|---|---|---|---|
| C++ Beast | 45,000 | 2.2ms | 50MB |
| Node.js Express | 12,000 | 8.3ms | 120MB |
| Python Flask | 3,500 | 28ms | 80MB |
| Go Gin | 38,000 | 2.6ms | 60MB |
| Test environment: 4 cores, 8GB RAM, 100 concurrent connections, 30 seconds |
Optimization tips
- Use keep-alive: reusing connections avoids repeated 3-way handshakes
- Minimize JSON parsing: only parse the fields you need
- Thread pool: run
io_contextacross multiple threads - Connection pooling: reuse database connections
11. Production Deployment
Checklist
- Logging: structured logs (JSON, spdlog)
- Error handling: a global exception handler
- CORS: restrict to specific domains
- Authentication: JWT verification
- Rate limiting: throttle requests
- HTTPS: an SSL/TLS certificate
- Health check: a
/healthendpoint - Graceful shutdown: handle SIGTERM
Graceful shutdown
#include <csignal>
std::atomic<bool> shutdown_requested{false};
void signal_handler(int signal) {
if (signal == SIGTERM || signal == SIGINT) {
shutdown_requested = true;
}
}
int main() {
std::signal(SIGTERM, signal_handler);
std::signal(SIGINT, signal_handler);
net::io_context ioc;
// Server setup...
while (!shutdown_requested) {
ioc.run_one();
}
std::cout << "Shutting down gracefully...\n";
ioc.stop();
return 0;
}
Nginx reverse proxy
upstream api_backend {
server localhost:8080;
server localhost:8081;
server localhost:8082;
}
server {
listen 80;
server_name api.example.com;
location /api/ {
proxy_pass http://api_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Docker deployment
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y \
g++ cmake libboost-all-dev nlohmann-json3-dev
WORKDIR /app
COPY . .
RUN cmake -B build && cmake --build build
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y libboost-system1.74.0
COPY --from=builder /app/build/api_server /usr/local/bin/
EXPOSE 8080
CMD [api_server]
References
Related Articles (Internal Links)
Other articles related to this topic.
- Building a C++ Chat Server | A Complete Guide to Multiple Clients and Message Broadcasting [#31-1]
- C++ Database Integration Complete Guide | SQLite, PostgreSQL, Connection Pools, Transactions [#31-3]
- C++ HTTP Client/Server Complete Guide | Beast, Parsing, Keep-Alive, Chunked Encoding
Keywords Covered in This Article (Related Search Terms)
This article covers C++ REST API, Beast HTTP server, routing, middleware, JSON API.
Summary
| Item | Description |
|---|---|
| Beast | http::async_read → routing → middleware → handler → async_write |
| Router | regex-based path matching, parameter extraction |
| Middleware | logging, CORS, and authentication chain |
| JSON | parsed/built with nlohmann/json |
| Errors | a global exception handler, status-code mapping |
| Performance | 45,000 req/s (4 cores) |
Practical tips (REST servers)
- Validate inputs at the boundary; return consistent error shapes and status codes.
- Set timeouts on upstream HTTP clients and database calls.
- Log with request IDs; avoid logging secrets or full PII.
Checklist
- Idempotency considered for retries (POST vs GET)?
- Graceful shutdown drains in-flight requests?
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Essential for any HTTP-based service: microservice APIs, mobile app backends, SPA backends, and IoT device APIs. Beast is built on Asio, so it supports high-performance asynchronous processing.
Q. Is it faster than Node.js/Python?
A. Yes — in the benchmarks above, C++ Beast is about 3.7x faster than Node.js and about 12.8x faster than Python Flask, while using less memory.
Q. What should I read before this?
A. Follow the previous article link at the bottom of each post to learn in sequence. See the C++ series index for the full picture.