A C++ HTTP router for you to copy-paste into your code
If you want a small, simple and easy to use HTTP request router for your web server application or micro-service this is for you.
By being small it is easy to understand and secure. You can see what it does, all it does.
When you are building a web service application a key component is the request routing. If your using a HTTP server framework/library/package that’s already included. You use it and it works, while you don’t know exactly how, it works. But is it “works” enough?
How does it works? Is it resilient? Safe? Efficient? Is something going on that I don’t know about and should? The engineer in me can’t help but to ask myself these questions.
You can avoid these questions by using the good old concept of copy pasting simple, straight to the point, code that does just that one thing. This HTTP router is one of those.
The objective
Let’s build a simple routing mechanism to direct HTTP requests to each endpoint handler function based on the request path. The handler function is the one that should be called to run the business logic associated to an endpoint.
For example, if your service provides the details of a product when a client send a GET
request to path /product/{id}
the handler function is the one that kicks-off the process of fetching the data from the data base and responding to the client.
Each endpoint has it’s own path
pattern and it can be used as a search key of mapping between request path and handler function that is responsible to implement the particular endpoint business logic.
The router will provide the methods to add routes to the router object and to call the appropriate handler function for a given request path.
The code
Let’s start by declaring the Router
class. Create a router.h
file and copy-paste the code bellow into it.
#pragma once
#include <string>
#include <vector>
#include "response.h"
#include "request.h"
class Router {
struct Route {
std::string url_regex;
std::string request_method;
void (*callback)(Request*, Response*);
};
std::vector<Route> routes;
public:
/*! Adds a route to the router
*
* \param url_regex String regex of the url path.
* \param request_method "GET", "POST", "PUT".
*/
void register_route(std::string url_regex,
std::string request_method,
void (*callback)(Request*, Response*) );
/*! Routes based on the path.
*
* It will match the path with the registered routes and call the callback
* to handle the specific request.
*
* \param req Request object.
* \param res Response object.
*/
void route_request(Request* req, Response* res);
};
Next, the Router
class implementation. Create a router.cpp
file and copy-paste the code bellow into it.
#include "router.h"
#include <regex>
void Router::register_route(std::string url_regex,
std::string request_method,
void (*callback)(Request*, Response*) )
{
// set a Route object to be pushed into the routes vector
Route route;
route.url_regex = url_regex;
route.request_method = request_method;
route.callback = callback;
routes.push_back(route);
}
void Router::route_request(Request* req, Response* res)
{
for (auto& r : routes) {
// match request path with route regex
std::regex pat {r.url_regex};
std::smatch match;
if (std::regex_match(req->header.path, match, pat)
&& (req->header.request_method.compare(r.request_method) == 0)) {
// call callback
r.callback(req, res);
// exit for loop
break;
}
}
}
And this is it for the router code.
How it works
We create a vector that contains in each entry the mapping between a regex
regular expression, that represents a request path pattern of a specific endpoint, and a pointer to the handler function for that specific endpoint.
When a request is received and parsed, we route it by matching the request path with the regular expressions in the routes vector. If a matching entry is found the associated handler function is called using the function pointer.
The routing procedure is done calling the route_request
method.
To add routes to the routing object we use the register_route
method.
Testing it
The best way to understand how to use it is to look at a code example. Let’s create a testing code for this purpose.
To test it in isolation, doing a unit test, you’ll need to create the Request
and Response
object.
These can be a class
(preferable) or a struct
.
For simplicity, and our goal here is just to do a unit test on the router code, we will use structs to mock these objects.
Create the request.h
as shown bellow.
#pragma once
#include <iostream>
struct Request {
std::string method;
std::string path;
}
Create the response.h
as shown bellow.
#pragma once
#include <iostream>
struct Request {
std::string body;
}
Create the test code entry code, the main.cpp
source file, by copying the code bellow.
#include "router.h"
#include <iostream>
void handle(Request* req, Response* res)
{
std::cout << "Endpoint handler here!" << std::endl;
}
int main()
{
// Create router
Router router;
// Register a route
router.register_route("^/product/([[:alnum:]]+)$", "GET", handle);
// Create mocks request and response object.
Request req;
Response res;
req.method = "GET";
req.path = "/product/123thisisaproductcode";
// Route
router.route_request(req, res);
return 0;
}
Building and running the above code should print in the terminal the text “Endpoint handler here!”.
I hope this is useful for you to speedup the development of you code without the need to rely on a third-party bundle that typically has a lot more than you need and it’s harder to audit and understand.
Take this code, make it yours and make it better!