Quick Start Your REST Client with CppREST
Online services are getting more and more popular as the demand grows exponentially. Millions of connected devices already take advantage of cloud and edge services using REST. Representational State Transfer is a highly scalable and easy to use API protocol. In this post you will find the details for building your very simple C++ client that talks to any RESTful service using Microsoft’s C++REST library.
Operations on REST Services
Since REST provides full functional API to its clients for accessing, creating and manipulating data, it is not easy to cover all the details. So let’s focus on some basics. A RESTful API server provides GET, POST, PUT, PATCH, DELETE methods (and more). I will build my tutorial around Reqres mock server to demonstrate several functions.
Accessing the Data with GET
Not different than HTTP, GET method accesses the data on the server. Since REST server keeps data separately under different paths, issuing GET on the desired path will bring the data. Let’s try this endpoint first https://reqres.in/api/users that brings us the following JSON populated with fake users.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
{ "page": 1, "per_page": 6, "total": 12, "total_pages": 2, "data": [ { "id": 1, "email": "george.bluth@reqres.in", "first_name": "George", "last_name": "Bluth", "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/calebogden/128.jpg" }, { "id": 2, "email": "janet.weaver@reqres.in", "first_name": "Janet", "last_name": "Weaver", "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg" } ] } |
Depending on the server implementation and the context itself, it is also possible to filter the data using different path. As an example https://reqres.in/api/users/1 brings us the user with id 1 alone.
1 2 3 4 5 6 7 8 9 |
{ "data": { "id": 1, "email": "george.bluth@reqres.in", "first_name": "George", "last_name": "Bluth", "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/calebogden/128.jpg" } } |
The same data can be requested with also using the query https://reqres.in/api/users?id=1. Another useful query is for paging https://reqres.in/api/users?page=1.
Creating New Data with POST
To create a new user, POST new user details to the endpoint https://reqres.in/api/users in JSON format. Server will respond 201 with the newly created user data including the assigned unique id.
1 2 3 4 5 6 |
{ "first_name": "atakan", "last_name": "sarioglu", "id": "999", "createdAt": "2019-01-01T01:01:01.000Z" } |
Updating/Editing with PUT or PATCH
There are two different ways of changing data on the server. If we send the data as JSON including all the required fields (just like creating a new data) the server will overwrite the data at https://reqres.in/api/users/1, notice the user id appended in the end. The server will respond 200 with the following. If the user with id 1 does not exist, it may be created instead of updating.
1 2 3 4 5 |
{ "first_name": "atakan", "last_name": "sarioglu", "updatedAt": "2019-01-01T01:01:01.000Z" } |
Other option is making PATCH request with incomplete fields. It is useful if the data size is large and we don’t want to send the fields that we don’t want to change. The only difference to PUT is that the data to be patched must exist on the server.
Deleting Data with DELETE
Finally, it is very easy to delete an entry. Make a DELETE request to the correct endpoint with the desired id https://reqres.in/api/users/1 and the record will be deleted. Usually 204 is responded with no content.
For more information on the operations refer to this website.
REST Client Implementation in C++
Implementing your native client is not extremely hard but don’t invent the wheel again. CppREST (a.k.a. Casablanca) provides a nice experience for C++ that has everything you need in one place.
Development Environment
For this simple demo code, I’m using Visual Studio 2017 with cpprestsdk.v141 package installed via NuGet and dialect is C++17. I recommend the same since this is the least painful way to take-off. All the code explained in this post is also pushed to GitHub, feel free to use it.
Making GET Request
Writing to File
Although it is not the easiest one, I will start with the same example code that CppREST wiki provides. Try this code on a new project to see if your environment works fine.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
#include <iostream> #include <cpprest/http_client.h> #include <cpprest/filestream.h> #include <cpprest/uri.h> #include <cpprest/json.h> using namespace utility; using namespace web; using namespace web::http; using namespace web::http::client; using namespace concurrency::streams; int main() { // Create a file stream to write the received file into it. auto fileStream = std::make_shared<ostream>(); // Open stream to output file. pplx::task<void> requestTask = fstream::open_ostream(U("users.json")) // Make a GET request. .then([=](ostream outFile) { *fileStream = outFile; // Create http_client to send the request. http_client client(U("https://reqres.in")); // Build request URI and start the request. return client.request(methods::GET, uri_builder(U("api")).append_path(U("users")).to_string()); }) // Get the response. .then([=](http_response response) { // Check the status code. if (response.status_code() != 200) { throw std::runtime_error("Returned " + std::to_string(response.status_code())); } // Write the response body to file stream. response.body().read_to_end(fileStream->streambuf()).wait(); // Close the file. return fileStream->close(); }); // Wait for the concurrent tasks to finish. try { while (!requestTask.is_done()) { std::cout << "."; } } catch (const std::exception &e) { printf("Error exception:%s\n", e.what()); } return 0; } |
Here are many things to explain. First of all, CppREST uses a concurrency framework named PPLX (a version of PPL) that starts parallel threads in the background and runs the tasks as programmed. I.e. fstream::open_ostream(U("users.json")) creates a PPLX task that will open a file stream and will return the stream object on exit. Later we define the next step using .then(...) method that gets the return value of the previous task and passes to the callable object (i.e. lambda expression). We can create a chain of operations that needs to be done in order and PPLX will take care of it in the background for us.
We have two options to keep track of the concurrent operation. We can use wait() method that will block the current caller thread until the concurrent task is finished or we can poll the task using is_done() before getting the return value using get() method (more information is here).
If everything is smooth, you will see the downloaded users.json file on your disk.
Parsing the JSON Response
As a more common use case, we can interpret the response which is in JSON format.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
int main() { // Make a GET request. auto requestJson = http_client(U("https://reqres.in")) .request(methods::GET, uri_builder(U("api")).append_path(U("users")).append_query(U("id"), 1).to_string()) // Get the response. .then([](http_response response) { // Check the status code. if (response.status_code() != 200) { throw std::runtime_error("Returned " + std::to_string(response.status_code())); } // Convert the response body to JSON object. return response.extract_json(); }) // Get the data field. .then([](json::value jsonObject) { return jsonObject[U("data")]; }) // Parse the user details. .then([](json::value jsonObject) { std::wcout << jsonObject[U("first_name")].as_string() << " " << jsonObject[U("last_name")].as_string() << " (" << jsonObject[U("id")].as_integer() << ")" << std::endl; }); // Wait for the concurrent tasks to finish. try { requestJson.wait(); } catch (const std::exception &e) { printf("Error exception:%s\n", e.what()); } return 0; } |
The code above successfully brings the user data and prints George Bluth (1) . Be careful about the strings when using C++REST sdk since it always uses unicode instead of std::string. Both the keys and values of JSON objects must be in U("...") format.
Making POST Request
This part gets tricky since we have to provide the details for a new user. We can create our user with json::value() and set the corresponding keys. Finally we will add the serialized json to the request body.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// Create user data as JSON object and make POST request. auto postJson = pplx::create_task([]() { json::value jsonObject; jsonObject[U("first_name")] = json::value::string(U("atakan")); jsonObject[U("last_name")] = json::value::string(U("sarioglu")); return http_client(U("https://reqres.in")) .request(methods::POST, uri_builder(U("api")).append_path(U("users")).to_string(), jsonObject.serialize(), U("application/json")); }) // Get the response. .then([](http_response response) { // Check the status code. if (response.status_code() != 201) { throw std::runtime_error("Returned " + std::to_string(response.status_code())); } // Convert the response body to JSON object. return response.extract_json(); }) // Parse the user details. .then([](json::value jsonObject) { std::wcout << jsonObject[U("first_name")].as_string() << " " << jsonObject[U("last_name")].as_string() << " (" << jsonObject[U("id")].as_string() << ")" << std::endl; }); |
After this task finishes executing, we can see atakan sarioglu (999) on stdout. Server return code is 201 which means CREATED. Note that we used pplx::create_task(...) in order to create the JSON object before making the request.
Updating Data on Endpoint
Full Update with PUT
Now all we have to do is sending a new user with the PUT request. The mock server will act as if the data is modified but don’t forget that it is volatile.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Make PUT request with {"name": "atakan", "location": "istanbul"} data. auto putJson = http_client(U("https://reqres.in")) .request(methods::PUT, uri_builder(U("api")).append_path(U("users")).append_path(U("1")).to_string(), U("{\"name\": \"atakan\", \"location\": \"istanbul\"}"), U("application/json")) // Get the response. .then([](http_response response) { if (response.status_code() != 200) { throw std::runtime_error("Returned " + std::to_string(response.status_code())); } // Convert the response body to JSON object. return response.extract_json(); }) // Parse the user details. .then([](json::value jsonObject) { std::wcout << jsonObject[U("name")].as_string() << " " << jsonObject[U("location")].as_string() << std::endl; }); |
On the console, atakan istanbul should be displayed.
Partial Update with PATCH
Similarly the following code is for PATCH request.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Make PATCH request with {"name": "sarioglu"} data. auto patchJson = http_client(U("https://reqres.in")) .request(methods::PATCH, uri_builder(U("api")).append_path(U("users")).append_path(U("1")).to_string(), U("{\"name\": \"sarioglu\"}"), U("application/json")) // Get the response. .then([](http_response response) { if (response.status_code() != 200) { throw std::runtime_error("Returned " + std::to_string(response.status_code())); } // Print the response body. std::wcout << response.extract_json().get().serialize() << std::endl; }); |
Observe {"name":"sarioglu","updatedAt":"2019-01-01T01:01:01.000Z"} on the output.
Making DEL Request
The simplest operation is DELETE which corresponds to DEL in C++REST SDK.
1 2 3 4 5 6 7 8 9 |
// Make DEL request. auto deleteJson = http_client(U("https://reqres.in")) .request(methods::DEL, uri_builder(U("api")).append_path(U("users")).append_path(U("1")).to_string()) // Get the response. .then([](http_response response) { std::wcout << "Deleted: " << std::boolalpha << (response.status_code() == 204) << std::endl; }); |
When server returns with 204, there is no content, means DEL is successful. On error we could get 404.
Conclusion
REST is an important tool and CppREST collects many nice tools in one place which brings a lot of comfort. However, a disadvantage is that it enforces wide strings (on windows) even when your implementation does not need it. Another problem arises in case of inappropriate using directives since C++REST redefines some STL elements i.e. Concurrency::streams::fstream is ambiguous to std::fstream.
Have a REST until the next post 😴