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 ...
});
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 ... */ });
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;
}
};
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:
-
qbm-http
Module: https://github.com/isndev/qbm-http - Official Examples: https://github.com/isndev/qb-examples/tree/main/examples/qbm/http
Top comments (0)