QtHttpServer routing API

Hi everybody. First of all, I want to say thank you for all of you for the comments
to the previous blog post.
Today, I'm going to talk about routing, how it works, and how we implement it.

Before I start, I want to clarify something. We've looked at many projects on GitHub, written in many languages - not only C++.
We found that most of the projects written in C++ are over-complicated, or very low level.
Sometimes, a "hello world" example takes 20-30 lines of code.
Projects written in other languages than C++ are very big, and they have big ecosystems. We aren't able to reimplement
all of the features that they have already done.
That's why we wanted to create something that is both simple and easily extensible at the same time.

Note: Big thank you to Flask. It inspired me to create that API.

What is routing and how does it work?

Routing is a process that is responsible for defining a callback for a specific request.
A route is a specific rule (callback) for a specific requested URL.

Let's take a look at an example:


http://www.qt.io/blog/dev // blogs for Dev loop
http://www.qt.io/blog/biz // blogs for Biz Circuit
http://www.qt.io/blog/2019/ // blogs for 2019 (dev loop + Biz Circuit)

Each address represents a specific request with a specific callback.
Also, you may have noticed that one of them has a parameter (http://www.qt.io/blog/2019/).
This URL requests whole blogs for the year 2019, so the callback should be able to understand which year is requested.
Such a route is called dynamic, and other two static.

Static routing

I think this is a very simple topic. Let's see how we can do it with QHttpServer.


QHttpServer server;

server.route("/dev/", [] () { return "All dev loop blogs"; });

server.route("/biz/", [] () { return "All biz loop blogs"; });

We just bound each path to its own callback. I think a more interesting topic is the dynamic routing.

Dynamic routing

First, I suggest splitting that task to sub-tasks. Then understand what we need to implement.

  • We need to have a mechanism to catch a parameter from a path in a url.
  • Convert the captured parameter to a type which we expect to have.

These are two main points, and the most important ones. I suggest going through each item in a row.

First, you might ask: "Is it so complicated? Can we just use a regular expression?"
I think, to understand it better, we should take the example from the top and try to solve it with a regexp.


/blog/(\\d+)/

Looks perfect. We can catch a parameter. How to convert the parameter to int?
The most common way is to just keep the parameter as a string, and then convert it manually.
However, this approach has several disadvantages:

  • If we have more than one parameter, we will need to do it for all of them.
  • Not everybody want to write regular expressions - especially for strings or floats.

How can we improve it? Let's take a look at our colleagues from other languages.
Modern frameworks (Django, flask, ruby on rails) all do it in the same way.
For example, it can be like this:


/blog/<int:year>/

Looks pretty clear. int used as an alias to the regular expression, and also now we know the type.
year is used to bind a captured parameter to a callback argument.
This approach allows you to easily add your custom types, which could look like this:


/blog/<HexInt:year>/

Good, now we know enough about routing to start to implement it.

QHttpServer::route

I suggest we do something like this:


route("/blog/<int:year>", [] (const QHttpServerRequest &request) {
    return blogs_by_year(request["year"].toInt());
});

route("/blog/<int:year>", [] (auto year) { return blogs_by_year(year.toInt()); });

It's not bad, but we can still improve on it. If you look closely on the second case, you'll see
that we don't really need the name of the parameter year in the path pattern. The reason for this is that we cannot bind a captured parameter to the callback argument in C++. In addition, we can't get a compile-time check of the path pattern type and the callback argument. So how can we improve this?

We can use static types. This way we get to use the compiler as a "controller", making sure that the type we get is a type we support.
To solve our second sub-task (converting, remember?), we can use a QVariant which can easily convert parameters.
So what can we do? We can combine them, like this:


QHttpServer server;
server.route("/blog/", [] (int year) {
    return blogs_by_year(year);
});

What do we get?

  • We don't need to duplicate the type name and an argument's name in the path pattern and callback.
  • Types will be automatically converted.
  • Additional advantage: compile time check of supported types.

Isn't this cool?!

Out of the box, we support several types: int, float, double, QString, QByteArray, QUrl, and a couple more...
You can also add support for your own types or even change the regex matching. Let me know in the comments if you want me to explain this in another blog post.

You might be asking: "How do I capture several parameters?". For example, I want to show all blog posts posted in February 2019:


QHttpServer server;
server.route("/blog/<arg>/<arg>", [] (int year, int month) {
    return blogs_by_year_and_month(year, month);
});

You may already have spotted the keyword <arg>. It works almost exactly like QString::arg, but doesn't support ordering.
That's why we don't use the same syntax as in QString::arg.

Also, we want to provide you with the possibility to create a REST API. For that, we need to split GET/POST/PUT/DELETE requests.
A small example:


server.route("/blog/", QHttpServerRequest::Method::Get, [] (int year) {
    return blogs_by_year(year);
});

or


server.route("/blog/", [] (int year, const QHttpServerRequest &req) {
    if (req.method() == QHttpServerRequest::Method::Get)
        return blogs_by_year(year);

return QHttpServerResponder::StatusCode::NotFound; });

Both of these are good, but I prefer the first one - it is shorter.

By default, QHttpServer::router works only with a path from a url. If you want to create a specific rule that works with a query,
you can inherit from QHttpServerRouterRule and pass it as a template argument to QHttpServer::route.

If you want to set up the HTTP response headers by yourself, then you can use the low-level API QHttpServerResponder.


QHttpServer server;
server.route("/blog/", [] (int year, QHttpServerResponder &&responder) {
    responder.write(blogs_by_year(year), "text/plain");
});

Note: QHttpServerResponder and QHttpServerRequest are special arguments that you can only use as the last argument of a callback.

There you have it: a simple web server that you can easily extend. If you’re interested in this project, don’t forget to clone it :)
And if you have suggestions or ideas on how to improve it further, you can consider contributing or provide some feedback in QTBUG-60105.

Thanks for reading!


Blog Topics:

Comments