How to Use Templates in Rust (Tera, Askama)

Web
Use the askama crate to define Rust structs with the Template trait and render HTML files by calling the render method.

When strings meet structure

You are building a web server. You have a list of users fetched from a database. You want to render an HTML table. In JavaScript, you might jam JSX into a component and rely on the runtime to assemble the DOM. In Python, you pass a dictionary to Jinja2 and hope the keys match. In Rust, the compiler is watching. You cannot inject arbitrary strings without considering types, lifetimes, and safety. You need a template engine that integrates with the type system and catches errors before the application starts.

Rust offers two dominant approaches. Askama compiles templates into Rust code at build time. Tera parses templates at runtime. Both produce HTML. Both handle loops, conditionals, and filters. They differ in when they verify correctness and how they handle data.

Think of a template as a stencil. The stencil has cutouts for specific shapes. If you try to push a square peg through a round hole, the machine stops and tells you immediately. You do not find out when a user requests the page. This is the compile-time advantage. Askama is the strict stencil. Tera is a flexible script that checks the shape when you apply it.

Askama: Compile-time templates

Askama generates Rust code from your HTML files. The generated code writes directly to a buffer. It avoids intermediate allocations. It checks that every variable in the template exists in the data structure. It verifies types. If you rename a field in your struct but forget to update the template, the build fails.

Convention aside: The community standard is to place templates in a templates directory at the crate root. Askama looks there by default. Deviating from this requires configuration. Stick to the convention unless you have a compelling reason.

use askama::Template;

/// Represents a greeting message with a recipient name.
#[derive(Template)]
#[template(path = "greeting.html")]
struct Greeting<'a> {
    name: &'a str,
}

fn main() {
    // Create the data structure.
    let greeting = Greeting { name: "Rustacean" };

    // Render converts the struct and template into a String.
    // unwrap() is safe here because we control the template path.
    let html = greeting.render().unwrap();
    println!("{}", html);
}

Create a file templates/greeting.html:

<!DOCTYPE html>
<html>
<body>
    <!-- Askama interpolates the name field from the struct. -->
    <h1>Hello, {{ name }}!</h1>
</body>
</html>

The #[derive(Template)] attribute macro runs during compilation. It reads greeting.html. It generates a render method on Greeting. The method writes the static HTML and inserts the name value. If name were missing, the generated code would reference a non-existent field. The compiler rejects the code with an error like no field 'name' on type 'Greeting'. The error points to the generated code, which can be confusing. Look for the askama error message in the output, which highlights the template line.

Askama turns your templates into Rust code. Treat them like code. Review them in pull requests. Test them. The compiler catches typos, but it will not catch a logic error in a loop condition.

Realistic example: Lists and filters

Real templates rarely render a single value. They iterate over collections. They apply transformations. Askama supports loops, conditionals, and filters. Filters modify output. Common filters include upper, lower, safe, and escape.

Convention aside: Askama escapes HTML by default. This prevents cross-site scripting attacks. Use the safe filter only for trusted content. Never mark user input as safe.

use askama::Template;

/// A single item in a todo list.
struct Item {
    title: String,
    done: bool,
}

/// The context for rendering the todo list page.
#[derive(Template)]
#[template(path = "todos.html")]
struct TodoList {
    items: Vec<Item>,
}

fn main() {
    let list = TodoList {
        items: vec![
            Item { title: "Learn Rust".into(), done: true },
            Item { title: "Write templates".into(), done: false },
        ],
    };

    println!("{}", list.render().unwrap());
}

Template templates/todos.html:

<ul>
    <!-- Iterate over the items vector. -->
    {% for item in items %}
    <li>
        <!-- Apply the upper filter to the title. -->
        {{ item.title | upper }}
        <!-- Conditionally display status. -->
        {% if item.done %}
            [DONE]
        {% else %}
            [PENDING]
        {% endif %}
    </li>
    {% endfor %}
</ul>

