Server-side HTML rendering with Tera templates in RustAPI.
📖 Cookbook: Crates → rustapi-view
This example demonstrates server-side rendering with:
- Tera template engine integration
- Template inheritance (layouts)
- Type-safe context building
- Static file serving
- Form handling
- Rust 1.70+
- Basic HTML/CSS knowledge
- Understanding of template engines
| Feature | Description |
|---|---|
View<T> |
Template response type |
Templates |
Tera engine wrapper |
| Template inheritance | Base layouts |
ContextBuilder |
Type-safe context |
| Static files | CSS/JS serving |
# Run the example
cargo run -p templates-example
# Server starts at http://127.0.0.1:8080
# Open in browser to see rendered HTML| Method | Path | Description |
|---|---|---|
| GET | / |
Home page with features |
| GET | /about |
About page |
| GET | /contact |
Contact form |
| POST | /contact |
Form submission |
| GET | /blog |
Blog listing |
| GET | /static/* |
Static files (CSS) |
templates/
├── src/
│ └── main.rs
├── templates/
│ ├── base.html # Base layout
│ ├── index.html # Home page
│ ├── about.html # About page
│ ├── contact.html # Contact form
│ └── blog.html # Blog listing
└── static/
└── style.css # Stylesheet
# Home page
curl http://127.0.0.1:8080/
# About page
curl http://127.0.0.1:8080/about
# Blog page
curl http://127.0.0.1:8080/blog
# Or open in browser
open http://127.0.0.1:8080/curl -X POST "http://127.0.0.1:8080/contact?name=Alice&message=Hello"#[derive(Serialize)]
struct HomeContext {
title: String,
features: Vec<Feature>,
}
#[derive(Serialize)]
struct Feature {
name: String,
description: String,
}use rustapi_rs::view::{Templates, View};
async fn home(State(templates): State<Templates>) -> View<HomeContext> {
let features = vec![
Feature {
name: "Type-Safe".to_string(),
description: "Compile-time validation".to_string(),
},
Feature {
name: "Fast".to_string(),
description: "Built on Tokio and Hyper".to_string(),
},
];
View::render(
&templates,
"index.html",
HomeContext {
title: "Home".to_string(),
features,
},
)
.await
}#[derive(Debug, Deserialize)]
struct ContactForm {
name: Option<String>,
message: Option<String>,
}
async fn contact_post(
State(templates): State<Templates>,
Query(params): Query<ContactForm>,
) -> View<ContactContext> {
tracing::info!("Contact form submitted: {:?}", params);
View::render(
&templates,
"contact.html",
ContactContext {
title: "Contact".to_string(),
submitted: true,
name: params.name,
message: params.message,
},
)
.await
}#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize templates from directory
let templates = Templates::new("templates")?;
RustApi::new()
.route("/", get(home))
.route("/about", get(about))
.route("/contact", get(contact_get).post(contact_post))
.route("/blog", get(blog))
// Serve static files
.static_files("/static", "static")
.state(templates)
.run("127.0.0.1:8080")
.await
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }} - RustAPI</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/blog">Blog</a>
<a href="/contact">Contact</a>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>Powered by RustAPI</p>
</footer>
</body>
</html>{% extends "base.html" %}
{% block content %}
<h1>Welcome to RustAPI</h1>
<section class="features">
{% for feature in features %}
<div class="feature">
<h3>{{ feature.name }}</h3>
<p>{{ feature.description }}</p>
</div>
{% endfor %}
</section>
{% endblock %}{% if submitted %}
<div class="success">
<p>Thank you, {{ name | default(value="Anonymous") }}!</p>
<p>Your message: {{ message }}</p>
</div>
{% else %}
<form method="post">
<input type="text" name="name" placeholder="Your name">
<textarea name="message" placeholder="Your message"></textarea>
<button type="submit">Send</button>
</form>
{% endif %}{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p class="meta">By {{ post.author }} on {{ post.date }}</p>
<p>{{ post.excerpt | truncate(length=100) }}</p>
<a href="/blog/{{ post.id }}">Read more →</a>
</article>
{% endfor %}base.html
└── index.html (extends base)
└── about.html (extends base)
└── contact.html (extends base)
| Feature | Syntax |
|---|---|
| Variables | {{ variable }} |
| Blocks | {% block name %}{% endblock %} |
| Extends | {% extends "base.html" %} |
| Loops | {% for item in items %}{% endfor %} |
| Conditions | {% if condition %}{% endif %} |
| Filters | {{ text | upper }} |
| Include | {% include "partial.html" %} |
// Serve entire directory
.static_files("/static", "static")
// In templates:
<link href="/static/style.css" rel="stylesheet">
<script src="/static/app.js"></script>
<img src="/static/images/logo.png">[dependencies]
rustapi-rs = { version = "0.2", features = ["view"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
utoipa = "4"
tracing = "0.1"<!-- partials/header.html -->
<header>
<h1>{{ site_name }}</h1>
</header>
<!-- page.html -->
{% include "partials/header.html" %}let mut templates = Templates::new("templates")?;
templates.register_filter("markdown", |value, _| {
// Convert markdown to HTML
Ok(markdown_to_html(value.as_str().unwrap()))
});async fn not_found(State(templates): State<Templates>) -> View<ErrorContext> {
View::render(&templates, "404.html", ErrorContext {
title: "Not Found".to_string(),
message: "Page not found".to_string(),
})
.await
.with_status(StatusCode::NOT_FOUND)
}- websocket — Real-time features
- proof-of-concept — Full application
- FEATURES.md — View feature reference
- Tera Documentation
- RustAPI Cookbook