Joshua Cooper I'm a full-stack web developer with a passion for well-written and efficient code. On the frontend, I use Typescript with React and on the backend I use Rust or Node.js with Typescript.

Top 3 templating libraries for Rust

6 min read 1753

The State of Rust Templating Libraries

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.

1. Handlebars

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 Despite this, the handlebars crate is not 100 percent compatible with the JavaScript implementation, so keep the following differences in mind.

  1. Mustache blocks aren’t supported
  2. Chained else isn’t implemented yet

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.

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 }}";
        .register_template_string("hello", source)

    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.


Now the above example will panic when no “name” field is given to the template when rendering.

We made a custom demo for .
No really. Click here to check it out.

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.

    .register_template_string("hello", include_str!("templates/hello.hbs"))

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.

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.

2. Tera

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.

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.

Tera templating language

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:

  1. {{ and }} for expressions
  2. {% and %} for statements ({%- and -%} can be used to strip leading / trailing whitespace respectively)
  3. {# and #} for comments

They 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 %}
  <h3>{{ }}</h3>
  <p>{{ student.score }}</p>
{% endfor %}

  {% for key, value in books %}
  <li>{{ key }} - {{ }}</li>
  {% endfor %}

Lastly, we can compose templates by using include.

{% include "header.html" %}
<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.

3. Liquid

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.

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()

    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.

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking 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 apps, recording literally everything that happens on your Rust app. 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 — .

Joshua Cooper I'm a full-stack web developer with a passion for well-written and efficient code. On the frontend, I use Typescript with React and on the backend I use Rust or Node.js with Typescript.

One Reply to “Top 3 templating libraries for Rust”

  1. > I’m a full-stack web developer with a passion for well-written and efficient code

    … uses React not Vue lol

Leave a Reply