Create a minimal C++ HTTP service with Oat++ and Bazel
Published: 2024-07-23This blog post provides step-by-step instructions on how to build a minimal C++ HTTP service using the Oat++ framework. We will jump through the hoops necessary to build this project with the Bazel build tool, which is required because Oat++ is not built with Bazel and uses CMake instead.
We're going to ignore the CMake build and tell Bazel to compile the project itself. This is harder to maintain in the long run and we will have to validate that the project builds every time we update our Oat++ library version. Oat++ makes our work a little easier because it doesn't have external dependencies or special build steps.
I might find a better way to do this in the future.
The code in this post is available at cstroe/oatpp-minimal-http
.
Installing Bazel
I used the bazelbuild/bazelisk
CLI to run Bazel. Download it, extract it to a
directory on your path, and then symlink the bazelisk
CLI to bazel
.
In my example, let's say $HOME/Bin
is where I like to put executable programs on my path:
cd $HOME/Bin
wget https://github.com/bazelbuild/bazelisk/releases/download/v1.20.0/bazelisk-linux-amd64
ln -s ./bazelisk-linux-amd64 ./bazel
You can run bazel
and have Bazelisk set itself up (requires Internet).
Initial repository
To start off, we can manually create a new directory for our project versioned with git:
mkdir minimal_http
cd minimal_http
git init .
NOTE: Make sure you install git
if you don't have it.
Bazel repository setup
Bazel requires at least one specially named file at the root directory of your repository. For our repository, we'll start
with creating a MODULE.bazel
file, so that we can easily bring in some of our dependencies from the
Bazel Central Registry.
touch MODULE.bazel
Content for MODULE.bazel
:
module(
name = "minimal-http",
)
bazel_dep(name = "hermetic_cc_toolchain", version = "3.1.0")
In this case, we depend on uber/hermetic_cc_toolchain
so that
we don't have to install a C++ compiler on our system and also to make our builds more reproducible. The downside to
using hermetic_cc_toolchain
is that it currently only supports C17.
Depending on Oat++
Now we'll use a file named WORKSPACE
which will contain Bazel commands for declaring our dependency on
Oat++.
touch WORKSPACE
Start with this content in WORKSPACE
:
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "oatpp",
build_file = "@//deps/oatpp.BUILD",
sha256 = "eb6e217187ebe3e42382130163830e94eb2b7e067187f6024e8c745889f012c5",
strip_prefix = "oatpp-1.3.0-latest/src",
url = "https://github.com/oatpp/oatpp/archive/refs/tags/1.3.0-latest.zip",
)
The first line loads the http_archive
build rule. The next
block runs the http_archive
build rule to download the Oat++ version 1.3.0 source code.
Explanation of the http_archive
build rule arguments
Argument | Purpose |
---|---|
name | defines a short name which becomes the repository name of this dependency |
url | where to download the archive |
sha256 | the sha256sum of the archive for verification |
strip_prefix | extracts only the files in the specified sub-directory from the archive |
build_file | points to a Bazel build file that will define build rules for the oatpp repository. This is how we will tell Bazel to build this dependency. |
NOTE: The value of build_file
is a path to a file. It uses a special prefix @//
which matches our current repository. Then the path in the repository is deps/oatpp.BUILD
.
Directory for dependency build files
We also want a directory to keep our build files for our dependencies. Let's call it deps
.
mkdir deps
touch deps/BUILD
NOTE: Bazel requires a special file named BUILD
in every directory in order to recognize it as a Bazel package.
Without this, Bazel will complain that it can't find the files in that directory.
Now, let's create a new file deps/oatpp.BUILD
in our repository:
cc_library(
name = "main",
srcs = glob([
"oatpp/**/*.cpp",
]),
hdrs = glob([
"oatpp/**/*.hpp",
]),
copts = [
"-Iexternal/oatpp"
],
visibility = ["//visibility:public"],
)
Here we're declaring the entire Oat++ dependency as a single C++ library. This is not how projects are normally
built with Bazel, since it means longer build times. If Oat++ were to be converted to use Bazel, the build steps
would be many cc_library
build rules, allowing us to only depend on the C++ libraries we need for our code, and
leading to smaller builds.
We also have to add a special include flag to the compiler using copts
so that our external dependency headers
and code are able to be imported. With Bazel builds this is not required.
Building Oat++
At this point, we can run Bazel to build our Oat++ dependency.
bazel build @oatpp//:main
Web app
Finally, let's write our code for our web app. First, create a package:
mkdir webapp
cd webapp
touch BUILD
Then populate the following files:
BUILD
cc_library(
name = "static_controller",
hdrs = ["StaticController.hpp"],
deps = ["@oatpp//:main"],
)
cc_library(
name = "status_dto",
hdrs = ["StatusDto.hpp"],
copts = ["-Iexternal/oatpp"],
deps = ["@oatpp//:main"],
)
cc_library(
name = "app_component",
hdrs = ["AppComponent.hpp"],
deps = ["@oatpp//:main"],
)
cc_library(
name = "error_handler",
srcs = ["ErrorHandler.cpp"],
hdrs = ["ErrorHandler.hpp"],
copts = ["-Iexternal/oatpp"],
deps = [
":status_dto",
"@oatpp//:main",
],
)
cc_binary(
name = "webapp",
srcs = ["App.cpp"],
copts = ["-Iexternal/oatpp"],
deps = [
":app_component",
":error_handler",
":static_controller",
"@oatpp//:main",
],
)
App.cpp
#include "AppComponent.hpp"
#include "StaticController.hpp"
#include "oatpp/network/Server.hpp"
#include <iostream>
void run() {
AppComponent components; // Create scope Environment components
/* Get router component */
OATPP_COMPONENT(std::shared_ptr<oatpp::web::server::HttpRouter>, router);
oatpp::web::server::api::Endpoints docEndpoints;
// docEndpoints.append(router->addController(UserController::createShared())->getEndpoints());
// router->addController(oatpp::swagger::Controller::createShared(docEndpoints));
router->addController(StaticController::createShared());
/* Get connection handler component */
OATPP_COMPONENT(std::shared_ptr<oatpp::network::ConnectionHandler>, connectionHandler);
/* Get connection provider component */
OATPP_COMPONENT(std::shared_ptr<oatpp::network::ServerConnectionProvider>, connectionProvider);
/* create server */
oatpp::network::Server server(connectionProvider,
connectionHandler);
OATPP_LOGD("Server", "Running on port %s...", connectionProvider->getProperty("port").toString()->c_str());
server.run();
}
/**
* main
*/
int main(int argc, const char * argv[]) {
oatpp::base::Environment::init();
run();
/* Print how many objects were created during app running, and what have left-probably leaked */
/* Disable object counting for release builds using '-D OATPP_DISABLE_ENV_OBJECT_COUNTERS' flag for better performance */
std::cout << "\nEnvironment:\n";
std::cout << "objectsCount = " << oatpp::base::Environment::getObjectsCount() << "\n";
std::cout << "objectsCreated = " << oatpp::base::Environment::getObjectsCreated() << "\n\n";
oatpp::base::Environment::destroy();
return 0;
}
AppComponent.hpp
#ifndef AppComponent_hpp
#define AppComponent_hpp
#include <memory>
#include "ErrorHandler.hpp"
#include "oatpp/web/server/HttpConnectionHandler.hpp"
#include "oatpp/web/server/HttpRouter.hpp"
#include "oatpp/network/tcp/server/ConnectionProvider.hpp"
#include "oatpp/parser/json/mapping/ObjectMapper.hpp"
#include "oatpp/core/macro/component.hpp"
/**
* Class which creates and holds Application components and registers components in oatpp::base::Environment
* Order of components initialization is from top to bottom
*/
class AppComponent {
public:
/**
* Create ObjectMapper component to serialize/deserialize DTOs in Controller's API
*/
OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::data::mapping::ObjectMapper>, apiObjectMapper)([] {
auto objectMapper = oatpp::parser::json::mapping::ObjectMapper::createShared();
objectMapper->getDeserializer()->getConfig()->allowUnknownFields = false;
return objectMapper;
}());
/**
* Create ConnectionProvider component which listens on the port
*/
OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::network::ServerConnectionProvider>, serverConnectionProvider)([] {
return oatpp::network::tcp::server::ConnectionProvider::createShared({"0.0.0.0", 8000, oatpp::network::Address::IP_4});
}());
/**
* Create Router component
*/
OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::web::server::HttpRouter>, httpRouter)([] {
return oatpp::web::server::HttpRouter::createShared();
}());
/**
* Create ConnectionHandler component which uses Router component to route requests
*/
OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::network::ConnectionHandler>, serverConnectionHandler)([] {
OATPP_COMPONENT(std::shared_ptr<oatpp::web::server::HttpRouter>, router); // get Router component
OATPP_COMPONENT(std::shared_ptr<oatpp::data::mapping::ObjectMapper>, objectMapper); // get ObjectMapper component
auto connectionHandler = oatpp::web::server::HttpConnectionHandler::createShared(router);
connectionHandler->setErrorHandler(std::make_shared<ErrorHandler>(objectMapper));
return connectionHandler;
}());
};
#endif /* AppComponent_hpp */
ErrorHandler.cpp
#include "ErrorHandler.hpp"
#include <memory>
ErrorHandler::ErrorHandler(const std::shared_ptr<oatpp::data::mapping::ObjectMapper>& objectMapper)
: m_objectMapper(objectMapper)
{}
std::shared_ptr<ErrorHandler::OutgoingResponse>
ErrorHandler::handleError(const Status& status, const oatpp::String& message, const Headers& headers) {
auto error = StatusDto::createShared();
error->status = "ERROR";
error->code = status.code;
error->message = message;
auto response = ResponseFactory::createResponse(status, error, m_objectMapper);
for(const auto& pair : headers.getAll()) {
response->putHeader(pair.first.toString(), pair.second.toString());
}
return response;
}
ErrorHandler.hpp
#ifndef CRUD_ERRORHANDLER_HPP
#define CRUD_ERRORHANDLER_HPP
#include <memory>
#include "webapp/StatusDto.hpp"
#include "oatpp/web/server/handler/ErrorHandler.hpp"
#include "oatpp/web/protocol/http/outgoing/ResponseFactory.hpp"
class ErrorHandler : public oatpp::web::server::handler::ErrorHandler {
private:
typedef oatpp::web::protocol::http::outgoing::Response OutgoingResponse;
typedef oatpp::web::protocol::http::Status Status;
typedef oatpp::web::protocol::http::outgoing::ResponseFactory ResponseFactory;
private:
std::shared_ptr<oatpp::data::mapping::ObjectMapper> m_objectMapper;
public:
ErrorHandler(const std::shared_ptr<oatpp::data::mapping::ObjectMapper>& objectMapper);
std::shared_ptr<OutgoingResponse>
handleError(const Status& status, const oatpp::String& message, const Headers& headers) override;
};
#endif //CRUD_ERRORHANDLER_HPP
StaticController.hpp
#ifndef CRUD_STATICCONTROLLER_HPP
#define CRUD_STATICCONTROLLER_HPP
#include "oatpp/web/server/api/ApiController.hpp"
#include "oatpp/parser/json/mapping/ObjectMapper.hpp"
#include "oatpp/core/macro/codegen.hpp"
#include "oatpp/core/macro/component.hpp"
#include OATPP_CODEGEN_BEGIN(ApiController) //<- Begin Codegen
class StaticController : public oatpp::web::server::api::ApiController {
public:
StaticController(const std::shared_ptr<ObjectMapper>& objectMapper)
: oatpp::web::server::api::ApiController(objectMapper)
{}
public:
static std::shared_ptr<StaticController> createShared(
OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper) // Inject objectMapper component here as default parameter
){
return std::make_shared<StaticController>(objectMapper);
}
ENDPOINT("GET", "/", root) {
const char* html =
"<html lang='en'>"
" <head>"
" <meta charset=utf-8/>"
" </head>"
" <body>"
" <p>Hello CRUD example project!</p>"
" </body>"
"</html>";
auto response = createResponse(Status::CODE_200, html);
response->putHeader(Header::CONTENT_TYPE, "text/html");
return response;
}
};
#include OATPP_CODEGEN_END(ApiController) //<- End Codegen
#endif //CRUD_STATICCONTROLLER_HPP
StatusDto.hpp
#ifndef CRUD_STATUSDTO_HPP
#define CRUD_STATUSDTO_HPP
#include "oatpp/core/macro/codegen.hpp"
#include "oatpp/core/Types.hpp"
#include OATPP_CODEGEN_BEGIN(DTO)
class StatusDto : public oatpp::DTO {
DTO_INIT(StatusDto, DTO)
DTO_FIELD_INFO(status) {
info->description = "Short status text";
}
DTO_FIELD(String, status);
DTO_FIELD_INFO(code) {
info->description = "Status code";
}
DTO_FIELD(Int32, code);
DTO_FIELD_INFO(message) {
info->description = "Verbose message";
}
DTO_FIELD(String, message);
};
#include OATPP_CODEGEN_END(DTO)
#endif //CRUD_STATUSDTO_HPP
Running the app
Now that we've created all the code, we can run our app.
You can either run this command from anywhere in the repository:
bazel run //webapp
Or, change into the webapp
directory and run this:
cd webapp
bazel run :webapp
You should see something like:
% bazel run //webapp
INFO: Analyzed target //webapp:webapp (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //webapp:webapp up-to-date:
bazel-bin/webapp/webapp
INFO: Elapsed time: 0.092s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/webapp/webapp
D |2024-07-23 21:22:45 1721787765486655| Server:Running on port 8000...
Browse to http://localhost:8000, or check it from the command line with cURL:
% curl localhost:8000
<html lang='en'> <head> <meta charset=utf-8/> </head> <body> <p>Hello CRUD example project!</p> </body></html>%
Save your work
To commit your work, first create an appropriate .gitignore
at the root of the repository:
/bazel-*
Then make your initial commit:
git add .
git commit -m "Initial commit"