본문으로 건너뛰기 C++ REST API Server Complete Guide | Beast Routing, JSON,...

C++ REST API Server Complete Guide | Beast Routing, JSON, Middleware [#31-2]

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:
  1. A Router class: regex-based path matching
  2. A middleware chain: logging → CORS → auth → handler
  3. Request/Response wrappers: simplified JSON parsing/building
  4. 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

CodeMeaningWhen to use it
200 OKSuccessGET, PUT, DELETE succeeded
201 CreatedCreatedPOST succeeded
204 No ContentNo contentDELETE succeeded (no body)
400 Bad RequestBad requestJSON parse failure, validation failure
401 UnauthorizedAuthentication requiredmissing or expired token
403 ForbiddenForbiddenauthenticated but lacking permission
404 Not FoundNot foundresource doesn’t exist
500 Internal Server ErrorServer erroran 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

ImplementationRequests/secAvg. latencyMemory
C++ Beast45,0002.2ms50MB
Node.js Express12,0008.3ms120MB
Python Flask3,50028ms80MB
Go Gin38,0002.6ms60MB
Test environment: 4 cores, 8GB RAM, 100 concurrent connections, 30 seconds

Optimization tips

  1. Use keep-alive: reusing connections avoids repeated 3-way handshakes
  2. Minimize JSON parsing: only parse the fields you need
  3. Thread pool: run io_context across multiple threads
  4. 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 /health endpoint
  • 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


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++ REST API, Beast HTTP server, routing, middleware, JSON API.

Summary

ItemDescription
Beasthttp::async_read → routing → middleware → handler → async_write
Routerregex-based path matching, parameter extraction
Middlewarelogging, CORS, and authentication chain
JSONparsed/built with nlohmann/json
Errorsa global exception handler, status-code mapping
Performance45,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.

Q. Where can I study this more deeply?

A. See the official Boost.Beast documentation, the nlohmann/json documentation, and REST API design guides. One-line summary: Beast, a Router, and middleware let you build an extensible REST API server. Next article: [C++ Practical Guide #31-3] Database Integration: SQLite and PostgreSQL Previous article: [C++ Practical Guide #31-1] Building a Chat Server: Multiple Clients and Message Broadcasting