Developing a Backend App in Rust
In this blog post, we will see how easy it is to build a backend application in Rust using a couple of community libraries.
To make it more practical, we are going to implement a service which converts an HTML report into a PDF file.
Rust Programming Language
Before we begin, let’s shortly look at Rust from a programming language perspective. Here are some of the main language characteristics:
- System programming language
- Statically typed language
- Focused on performance and stability
- No garbage collector, no runtime
- Safe concurrency through compiler checks, no data-races
- Memory management is done by the compiler through
the borrow checker, lifetimes, mutable/immutable references separation, etc. - Developed by Mozilla and completely open-sourced on GitHub
Application Requirements
Here is what needs to be covered by our Rust implementation:
- An HTTP endpoint to send dynamic user data and get a PDF report back
- The report template must be stored on disk, so that one can modify it when needed
- The template must be in HTML format
- The template must support transformation of input data before it is rendered into the final PDF
Solution
In general, our solution will be based on the following Rust crates[1]:
- HTTP Server by using Rocket web-framework
- Template engine by using handlebars-rust
- wkhtmltopdf command line tool to generate PDF (not a crate)
- Utility crates for logging, ini-file parsing, UUID generation
Implementation
A common approach when developing an application in Rust is to use its package manager called Cargo. It downloads dependencies, compiles packages, publishes packages to a central crates storage for collaboration and much more. Here is the link to the installation guide for Cargo, which has to be installed first: Installation.
A Cargo project is configured via a TOML file, which describes project dependencies and more. It also has a convenient command-line option to create a draft package, which we are going to use as well. Run the following command in your terminal:
It should create a new folder with a predefined file structure as well the Cargo.toml file. We are going to replace the auto-generated Cargo.toml file with the content below.
Cargo.toml:
A Cargo file contains a list of crates and their versions which we are going to use to implement the application.
Workflow
The service workflow, which we are going to implement, can be described as a synchronous call over HTTP and looks like this:
The PDF is sent back with an application/pdf HTTP header value.
HTTP endpoint
First, we define a Rocket route at the uri “/generate” and mount it into Rocket framework.
src/routes.rs:
Later on, we will launch the Rocket server using the mount_routes
function.
Note that we omit module import statements everywhere (i.e. use/extern) to focus on the main package code. Please go to the source code repository for these details.
Macros
You have probably noticed the usage of a special syntax in above code snippet like !
and #
. These symbols come from the Rust macros feature.
The #
character is an attribute-like macro, which is going to be expanded by the compiler into another Rust code.
In the above route macro, Rocket will generate additional code to handle HTTP requests according to parameters like URI,
body format and binding variable to set the payload.
The !
character is a function-like Rust macro. Examples above are routes!
and format!
.
To read more on the Rust macros feature: The Rust Programming Language -> Macros
Input Parameters
There are two parameters in the generate_report
function. The first one is a Rocket state wrapper. This is the way
one can work with in-memory state when using the Rocket framework. The second parameter contains the report user data.
In above case, it comes from the HTTP body JSON payload, which will be decoded into the GetRequest
struct:
Here is a definition of the struct for generating the report:
The second field is deserialized into a JSON object. It covers the case that user data is dynamic.
It does not makes sense to parse it into a specific structure, because its structure is unknown beforehand.
Basically, user_params
is dynamic user data as per requirement #1.
We will dump it as JSON text into the template engine and will render it using Handlebars-rust.
Rendering
ReportService
contains the main service logic.
src/service.rs:
There is a render
function, which will be called from the HTTP route. Here are the functions of impl ReportService
:
Eventually, it runs an OS command to execute the wkhtmltopdf tool passing the rendered HTML as byte array:
Note that we use the ?
postfix operator everywhere in the package code, which is an early-return for Result.Err value or
a get/unwrap of a value in case of Result.Ok value. By using ?
, we short-circuit error handling and return an error state
immediately back to caller. Otherwise, in any non-error case, the Ok value is unwrapped and the program gets to continue its flow.
Here is a function to generate the file name of the final PDF report. The name will be unique:
Template Engine
We abstract the Handlebars crate by wrapping it into a separate struct to render HTML templates in memory. The result of the TemplateEngine is an HTML text, which is used by ReportService.
src/templates.rs:
Implementation:
As we can see, the Handlebars crate takes a generic data type T, which must implement the traits Serializable and Debug. It is actually going to be a JSON text,
when it comes to call the handlebars.render
function.
Launch Application
Now, we have all the pieces to start the application with the Rocket framework. Here are our main.rs
functions:
The main function creates a new ReportService
. If it is initialised successfully, then the .launch()
function is called.
At this point, the main application thread is blocked by Rocket, which is waiting to handle incoming requests.
In order to launch the report-generator locally, let’s use the Cargo build tool.
It has a special run
command to execute an application binary in debug/unoptimized way:
As you might have noticed, there is one template registered, which I prepared to test the service. This template is an HTML file with Handlebars scripts. Let’s look at the important part of the original HTML file:
Handlebars code is written inside double-curly braces {{ }}. It supports loops and a couple of more programming constructions to create a simple template. This template is an example of rendering a theoretical customer order at some book store. There are a couple customer fields like name and address as well as an array of books they purchased.
Above template prints:
- simple user fields in HTML paragraphs
<p>
. - array of books inside the HTML table. One table row
<tr>
per book.
Acceptance Test
The idea of a pdf-generator is that the HTML template is designed for a specific user data JSON structure. If the user data structure is changed, then the HTML template needs to be adjusted accordingly. However, the application Rust code stays the same.
In order to print something meaningful based on the HTML template above, we need to send the following JSON structure as POST HTTP request:
and finally the result looks like this:
Improvements
There are a couple of things which could be improved in the implementation above:
- the user data could be received as a String.
The report name could be moved to URI parameters, so that the request body could be used
as is
by the template engine. This would help to avoid memory allocation and CPU cycles when parsing JSON text into the JSON object - if a service throughput was more important than a single request performance, we could design an asynchronous flow. For example, the report-generator could accept HTTP requests as tasks. One more endpoint could be added to show task statuses and destination links where prepared PDFs would be stored
- a clean working directory where generated PDF files are stored. This could be done either by some scheduler, or the application itself could spawn an asynchronous delayed task to clean the generated file.
Summary
We have seen that writing a backend application in Rust is easy and is great fun, thanks to community libraries and a nice build tool. There are more and more crates becoming available and, what is more important, some of them have reached stable version 1.x, so that one can use them as building blocks to make something bigger. Familiar C-style syntax plus modern features makes Rust a good choice for the implementation of backend and cloud-native applications, development tools or command-line utilities.
Although Rust is in active development, it provides stable, beta and nightly release versions. The Rust Development Team follows a six week schedule to release a new stable compiler version. More on the release cycle is here
Rust can be attractive from a Functional Programming perspective as well. For that Rust provides closures, Iterator trait, pattern matching, built-in Result enum for error-handling, Option enum for empty values, separation of mutability and immutability, separation of data and logic via structs and functions.
Links
- The Rust Programming Language book
- Source Code of the Report Generator on Github
- Rocket framework
- wkhtmltopdf tool
- Central registry of Rust Crates crates.io
-
A Rust dependency/library is called crate. ↩