Seamless Server-Side Templating in Rust Web Applications with Askama and Tera
Lukas Schneider
DevOps Engineer · Leapcell

Introduction
Building dynamic web applications often requires generating HTML content on the server and sending it to the client. This process, known as server-side rendering (SSR), is crucial for various reasons, including initial page load performance, SEO, and providing a fallback for users with JavaScript disabled. In the Rust ecosystem, while frameworks like Actix Web or Warp provide excellent foundations for building robust web services, they don't inherently dictate how to render dynamic HTML. This is where dedicated templating engines come into play, offering a powerful way to inject data into predefined HTML structures. This article will delve into two highly regarded templating engines in Rust: Askama and Tera, demonstrating how they enable efficient and expressive server-side template rendering in your Rust web applications.
Understanding Server-Side Templating in Rust
Before we dive into the specifics of Askama and Tera, let's clarify some core concepts related to server-side templating.
Templating Engine: A templating engine is a software component that allows you to combine static template files (often HTML with special placeholders or logic) with dynamic data to produce a final document, typically an HTML page.
Server-Side Rendering (SSR): The process of rendering web pages on the server and then sending the fully formed HTML to the client. This contrasts with client-side rendering (CSR), where a minimal HTML page is sent, and JavaScript on the client fetches data and builds the UI.
Context/Data: The dynamic information passed from your Rust application to the templating engine. This usually takes the form of structs or maps that hold the values to be inserted into the template.
Template Language: The specific syntax and rules used within the template files (e.g., {{ variable }}
, {% for item in items %}
) that the templating engine understands and processes.
The primary benefit of using a templating engine is the clear separation of concerns: your Rust code handles business logic and data retrieval, while your template files focus solely on the presentation layer. This separation makes your codebase more maintainable, readable, and easier for front-end developers to work with.
Askama: A Type-Safe and Compile-Time Checked Templating Engine
Askama is a popular templating engine in Rust known for its compile-time checking and performance. It leverages Rust's macro system to generate code for templates at compile time, leading to excellent performance and early detection of template errors.
Implementation with Askama
Askama uses a Jinja-like syntax, familiar to many developers. Let's walk through a simple example of using Askama with an Actix Web application.
First, add Askama and Actix Web to your Cargo.toml
:
[dependencies] actix-web = "4" askama = "0.12"
Next, define your template and the data structure that will populate it. Askama uses a derive macro to associate a struct with a template file.
// src/templates.rs use askama::Template; #[derive(Template)] #[template(path = "hello.html")] pub struct HelloTemplate<'a> { pub name: &'a str, pub items: Vec<&'a str>, }
Now, create the hello.html
template file in a directory named templates
at the root of your project:
<!-- templates/hello.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Hello from Askama</title> </head> <body> <h1>Hello, {{ name }}!</h1> <p>Here are some of your favorite things:</p> <ul> {% for item in items %} <li>{{ item }}</li> {% endfor %} </ul> </body> </html>
Finally, integrate this into your Actix Web application:
// src/main.rs use actix_web::{web, App, HttpResponse, HttpServer, Responder}; use askama::Template; // Bring Template trait into scope use crate::templates::HelloTemplate; mod templates; // Declare the module where your Askama templates are defined async fn greet() -> impl Responder { let template = HelloTemplate { name: "Rustacean", items: vec!["🦀", "💻", "🚀"], }; HttpResponse::Ok().body(template.render().unwrap()) } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().route("/", web::get().to(greet)) }) .bind(("127.0.0.1", 8080))? .run() .await }
When you run this application and navigate to http://127.0.0.1:8080
, you will see an HTML page rendered with "Hello, Rustacean!" and a list of items dynamically inserted from your Rust code. The beauty of Askama here is that if you make a typo in {{ name }}
(e.g., {{ namme }}
), the Rust compiler will catch it, preventing runtime errors.
Tera: A Feature-Rich and Flexible Templating Engine
Tera is another robust templating engine for Rust, largely inspired by Jinja2 and Django templates. It offers a rich set of features, including template inheritance, macros, filters, and custom functions, making it highly versatile for complex web applications. Unlike Askama's compile-time approach, Tera processes templates at runtime, giving it more flexibility in certain scenarios, such as loading templates from different sources or hot-reloading them during development.
Implementation with Tera
Let's adapt our previous example to use Tera.
First, add Tera and Actix Web to your Cargo.toml
:
[dependencies] actix-web = "4" tera = "1" serde = { version = "1", features = ["derive"] } serde_json = "1"
Tera typically works with serde
to handle data serialization into a Context
object.
Next, set up tera
and define an endpoint to render a template. Since Tera doesn't use derive macros on structs, you'll explicitly create a Context
.
// src/main.rs use actix_web::{web, App, HttpResponse, HttpServer, Responder}; use serde::{Deserialize, Serialize}; use tera::{Context, Tera}; // Struct to hold data for the template #[derive(Serialize, Deserialize)] struct UserData { name: String, items: Vec<String>, } async fn greet_tera(tera: web::Data<Tera>) -> impl Responder { let mut context = Context::new(); let user_data = UserData { name: "Gopher".to_string(), items: vec!["🏝️".to_string(), "☁️".to_string(), "🔬".to_string()], }; context.insert("user", &user_data); // Tera expects a map-like structure for context data let rendered = tera.render("hello.html", &context).unwrap_or_else(|err| { eprintln!("Tera rendering error: {}", err); "Error rendering template".to_string() }); HttpResponse::Ok() .content_type("text/html") .body(rendered) } #[actix_web::main] async fn main() -> std::io::Result<()> { let tera = match Tera::new("templates/**/*.html") { Ok(t) => t, Err(e) => { eprintln!("Parsing error(s): {}", e); ::std::process::exit(1); } }; HttpServer::new(move || { App::new() .app_data(web::Data::new(tera.clone())) // Share Tera instance across workers .route("/tera", web::get().to(greet_tera)) }) .bind(("127.0.0.1", 8081))? .run() .await }
Create the hello.html
template file in a templates
directory at the root of your project. Note how we access nested data in Tera (e.g., user.name
).
<!-- templates/hello.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Hello from Tera</title> </head> <body> <h1>Hello, {{ user.name }}!</h1> <p>Here are some of your favorite things:</p> <ul> {% for item in user.items %} <li>{{ item }}</li> {% endfor %} </ul> </body> </html>
When you run this application and navigate to http://127.0.0.1:8081/tera
, you'll see a similar rendered page, but this time powered by Tera.
Comparing Askama and Tera
Both Askama and Tera are excellent choices, but they cater to slightly different preferences and use cases:
- Askama:
- Pros: Compile-time checking (catches errors early), excellent performance due to generated code, strong type safety when mapping structs to templates.
- Cons: Less flexible for dynamic template loading/reloading, templates are recompiled with the application.
- Best for: Applications where performance and compile-time guarantees are paramount, and template structure is relatively stable.
- Tera:
- Pros: Rich feature set (macros, inheritance, filters), runtime loading and caching of templates, good for complex template hierarchies, hot-reloading during development.
- Cons: Runtime errors (typos in template variables are only caught when rendered), slightly lower performance than Askama (though still very fast).
- Best for: Applications requiring extensive template features, dynamic template loading, or when development flexibility regarding templates is prioritized.
Application Scenarios for Server-Side Templating
Server-side templating with engines like Askama and Tera finds its utility across various web application patterns:
- Traditional Web Applications: For full-stack applications where most of the UI is rendered on the server, ensuring fast initial page loads and better SEO.
- Hybrid Applications (SSR + Hydration): Providing the initial, fully rendered HTML for a fast first paint, which can then be "hydrated" by client-side JavaScript for interactivity.
- Email Templates: Generating rich HTML emails by dynamically inserting user-specific data into predefined layouts.
- Static Site Generators (SSG): While not their primary focus, templating engines are the core of many SSGs, processing data files and templates into static HTML.
Conclusion
Integrating server-side templating into your Rust web applications with engines like Askama and Tera greatly enhances your ability to build dynamic, maintainable, and robust user interfaces. Askama offers unparalleled type safety and performance through compile-time guarantees, while Tera provides a rich, flexible feature set for more complex templating needs. By understanding the strengths of each, you can choose the right tool to seamlessly render dynamic content, making your Rust web development experience both powerful and enjoyable. Ultimately, selecting Askama or Tera empowers you to create compelling web experiences directly from your high-performance Rust backend.