Creating RESTful applications with Qt and Cutelyst
This mini tutorial aims to show you the fundamentals of creating a RESTful application with Qt, as a client and as a server with the help of Cutelyst.
Services with REST APIs have become very popular in recent years, and interacting with them may be necessary to integrate with services and keep your application relevant, as well as it may be interesting to replace your own protocol with a REST implementation.
REST is very associated with JSON, however, JSON is not required for a service to become RESTful, the way data is exchanged is chosen by the one who defines the API, ie it is possible to have REST exchanging messages in XML or another format. We will use JSON for its popularity, simplicity and due to the QJsonDocument class being present in the Qt Core module.
A REST service is mainly characterized by making use of the little-used HTTP headers and methods, browsers basically use GET to get data and POST to send form and file data, however REST clients will use other methods like DELETE, PUT and HEAD, concerning headers many APIs define headers for authentication, for example X-Application-Token can contain a key generated only for the application of a user X, so that if this header does not contain the correct data it will not have access to the data.
Let's start by defining the server API:
- /api/v1/users
- GET - Gets the list of users
- Answer: ["uuid1", "uuid2"]
- POST - Register new user
- Send: {"name": "someone", "age": 32}
- Answer: {"status": "ok / error", "uuid": "new user uuid", "error": "msg in case of error"}
- GET - Gets the list of users
- /api/v1/users/ - where UUID should be replaced by the user's UUID
- GET - Gets user information
- Answer: {"name": "someone", "age": 32}
- PUT - Update user information
- Send: {"name": "someone", "age": 57}
- Answer: {"status": "ok / error", "error": "msg in case of error"}
- DELETE - Delete user
- Answer: {"status": "ok / error", "error": "msg in case of error"}
- GET - Gets user information
For the sake of simplicity we will store the data using QSettings, we do not recommend it for real applications, but Sql or something like that escapes from the scope of this tutorial. We also assume that you already have Qt and Cutelyst installed, the code is available at https://github.com/ceciletti/example-qt-cutelyst-rest
Part 1 - RESTful Server with C ++, Cutelyst and Qt
First we create the server application:
$ cutelyst2 --create-app ServerREST
And then we will create the Controller that will have the API methods:
$ cutelyst2 --controller ApiV1
Once the new class has been instantiated in serverrest.cpp, init() method with:
#include "apiv1.h" bool ServerREST::init() { new ApiV1 (this); ...
Add the following methods to the file "apiv1.h"
C_ATTR(users, :Local :AutoArgs :ActionClass(REST)) void users(Context *c); C_ATTR(users_GET, :Private) void users_GET(Context *c); C_ATTR(users_POST, :Private) void users_POST(Context *c); C_ATTR(users_uuid, :Path('users') :AutoArgs :ActionClass(REST)) void users_uuid(Context *c, const QString &uuid); C_ATTR(users_uuid_GET, :Private) void users_uuid_GET(Context *c, const QString &uuid); C_ATTR(users_uuid_PUT, :Private) void users_uuid_PUT(Context *c, const QString &uuid); C_ATTR(users_uuid_DELETE, :Private) void users_uuid_DELETE(Context *c, const QString &uuid);
The C_ATTR macro is used to add metadata about the class that the MOC will generate, so Cutelyst knows how to map the URLs to those functions.
- :Local - Map method name to URL by generating /api/v1/users
- :AutoArgs - Automatically checks the number of arguments after the Context *, in users_uuid we have only one, so the method will be called if the URL is /api/v1/users/any-thing
- :ActionClass(REST) - Will load the REST plugin that will create an Action class to take care of this method, ActionREST will call the other methods depending on the called method
- :Private - Registers the action as private in Cutelyst, so that it is not directly accessible via URL
This is enough to have an automatic mapping depending on the HTTP method for each function, it is important to note that the first function (without _METHOD) is always executed, for more information see the API of ActionREST
For brevity I will show only the GET code of users, the rest can be seen in GitHub:
void ApiV1::users_GET(Context *c) { QSettings s; const QStringList uuids = s.childGroups(); c->response()->setJsonArrayBody(QJsonArray::fromStringList(uuids)); }
After all the implemented methods start the server:
cutelyst2 -r --server --app-file path_to_it
To test the API you can test a POST with curl:
curl -H "Content-Type: application/json" -X POST -d '{"name": "someone", "age": 32}' http://localhost:3000/api/v1/users
Okay, now you have a REST server application, made with Qt, with one of the fastest answers in the old west :)
No, it's serious, check out the benchmarks.
Now let's go to part 2, which is to create the client application that will consume this API.
Part 2 - REST Client Application
First create a QWidgets project with a QMainWindow, the goal here is just to see how to create REST requests from Qt code, so we assume that you are already familiar with creating graphical interfaces with it.
Our interface will be composed of:
- 1 - QComboBox where we will list users' UUIDs
- 1 - QLineEdit to enter and display the user name
- 1 - QSpinBox to enter and view user age
- 2 - QPushButton
- To create or update a user's registry
- To delete the user record
Once designed the interface, our QMainWindow sub-class needs to have a pointer to QNetworkAccessManager, this is the class responsible for handling communication with network services such as HTTP and FTP. This class works asynchronously, it has the same operation as a browser that will create up to 6 simultaneous connections to the same server, if you have made more requests at the same time it will put them in a queue (or pipeline them if set).
Then create a QNetworkAccessManager *m_nam; as a member of your class so we can reuse it. Our request to obtain the list of users will be quite simple:
QNetworkRequest request(QUrl("http://localhost:3000/api/v1/users")); QNetworkReply *reply = m_nam->get(request); connect(reply, &QNetworkReply::finished, this, [this, reply] { reply->deleteLater(); const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); const QJsonArray array = doc.array(); for (const QJsonValue &value : array) { ui->uuidCB->addItem(value.toString()); } });
This fills with the data via GET from the server our QComboBox, now we will see the registration code which is a little more complex:
QNetworkRequest request(QUrl("http://localhost:3000/api/v1/users")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QJsonObject obj { {"name", ui->nameLE->text()}, ("age", ui->ageSP->value()} }; QNetworkReply *reply = m_nam->post(request, QJsonDocument(obj).toJson()); connect(reply, &QNetworkReply::finished, this, [this, reply] { reply->deleteLater(); const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); const QJsonObject obj = doc.object(); if (obj.value("status").toString() == "ok") { ui->uuidCB->addItem(obj.value("uuid").toString()); } else { qWarning() << "ERROR" << obj.value("error").toString(); } });
With the above code we send an HTTP request using the POST method, like PUT it accepts sending data to the server. It is important to inform the server with what kind of data it will be dealing with, so the "Content-Type" header is set to "application/json", Qt issues a warning on the terminal if the content type has not been defined. As soon as the server responds we add the new UUID in the combobox so that it stays up to date without having to get all UUIDs again.
As demonstrated QNetworkAccessManager already has methods ready for the most common REST actions, however if you wanted to send a request of type OPTIONS for example will have to create a request of type CustomOperation:
m_nam->sendCustomRequest("OPTIONS", request);
Did you like the article? Help by giving a star to Cutelyst and/or supporting me on Patreon