DEV Community

ISNDEV
ISNDEV

Posted on

Building a REST API in C++: Advanced Routing & Middleware with `qbm-http`

Go beyond "Hello World." Learn how to build a feature-rich REST API in C++ with qbm-http, leveraging path parameters, powerful middleware, and the clean controller pattern for organized, maintainable code.

Target Audience: Intermediate to Advanced C++ developers building web APIs.

GitHub: https://github.com/isndev/qbm-http


A modern web framework needs more than just basic request handling. It needs a robust routing system, a way to handle cross-cutting concerns cleanly, and patterns for organizing code as an application grows. qbm-http provides all of these with a focus on performance and C++ language elegance.

In this article, we'll explore three powerful features: parameterized routing, middleware, and the controller pattern.

1. Expressive Routing: Parameters and Wildcards

Real-world APIs need to handle dynamic URLs, like fetching a user by their ID (/users/123). qbm-http's router makes this trivial.

  • Path Parameters: Use a colon (:) to define a named parameter.
  • Wildcard Routes: Use an asterisk (*) to catch all subsequent path segments.

The values are easily accessible inside your handler via the ctx->path_param("name") method.

// From examples/qbm/http/03_basic_routing.cpp

// GET /api/users/:id
router().get("/api/users/:id", [this](auto ctx) {
    // Retrieve the 'id' parameter from the path
    int user_id = std::stoi(ctx->path_param("id"));

    // ... find user by ID and send response ...
});

// GET /files/images/avatars/user.png
router().get("/files/*filepath", [this](auto ctx) {
    // 'filepath' will contain "images/avatars/user.png"
    std::string path = ctx->path_param("filepath");

    // ... serve the file from the filesystem ...
});
Enter fullscreen mode Exit fullscreen mode

2. Middleware: The Power of Composition

Middleware functions are the heart of a modern web framework. They are small, reusable handlers that inspect or modify a request before it reaches your main logic, or modify the response on its way out. They form a chain, processing requests in a specific order.

qbm-http's middleware is implemented as a simple lambda that receives the Context and a next function. You call next() to pass control to the next middleware in the chain.

This is perfect for:

  • Logging: Recording every incoming request.
  • Authentication: Checking for a valid JWT or API key.
  • CORS: Adding cross-origin resource sharing headers.
  • Compression: Gzip/deflate encoding responses.
  • Caching: Adding cache-control headers.

Here is a corrected example of a logging and timing middleware that correctly handles asynchronous operations using LifecycleHook.

// A single middleware for logging and timing, demonstrating the correct async pattern.
router().use([](auto ctx, auto next) {
    // 1. Store the start time in the context. This is safe across async boundaries.
    ctx->set("start_time", std::chrono::high_resolution_clock::now());

    std::cout << "[LOG] Request Start: " << ctx->request().method()
              << " " << ctx->request().uri().path() << std::endl;

    // 2. Add a lifecycle hook. This lambda will be executed by the framework
    //    just before the response is sent to the client.
    ctx->add_lifecycle_hook([](auto& context, auto point) {
        // We only care about the point just before sending the response.
        if (point != qb::http::routing::HookPoint::PRE_RESPONSE_SEND) {
            return;
        }

        // 3. Calculate the duration now that we know the response is ready.
        auto start_time_any = context.template get<std::chrono::high_resolution_clock::time_point>("start_time");
        if (start_time_any) {
            auto end_time = std::chrono::high_resolution_clock::now();
            auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - *start_time_any);

            // Add the timing header to the final response.
            context.response().add_header("X-Response-Time-us", std::to_string(duration.count()));

            std::cout << "[LOG] Request End: " << context.request().method()
                      << " " << context.request().uri().path()
                      << " -> " << context.response().status().code()
                      << " in " << duration.count() << "us" << std::endl;
        }
    });

    // 4. Pass control to the next middleware or route handler.
    //    If the handler is async, this call returns immediately.
    next();
});

// A simple authentication middleware for a specific group of routes
auto protected_group = router().group("/api");
protected_group->use([](auto ctx, auto next) {
    std::string token = ctx->request().header("Authorization");

    if (token != "Bearer secret-token-123") {
        ctx->response().status() = qb::http::Status::UNAUTHORIZED;
        ctx->response().body() = R"({"error": "Invalid token"})";
        // Stop processing and send the response immediately
        ctx->complete(); 
        return;
    }

    // Token is valid, proceed to the handler
    next();
});

protected_group->get("/profile", [](auto ctx) { /* ... user profile logic ... */ });
Enter fullscreen mode Exit fullscreen mode

3. The Controller Pattern: Organizing Your API

As your API grows, putting all your logic into a single server class becomes unmanageable. The Controller pattern solves this by grouping related routes into dedicated classes.

A qb::http::Controller is a class that inherits from IHandlerNode and defines its own routes. You can then "mount" this controller on a specific path prefix in your main router.

From examples/qbm/http/05_controller_pattern.cpp

// 1. Define the Controller class for user-related routes
class UserController : public qb::http::Controller<qb::http::DefaultSession> {
private:
    // This controller can have its own state, e.g., a DB connection
    qb::unordered_map<int, qb::json> _users;
    int _next_id = 1;

public:
    UserController() { /* ... initialize _users ... */ }

    // This method is called by the router to set up the controller's routes
    void initialize_routes() override {
        // Define routes relative to the controller's mount point
        // e.g., if mounted at "/api/users", this becomes GET /api/users/
        get("/", MEMBER_HANDLER(&UserController::list_users));

        // This becomes GET /api/users/:id
        get("/:id", MEMBER_HANDLER(&UserController::get_user));

        // This becomes POST /api/users/
        post("/", MEMBER_HANDLER(&UserController::create_user));
    }

    // 2. Implement the route handlers as member functions
    void list_users(std::shared_ptr<qb::http::Context<qb::http::DefaultSession>> ctx) {
        // ... logic to list all users ...
        ctx->response().body() = qb::json{{"users", _users}};
        ctx->complete();
    }

    void get_user(std::shared_ptr<qb::http::Context<qb::http::DefaultSession>> ctx) {
        // ... logic to get a single user by id ...
        ctx->complete();
    }

    void create_user(std::shared_ptr<qb::http::Context<qb::http::DefaultSession>> ctx) {
        // ... logic to create a new user ...
        ctx->complete();
    }
};

// 3. In your main server actor's onInit()
class ApiServer : public qb::Actor, public qb::http::Server<> {
public:
    bool onInit() override {
        // ...
        // Mount the UserController at the "/api/users" path
        router().controller<UserController>("/api/users");

        router().compile();
        // ...
        return true;
    }
};
Enter fullscreen mode Exit fullscreen mode

This pattern provides a clean separation of concerns, making your API more modular, testable, and easier to navigate.

By combining expressive routing, a flexible middleware chain, and the organizational power of controllers, qbm-http provides a robust foundation for building sophisticated, high-performance web services in C++.

Explore the qbm-http module and its examples on GitHub:

Top comments (0)