A lightweight, reusable Qt library for handling HTTP requests (GET / POST) with native JSON support, custom headers, and asynchronous error handling.
- Requirements
- Installation
- CMake Integration
- Usage
- Signals Reference
- Options Reference
- Tests
- Project Structure
- FAQ
| Tool | Minimum Version |
|---|---|
| Qt | 5.15 or Qt 6.x |
| CMake | 3.16+ |
| Compiler | C++17 (MSVC, GCC, Clang) |
Required Qt modules: Core, Network
This option lets you keep QApi up to date independently inside your project.
1. Add QApi as a submodule
# From the root of your project
git submodule add https://github.com/mecanes/QApi.git libs/QApi
git commit -m "add: QApi submodule"2. When cloning your project on a new machine
# Clone with submodules automatically
git clone --recurse-submodules https://github.com/your-org/your-project.git
# Or if you already cloned without the flag
git submodule update --init3. Update QApi to the latest version
cd libs/QApi
git pull origin main
cd ../..
git add libs/QApi
git commit -m "update: QApi to latest version"If you do not want to use Git Submodule, simply copy the QApi folder into your project.
MyProject/
libs/
QApi/ ← folder copied here
CMakeLists.txt
qapi.h
qapi.cpp
QApi_global.h
src/
main.cpp
CMakeLists.txt
⚠️ With this method, QApi updates must be applied manually.
Once QApi is present in your project, update your CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(MyProject LANGUAGES CXX)
set(CMAKE_AUTOMOC ON)
set(CMAKE_CXX_STANDARD 17)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network)
# ── 1. Declare QApi before your executable ──
add_subdirectory(libs/QApi)
# ── 2. Declare your executable ──
add_executable(MyProject
src/main.cpp
)
# ── 3. Link QApi to your project ──
target_link_libraries(MyProject
PRIVATE
QApi
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network
)✅ QApi's
target_include_directoriesis declaredPUBLIC, so you do not need to add it manually.
#include "qapi.h"// With QObject parent (recommended — automatic memory management)
QApi* api = new QApi(this);
// Without parent
QApi api;#include "qapi.h"
QApi* api = new QApi(this);
// Listen for the response
connect(api, &QApi::QApiReadyObject, this, [](const QJsonObject& obj, int status) {
qDebug() << "HTTP Status :" << status;
qDebug() << "Username :" << obj["pseudo"].toString();
qDebug() << "Email :" << obj["email"].toString();
});
// Listen for errors
connect(api, &QApi::QApiReadyErrorOccurred, this, [](const QString& err, int status) {
qDebug() << "Error" << status << ":" << err;
});
// Fire the request
api->Get(QUrl("https://api.myserver.com/user"));Expected JSON response:
{ "pseudo": "johndoe", "email": "john.doe@example.com" }If your endpoint returns a JSON array, connect to QApiReadyArray:
connect(api, &QApi::QApiReadyArray, this, [](const QJsonArray& arr, int status) {
qDebug() << "Items count:" << arr.size();
for (const QJsonValue& val : arr) {
QJsonObject item = val.toObject();
qDebug() << item["title"].toString();
}
});
api->Get(QUrl("https://api.myserver.com/games"));Expected JSON response:
[
{ "id": 1, "title": "Game One" },
{ "id": 2, "title": "Game Two" }
]You can pass custom headers and query parameters via QApi::Options:
QApi::Options opt;
// Custom headers
opt.headers.insert("Authorization", "Bearer my_jwt_token");
opt.headers.insert("X-App-Version", "1.0.0");
// Query params (?page=1&limit=20)
opt.query.addQueryItem("page", "1");
opt.query.addQueryItem("limit", "20");
api->Get(QUrl("https://api.myserver.com/games"), opt);
// Actual URL sent: https://api.myserver.com/games?page=1&limit=20By default, the body is encoded as application/json:
QJsonObject body;
body["username"] = "johndoe";
body["password"] = "mypassword123";
connect(api, &QApi::QApiReadyObject, this, [](const QJsonObject& obj, int status) {
qDebug() << "Token:" << obj["access_token"].toString();
});
api->Post(QUrl("https://api.myserver.com/auth/login"), body);Body sent:
{ "username": "johndoe", "password": "mypassword123" }To send application/x-www-form-urlencoded, use BodyType::FormUrlEncoded:
QJsonObject body;
body["grant_type"] = "password";
body["username"] = "johndoe";
body["password"] = "mypassword123";
QApi::Options opt;
opt.bodyType = QApi::BodyType::FormUrlEncoded;
api->Post(QUrl("https://api.myserver.com/oauth/token"), body, opt);
// Body sent: grant_type=password&username=johndoe&password=mypassword123All errors (invalid URL, network failure, JSON parse error) are reported through a single signal:
connect(api, &QApi::QApiReadyErrorOccurred, this,
[](const QString& message, int httpStatus) {
if (httpStatus == -1) {
// Error before the request (invalid URL, no network)
qDebug() << "Local error:" << message;
} else if (httpStatus == 401) {
qDebug() << "Unauthorized — token expired?";
} else if (httpStatus == 404) {
qDebug() << "Resource not found";
} else if (httpStatus >= 500) {
qDebug() << "Server error:" << message;
}
});Possible errors:
| Case | httpStatus | Message |
|---|---|---|
| URL missing scheme or host | -1 |
"Invalid API URL: missing scheme or host" |
| Server unreachable | -1 |
Qt system error message |
| Non-JSON response | HTTP code | "JSON parse error: ..." |
| HTTP error (401, 404, 500…) | HTTP code | Network error message |
⚠️ A URL like"localhost:3000"is considered invalid because Qt interprets it asscheme=localhostwith an empty host. Always use"http://localhost:3000".
// Full response (object or array)
void QApiReady(const QJsonDocument& json, int httpStatus);
// JSON object response → { "key": "value" }
void QApiReadyObject(const QJsonObject& obj, int httpStatus);
// JSON array response → [ {...}, {...} ]
void QApiReadyArray(const QJsonArray& arr, int httpStatus);
// Error (network, invalid URL, JSON parse failure)
void QApiReadyErrorOccurred(const QString& message, int httpStatus = -1);💡
QApiReadyis always emitted on success, followed by eitherQApiReadyObjectorQApiReadyArraydepending on the response type. Connect to the most specific signal for your use case.
QApi::Options opt;
// Custom HTTP headers (key / value as QByteArray)
opt.headers.insert("Authorization", "Bearer <token>");
opt.headers.insert("X-Custom-Header", "value");
// Body encoding for POST requests
opt.bodyType = QApi::BodyType::Json; // application/json (default)
opt.bodyType = QApi::BodyType::FormUrlEncoded; // application/x-www-form-urlencoded
// Query parameters appended to the URL
opt.query.addQueryItem("page", "1");
opt.query.addQueryItem("sort", "desc");
// Timeout in milliseconds (reserved for future use)
opt.timeoutMs = 30000; // 30s defaultQApi ships with a test suite based on Qt Test.
cd build
cmake --build . --target testApi
ctest --output-on-failureOr from Qt Creator: right-click on testApi → Run.
| Test | Description |
|---|---|
test_invalidUrl_emitsError |
URL without scheme → immediate synchronous error |
test_invalidUrl_async_emitsError |
Closed port → asynchronous network error |
test_get_realEndpoint |
Real GET on a public endpoint |
test_post_jsonBody |
POST with correctly encoded JSON body |
test_customHeaders |
Custom headers correctly forwarded to the server |
PASS : testApi::initTestCase()
PASS : testApi::test_invalidUrl_emitsError()
PASS : testApi::test_invalidUrl_async_emitsError()
PASS : testApi::test_get_realEndpoint()
PASS : testApi::test_post_jsonBody()
PASS : testApi::test_customHeaders()
PASS : testApi::cleanupTestCase()
Totals: 7 passed, 0 failed
QApi/
CMakeLists.txt ← shared library build
QApi_global.h ← QAPI_EXPORT macros (dllexport / dllimport)
qapi.h ← public interface
qapi.cpp ← implementation
QApiTest/
CMakeLists.txt ← test build
tst_testapi.cpp ← Qt Test suite
Q: Do I need to manage QApi memory manually?
No, as long as you pass a QObject parent to the constructor. Qt will automatically delete the instance when the parent is destroyed.
QApi* api = new QApi(this); // deleted when "this" is destroyedQ: Can I fire multiple requests in parallel with the same instance?
Yes. QNetworkAccessManager handles multiple concurrent requests. However, signals like QApiReady, QApiReadyObject, etc. will be emitted for each response. If you need to tell responses apart, create one instance per request or add an identifier in your callbacks.
// Two parallel requests with separate instances
auto* apiGames = new QApi(this);
auto* apiUser = new QApi(this);
connect(apiGames, &QApi::QApiReadyArray, this, &MyClass::onGamesReady);
connect(apiUser, &QApi::QApiReadyObject, this, &MyClass::onUserReady);
apiGames->Get(QUrl("https://api.myserver.com/games"));
apiUser->Get(QUrl("https://api.myserver.com/user"));Q: How do I attach a JWT token to all my requests?
Create a small wrapper in your project that injects the header automatically:
void MyApiService::authenticatedGet(const QUrl& url) {
QApi::Options opt;
opt.headers.insert("Authorization",
QString("Bearer %1").arg(m_token).toUtf8());
m_api->Get(url, opt);
}Q: Why does the URL localhost:3000 not work?
Qt parses "localhost:3000" as scheme=localhost with an empty host. Always provide the scheme explicitly:
// ❌ Wrong
api->Get(QUrl("localhost:3000/api/user"));
// ✅ Correct
api->Get(QUrl("http://localhost:3000/api/user"));Q: How do I update QApi in my project?
cd libs/QApi
git pull origin main # fetch the latest version
cd ../..
git add libs/QApi
git commit -m "update: QApi"Proprietary — Mecanes © 2026. All rights reserved.