Askama checks that items is iterable. It checks that item has title and done. It verifies that upper is a valid filter. If you use a filter that does not exist, the compiler rejects the code. The error might reference a missing function in the generated code. Askama provides a set of built-in filters. You can also define custom filters by implementing a trait.

Tera: Runtime flexibility

Tera parses templates at runtime. It loads files into a Tera instance. You render with a context. The context is dynamic. You can pass a serde_json::Value or a tera::Context struct. This allows templates to be loaded from a database or customized by users. It also means errors occur when the request is processed, not at build time.

Tera syntax is similar to Jinja2. It supports variables, loops, conditionals, filters, and macros. It supports template inheritance with blocks. If you are migrating from Python or JavaScript, Tera feels familiar.

use tera::Tera;

fn main() {
    // Load templates from a directory.
    // The glob pattern matches all files in templates and subdirectories.
    let mut tera = Tera::new("templates/**/*").unwrap();

    // Create a context with dynamic data.
    let context = tera::Context::from_value(serde_json::json!({
        "name": "World",
        "items": ["Rust", "Templates", "Safety"]
    })).unwrap();

    // Render the template with the context.
    let html = tera.render("hello.html", &context).unwrap();
    println!("{}", html);
}

Template templates/hello.html:

<h1>Hello, {{ name }}!</h1>
<ul>
    {% for item in items %}
    <li>{{ item }}</li>
    {% endfor %}
</ul>

Tera does not check keys at compile time. If you typo name as nam, the template renders empty or errors at runtime. You lose the safety guarantee. Tera is useful when templates are dynamic. It is less suitable for standard web apps where structure is fixed.

Convention aside: Tera requires serde for context serialization. Add serde and serde_json to your dependencies. The community often uses serde_json::json! for quick contexts in tests. In production, derive Serialize on structs and pass them to Context::new.

Pitfalls and errors

Template engines introduce new failure modes. Understanding them saves debugging time.

If you reference a variable in an Askama template that does not exist in the struct, the generated code fails to compile. The error looks like a standard Rust error. It might say no field 'foo' on type 'Bar'. Check the template for typos.

If you use a type that does not implement Display or Template in Askama, you get a trait bound error. E0277 is common for trait bounds. The error says something like the trait 'Display' is not implemented for 'MyType'. Implement Display or Template on the type.

If you move a template file in Askama, the build fails. The error says the file was not found. Askama caches the template path. Rebuild after moving files.

If you pass a context with missing keys in Tera, the template renders empty for that variable. Tera does not error on missing keys by default. This can lead to subtle bugs. Enable strict mode in Tera to error on missing keys during development.

If you forget to escape user input, you risk XSS. Both Askama and Tera escape by default. Do not disable escaping unless necessary. Use the safe filter only for trusted content.

Trust the borrow checker in templates. Use owned strings or lifetimes deliberately. Askama handles lifetimes correctly. If you pass a reference with a short lifetime, the compiler rejects the code. E0597 (borrowed value does not live long enough) can occur. Ensure the data lives at least as long as the template render call.

Decision: Askama vs Tera

Choose the tool that matches your needs. The choice affects safety, performance, and flexibility.

Use Askama when you want compile-time safety and your templates are static HTML files. Use Askama when performance matters and you want to avoid parsing overhead on every request. Use Askama when you are building a standard web app where the template structure is known ahead of time. Use Askama when you want the compiler to catch typos and type mismatches before deployment.

Use Tera when you need runtime flexibility, like loading templates from a database or allowing users to customize themes. Use Tera when you are migrating from a Jinja2 background and want familiar syntax with minimal friction. Use Tera when your application logic requires dynamic template generation that the compiler cannot verify. Use Tera when you need to render templates in a context where compile-time generation is impossible, such as a plugin system.

Use plain string formatting when the HTML is trivial and you do not want the dependency overhead. String interpolation works for simple responses. It becomes unmanageable for complex pages. Switch to a template engine when the HTML grows beyond a few lines.

Where to go next