Before you start
Make sure that you can compile and run core tests as described at Configure, Build and Install.
This sample demonstrates how to serve static files (HTML, CSS, JavaScript, images, etc.) using 🐙 userver and how to create a client-side dynamic web page. This is a common requirement for web services that need to provide a web interface, documentation, or other static assets alongside their dynamic API.
Step by step guide
Let's create a single page web application that
- on POST request to
/v1/messages
remembers the JSON request body that consists of JSON object {"name": "some-name", "message": "the message"}
;
- on GET request to
/v1/messages
returns all the messages as JSON array of {"name": "some-name", "message": "the message"}
objects;
- serves static HTML, CSS and JavaScript files
- static HTML uses trivial JavaScript to create a client-side dynamic page using requests to the
/v1/messages
.
In this example we won't use database for simplicity of the sample.
Application Logic
Our application logic is straightforward, we'll implement it in a single file. First we start with messages parsing and serialization:
struct Message {
std::string name;
std::string message;
};
Message result;
result.name = json[
"name"].
As<std::string>();
result.message = json[
"message"].
As<std::string>();
return result;
}
json["name"] = value.name;
json["message"] = value.message;
}
For more information on Parse
and Serialize
customization points see Formats (JSON, YAML, BSON, ...). In more mature services it is recommended to use JSON schema codegen - the Chaotic instead of manual parsing.
Next step is to define the handler class:
public:
using HttpHandlerJsonBase::HttpHandlerJsonBase;
Value HandleRequestJsonThrow(const HttpRequest& request, const Value& json, RequestContext&) const override {
if (request.GetMethod() == server::http::HttpMethod::kPost) {
auto message = json.
As<Message>();
auto locked_data = messages_.Lock();
locked_data->push_back(std::move(message));
return {};
}
std::vector<Message> snapshot;
{
auto locked_data = messages_.Lock();
snapshot = *locked_data;
}
}
private:
};
In the above code we have to use concurrent::Variable, because the HandleRequestJsonThrow
function is called concurrently on the same instance of MessagesHandler
if more than one requests is performed at the same time.
Serving Static Content
The C++ code for serving static content is extremely simple because all the logic is provided by the ready-to-use userver::server::handlers::HttpHandlerStatic. Just add it and the components::FsCache into the component list:
int main(int argc, char* argv[]) {
.
Append<MessagesHandler>(
"handler-messages")
.Append<components::FsCache>("fs-cache-main")
}
Static config
Providing paths to files in file system to the fs-cache-main
component, telling the handler-static
to work with fs-cache-main
and configuring our MessagesHandler
handler:
# yaml
handler-messages:
path: /v1/messages
method: GET,POST
fs-cache-main:
dir: /var/www/ # Path to the directory with files
update-period: 10s # Update cache each N seconds.
handler-static: # Static Content handler.
fs-cache-component: fs-cache-main
path: /* # Registering handlers '/*' find files.
method: GET # Handle only GET requests.
With the above config a request to
/index.html
will receive file /var/www/index.html
;
/
will receive file /var/www/index.html
due to the default value of directory-file
static config option of userver::server::handlers::HttpHandlerStatic;
/custom.js
will receive file /var/www/custom.js
;
/custom.css
will receive file /var/www/custom.css
;
/v1/messages
request will be processed by our MessagesHandler
handler.
Dynamic Web Page
In this example the logic related to /v1/messages
requests is moved into a separate custom.js
file:
function PostJsonForm(event, on_post_response) {
event.preventDefault();
const formData = new FormData(event.target);
var obj = {};
formData.forEach((value, key) => obj[key] = value);
fetch('/v1/messages', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(obj),
})
.then(response => response.json())
.then(on_post_response)
.catch(error => {
alert("Error while retrieving initial data from backend: " + error);
});
}
function GetMessages(on_messages_response) {
fetch('/v1/messages', {
method: 'GET',
headers: {'Content-Type': 'application/json'},
})
.then(response => response.json())
.then(on_messages_response)
.catch(error => {
alert("Error while retrieving initial data from backend: " + error);
});
}
The index.html
page after DOM content is loaded fetches the existing messages. On button click new message is sent to the server and the page is reloaded:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>userver sample</title>
<link href="custom.css" rel="stylesheet">
<script type="text/javascript" src="custom.js"></script>
</head>
<body>
<div class="nav-bar">
<form action="/v1/messages" id="send-message-form">
<label>Name:</label>
<input type="text" id="form-name" name="name">
<label>Message:</label>
<input type="text" id="form-message" name="message">
<input type="submit" value="Send message">
</form>
</div>
<div class="clear-both"></div>
<h1>Messages:</h1>
<div id="the-messages"></div>
<script>
function OnSubmit(event) {
PostJsonForm(event, _ => { window.location.reload(); });
}
document.getElementById('send-message-form').addEventListener('submit', OnSubmit);
function EscapeHTML(str){
var p = document.createElement("p");
p.appendChild(document.createTextNode(str));
return p.innerHTML;
}
function OnLoaded() {
GetMessages(messages_array =>{
var content = "";
messages_array.forEach(message => {
content += "<p><span class='name'>" + EscapeHTML(message["name"]) + "</span>: " + EscapeHTML(message["message"]) + "</p>";
});
document.getElementById("the-messages").innerHTML = content;
});
}
document.addEventListener('DOMContentLoaded', OnLoaded);
</script>
</body>
</html>
More realistic applications usually use some JavaScript framework to manage data. For such example see the uservice-dynconf. Another example is the upastebin implementation.
Build and Run
To build the sample, execute the following build steps at the userver root directory:
mkdir build_release
cd build_release
cmake -DCMAKE_BUILD_TYPE=Release ..
make userver-samples-static_service
The sample could be started by running make start-userver-samples-static_service
. The command would invoke testsuite start target that sets proper paths in the configuration files and starts the service. After a prompt:
====================================================================================================
Started service at http://localhost:8080/
Monitor URL is http://localhost:-1/
====================================================================================================
open the address in web browser to play with the example:

To start the service manually run ./samples/hello_service/userver-samples-hello_service -c </path/to/static_config.yaml>
.
- Note
- CMake doesn't copy
static_config.yaml
and file from samples
directory into build directory. As an example see uservice-dynconf for an insights of how to update CMakeLists.txt
and how to test installation in CI.
Functional testing
In conftest.py
path to the directory with static content should be adjusted:
import pathlib
import pytest
pytest_plugins = ['pytest_userver.plugins.core']
USERVER_CONFIG_HOOKS = ['static_config_hook']
@pytest.fixture(scope='session')
def static_config_hook(service_source_dir):
def _patch_config(config_yaml, config_vars):
components = config_yaml['components_manager']['components']
assert 'fs-cache-main' in components
components['fs-cache-main']['dir'] = str(
pathlib.Path(service_source_dir).joinpath('public'),
)
return _patch_config
After that, Functional tests for the service could be implemented using the service_client fixture from pytest_userver.plugins.core in the following way:
import pytest
@pytest.mark.parametrize('path', ['/index.html', '/'])
async def test_file(service_client, service_source_dir, path):
response = await service_client.get(path)
assert response.status == 200
assert response.headers['Content-Type'] == 'text/html'
assert response.headers['Expires'] == '600'
file = service_source_dir.joinpath('public') / 'index.html'
assert response.content.decode() == file.open().read()
Full sources
See the full example at: