userver: Serving Static Content and Dynamic Web Pages
Loading...
Searching...
No Matches
Serving Static Content and Dynamic Web Pages

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;
return json.ExtractValue();
}

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:

class MessagesHandler final : public server::handlers::HttpHandlerJsonBase {
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[]) {
const auto component_list = components::MinimalServerComponentList()
.Append<MessagesHandler>("handler-messages")
.Append<components::FsCache>("fs-cache-main")
return utils::DaemonMain(argc, argv, component_list);
}

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();
// event.target — HTML-element of the form
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:

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: