Rust has plenty of good templating crates, including some that are stable and ready for production use. Some of them are ports of templating libraries from other languages, while others were created specifically for use with Rust.
In this guide, we’ll compare a few of these crates and show you how to get started with each.
Handlebars is a minimal templating system originally developed for JavaScript. With the Handlebars crate, we can use the same system in Rust. This crate is among one of the most production-ready templating crates for Rust and is even used to render rust-lang.org. Despite this, the handlebars crate is not 100 percent compatible with the JavaScript implementation, so keep the following differences in mind.
If these limitations aren’t deal-breakers, Handlebars could be a good choice for you.
Let’s take a look at how to use this crate.
First, create a new project with cargo and add it to your dependencies.
[dependencies] handlebars = "3"
We can start with a hello world example.
use handlebars::Handlebars; use std::collections::HashMap; fn main() { let mut handlebars = Handlebars::new(); let source = "Hello {{ name }}"; handlebars .register_template_string("hello", source) .unwrap(); let mut data = HashMap::new(); data.insert("name", "Rust"); println!("{}", handlebars.render("hello", &data).unwrap()); }
Here we register a template using a string and supply the required data as a key-value pair in a HashMap
. This code outputs “Hello Rust.” Try replacing the “name” key in the hash map with something else and running the code again. You should see it output “Hello,” since the “name” field is never provided to the template when rendering.
Sometimes this isn’t what we want; the template expects to be given a name but silently skips it if not given. Fortunately, the handlebars crate has a strict mode that will produce a RenderError
when trying to access fields that don’t exist, something not available in the JavaScript version. We can enable strict mode with the following line before rendering.
handlebars.set_strict_mode(true);
Now the above example will panic when no “name” field is given to the template when rendering.
For longer templates, it may be a good idea to move them into their own files. One way to do this is with the include_str
macro.
handlebars .register_template_string("hello", include_str!("templates/hello.hbs")) .unwrap();
template/hello.hbs
is the location of the template file, relative to the Rust source file. The cool thing about this macro is that it includes the content of the file at compile time, yielding a &'static str
. This means the template strings will be included in the compiled binary and we won’t need to load files at runtime.
However, in cases where we want to load the templates at runtime, Handlebars provides some utility functions to help us.
To load and register a single template from a file:
handlebars.registertemplatefile("template_name", "templates/index.hbs");
To load and register an entire directory of template files:
handlebars.registertemplatesdirectory(".hbs", "path/to/templates");
The first argument (.hbs
) is the file extension that will be looked for. In this case, the names of the registered templates will be the relative path of the template from the templates directory, without the extension. For example, path/to/templates/blog/hello.hbs
would have a name blog/hello
with the above code. Note that loading directories of templates this way requires the dir_source
feature.
[dependencies] handlebars = { version = "3", features = ["dir_source"] }
One final thing to be aware of is that the render
method accepts any data that implements Serde’s Serialize
trait. Earlier we used a HashMap
, but feel free to use something else when appropriate.
Overall, I would recommend the Handlebars crate if you want as little logic as possible embedded in the templates. It’s widely used in both the Rust and JavaScript communities and the Rust implementation is rock-solid stable.
Tera is a templating language inspired by Jinja2 and the Django template language. Unlike Handlebars, this crate isn’t a direct port and doesn’t aim to be 100 percent compatible with them. Also unlike Handlebars, the Tera templating language allows for complex logic within the templates and is more featureful.
To get started, add the tera
dependency to Cargo.toml
.
[dependencies] tera = "1"
An equivalent “hello world” to what we used in the Handlebars example looks like the following.
use tera::{Context, Tera}; fn main() { let mut tera = Tera::default(); let source = "Hello {{ name }}"; tera.add_raw_template("hello", source).unwrap(); let mut context = Context::new(); context.insert("name", "Rust"); println!("{}", tera.render("hello", &context).unwrap()); }
As you can see, it is very similar. First, we register a template string, then define some data context
, and, finally render an output string using the template and data. One important difference here is that the second argument to Tera::render
takes &Context
, a type provided by the Tera crate. Fortunately, we don’t lose the flexibility of Serde’s Serialize
because the Context
type itself can be built from any value that implements Serialize
.
let value = // something that implements `Serialize` let context = Context::from_serialize(value);
Just like we saw before, the include_str
macro can be used to include external template files at compile time. Reading templates at runtime can be done manually or with the helper methods provided by Tera, the one we would normally use being Tera::new
.
let tera = Tera::new("templates/**/*").unwrap();
This takes a glob then loads and registers every template that matches the expanded glob.
Since the Tera templating language has a lot of features and doesn’t really exist outside of Rust, we will go over the basics of using it. For a full reference, check out the excellent Tera documentation.
Tera templates have three kinds of special delimiters:
{{
and }}
for expressions{%
and %}
for statements ({%-
and -%}
can be used to strip leading / trailing whitespace respectively){#
and #}
for commentsThey support math and comparisons.
<h1>Flower shop</h1> {% if visit_count % 10000 == 0 %} <p>Congratulations lucky visitor! You win a flower!</p> {% else %} <p>Welcome to the flower shop.</p> {% endif %}
This example also demonstrates the syntax for conditional control structures with if
and else
. In addition to this, you can use the logical operators and
, or
, and not
in template conditionals.
{% if month == 1 and day == 1 %} <p>Happy new year!</p> {% endif %}
Tera includes a concept called filters that can be used to modify data from within a template. Filters can be chained and you can register your own custom filters.
tera.register_filter("upper", string::upper);
This can then be used in templates similar to the following.
<h2>Hello, {{ name | upper }}!</h2>
Although we can create our own filters, Tera has some built-in filters for some of the common things people want to do, which may often be enough.
For iterating over arrays and structs, Tera provides for loops.
{% for student in students %} <div> <h3>{{ student.name }}</h3> <p>{{ student.score }}</p> </div> {% endfor %} <ul> {% for key, value in books %} <li>{{ key }} - {{ value.author }}</li> {% endfor %} </ul>
Lastly, we can compose templates by using include
.
{% include "header.html" %} <h1>Blog</h1> <p>Welcome to my blog</p> {% include "footer.html" %}
This is just scratching the surface of what you can do with Tera templates. They also support functions, macros, inheritance, and more, but for the sake of brevity, I won’t go over them here.
Tera can be considered stable and production-ready. I believe it’s usually best not to include too much logic in templates themselves, but when the extra flexibility is needed, Tera is one of the best options available for Rust today.
The last crate we will look at is liquid, a port of the popular Liquid template language originally written in Ruby. One of the explicit goals of this crate is 100 percent compatibility with Shopify/liquid.
As always, we can get started by adding liquid to our dependencies.
[dependencies] liquid = "0.21"
Again, the equivalent “hello world” example looks like this:
use liquid::ParserBuilder; fn main() { let source = "Hello {{ name }}"; let template = ParserBuilder::with_stdlib() .build() .unwrap() .parse(source) .unwrap(); let globals = liquid::object!({ "name": "Rust" }); println!("{}", template.render(&globals).unwrap()); }
Instead of having a struct containing all our templates, as was the case with Handlebars
and Tera
, we create a single template struct at a time. This time, our data globals
is defined with the liquid::object
macro, but we could have also used a familiar HashMap
-like API by working with the liquid::Object
type directly. Like the other crates, the data object can be directly created from types that implement Serialize
.
let value = // something that implements `Serialize` let globals = liquid::to_object(&value).unwrap();
The liquid crate doesn’t have much public API surface in general and lacks utility functions for things like loading templates from files. That said, loading files into a string at runtime is something you can easily do yourself.
Liquid templates have many similarities to the Tera templates. They share the same delimiters for expressions and statements and also have filters with a similar syntax.
<p>{{ "rust!" | capitalize | prepend: "Hello " }}</p>
This template will render the string "<p>Hello Rust!</p>"
.
Of the three crates we looked at, liquid is least production-ready, a fact supported by its lack of a version 1.0. Nevertheless, it may still be a good option for people coming from the Ruby version of liquid or people who just like the templating language itself.
Having looked at three of the most popular templating crates, it is easy to say that templates will not be an issue for web projects written in Rust and that Rust has great support for them already. Both Handlebars and Tera are stable, production-ready crates, and while liquid may not be at the same level just yet, it is still a solid choice.
It’s worth noting that there are plenty more than these three crates for doing templating in Rust, but most of the other options are less popular and don’t yet have a stable release. No matter which crate you decide to use, you will be able to reap the performance and reliability benefits of writing web applications in Rust.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
One Reply to "Top 3 templating libraries for Rust"
> I’m a full-stack web developer with a passion for well-written and efficient code
… uses React not Vue lol