Intro

We are going to introduce you HTMX with the rust server framework axum, which enables you to write reactive web applications with ease without the need of javascript frameworks.

You can get a good grasp by looking at fireship's 100s of HTMX video.

First Workshop Part

In this first part we will show you some basics with axum, rendering html with askama and using HTMX.

In detail the first part consists of:

  • Serving with axum.
  • How to return html directly using askama and how askama works.
  • How to add HTMX to our html and how HTMX works with small examples.

After you got the basics with server side rending and using htmx and having a good lunch, we will hop on to the second part.

Second Workshop Part

In the second part of the workshop you will build something yourself, without further guidance. Of course we will help if you get stuck somewhere. Getting your hands dirty is the best way to learn.

You can mix and match server frameworks and rendering engines.

General

There are plenty of web application frameworks out there. Other big examples are warp, actix-web, or rocket.

For our workshop, we will use axum, the web framework created by the folks from tokio, as we have the most knowledge about with it (If you get stuck and e.g. need help).

But you can translate features, that axum offers also to the other frameworks in the one or the other way. In the end the only thing you need is a framework that is capable of sending html over the wire and process incoming http requests, which all of the above frameworks are capable of.

Axum

Axum, the web framework created by the folks from tokio.

What does axum feature, or set them apart from other frameworks:

  • Macro free.
  • Parse requests using extractors. E.g. to extract JSON in a handler.
  • Has no own middleware - relies on integrating tower and tower-http and thus making it quite modular.

So what will we show you, that you will need later on:

  • How to write a handler in axum and return data as http response
  • How to extract a payload from an incoming request (Extractors)
  • How to add state to your application

How to add Handlers

Below is a small boilerplate in axum, which we examine, on how to write a handler in axum:

use axum::{response::IntoResponse, routing::get, Router};

async fn hello_world_handler() -> impl IntoResponse {
    "Hello World"
}

#[tokio::main]
async fn main() {
    let router = Router::new().route("/", get(hello_world_handler));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();

    axum::serve(listener, router).await.unwrap();
}

As everything is asynchronous, we will need an asynchronous runtime, that executes our async functions. We will use tokio for this (as axum is also from the same guys).

A handler in axum is just an ordinary asynchronous free function (hello_world_handler), that returns something, that implements IntoResponse. IntoResponse is a trait (Rust's interfaces) that tell the compiler, that this thing is convertible into a response.

To look what this means, you would have to look into the definition of IntoResponse. Fow now, it's sufficient to know, that it returns a payload and a status code.

For this example our handler returns a string literal, which will be converted in a http response of with payload of type text/plain with status code 200.

If you want to run the code, you can go to project in the snippets directory snippets/axum/handlers and run the project with cargo run.

How extraction works in axum

In the chapter before you are only able to answer to client requests, that do not contain any payload. But there must be a way to send our small little server a payload.

For example:

  • A server, that has some sort of user management, you want to add a new person to your database at some point in time.

This is done by so called extractors. The incoming request is parsed, and if it for example contains a JSON payload, you have a JSON extractor, that parses that JSON. After parsing you then can work on the data contained in the JSON.

There are also different extractors, which are ready to use. That would be for example:

  • For queries: e.g. /person?id=1234
  • For paths: e.g. /person/1, where the 1 would be extracted from the path
  • For forms: You can directly parse a form in a handler - that will come handy later for our workshop.

You can also write your own extractor, if you need to. But this is out of scope for now (If you are interested, have a look at axum's docs.

Extracting Json

Below is a small snippet, where we explain, how for example a JSON extractor in axum looks like, that extracts the JSON payload from the incoming request.

use axum::{response::IntoResponse, routing::post, Json, Router};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct PersonToAdd {
    name: String,
}

#[derive(Serialize)]
struct Person {
    id: i32,
    name: String,
}

async fn add_person_handler(Json(person_to_add): Json<PersonToAdd>) -> impl IntoResponse {
    // let's just echo the incoming json
    // usually you would insert this person now into your database
    // ...

    let imaginary_id = 42;
    let person = Person {
        id: imaginary_id,
        name: person_to_add.name,
    };

    Json(person)
}

#[tokio::main]
async fn main() {
    let router = Router::new().route("/person", post(add_person_handler));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();

    axum::serve(listener, router).await.unwrap();
}

If you look closely, only the handler function and the route changed in our axum's main function. To be detailed our new route expects a JSON payload, that is deserializable into PersonToAdd.

To send it fitting paylod, we can use curl, as CLI tool, with which we can send http request.

curl --header "Content-Type: application/json" --request POST --data '{"name":"Hans"}' http://localhost:3000/person

As with the hello_world_handler from before, we can return anything from a handler, that implements IntoResponse, which axum's Json<T>(T) is doing.

We just echo the payload inside the handler and add an id to it, which a normal server would do, after creating a ressource in a database to confirm creation of the new ressource.

Task

You have seen above, how you can echo a Json. Let's do it - but with a form this time! Write a handler, that accepts a form instead of a Json - and just echoes the data you have provided. You can go to snippets/axum/extraction. There is already the boilerplate for you at hand.

  • Go ahead and try to implement the following function
#![allow(unused)]
fn main() {
// TODO: You have to implement this one :)
async fn my_form(Form(form): Form<MyForm>) -> impl IntoResponse {
    todo!()
}
}
  • If you think you implemented it correctly, you can run cargo test.

Hint: You can also provide form data with curl with the following command:

curl -X POST -d "name=SomeName&age=44" localhost:3000/submit

State

In this chapter we look how you add state to your webserver. Then we'll look how you can access this state in your handlers.

How to add state

What's a webserver worth, if it cannot handle state? In this example we will fill a HashMap of people (from the previous chapter) and add a query to fetch all - and fetch one. That will be your task.

To add state in axum we have to options:

  • State: it's type safe - will fail at compile time
  • Extension: Not typesafe - i.e will fail at runtime

We will use State here. To add a state, you can call the .with_state() at the end of your Router Builder. We will just put a HashMap for learning reason (It's wrapped in an Arc<Mutex<_>>, so we can share it between threads). Usually you would give a handle to a database pool - which the handler then can use to make queries and insertions to a database.

#[tokio::main]
async fn main() {
    let persons: HashMap<String, Person> = HashMap::new();
    let in_memory_db = Arc::new(Mutex::new(persons));

    let router = Router::new()
        .route("/persons", post(add_person_handler))
        .with_state(in_memory_db);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();

    axum::serve(listener, router).await.unwrap();
}

How to access (and mutate) state

With axum's extraction logic, you can also extract State in the same way in the handler like a Json extractor. Now you can really add persons in the add_person_handler, like the following:

#![allow(unused)]
fn main() {
async fn add_person_handler(
    State(db): State<InMemoryDb>, // <-- State extractor - here we get the state
    Json(person_to_add): Json<PersonToAdd>,
) -> impl IntoResponse {
    let mut db_lock = db.lock().await; // <-- Get the mutex lock

    let uuid = Uuid::new_v4();
    let person = Person {
        uuid: uuid.to_string(),
        name: person_to_add.name,
    };
    db_lock.insert(uuid.to_string(), person.clone()); // <-- Insert the person

    Json(person)
}
}

Let's write a small handler to receive all of our current person - but this time as a small challenge for you ;).

Your Task

  • Go into the project snippets/axum/state and open your IDE of choice there.
  • Write a handler, that returns a list of Person as Json, when you call GET /persons. Of course the list will only be filled up, if you posted some persons
  • We already gave you a small starter here:
#![allow(unused)]
fn main() {
// TODO: This is your task ;)
pub async fn get_all_persons_handler(State(db): State<InMemoryDb>) -> impl IntoResponse {
    todo!()
}
}
  • run cargo test to verify you implemented the handler correctly

Hint: You can also test your webserver afterwards with cargo run. To add persons with this curl command:

curl --header "Content-Type: application/json" --request POST --data '{"name":"Hans"}' http://localhost:3000/persons

(Optional) Dependency Injection/Inversion with Axum

As of now we added our state directly into our web project. For greater projects it usually makes sense to abstract details away. In the end it should not matter, how your state is stored. It might be an SQL like database, a file, or something else.

In the end you are only interested in "storing" your state. But how - is a matter of detail, that we are now going to abstract away.

Currently, we have this dependency tree (left) - but we want to invert the dependency (right):

img.png

Here you do not have to code anything - we just wanted you to show, how a production style rust webserver could look like. The code for it is in snippets/injection. You can run the unit tests with cargo test, or run the webserver by cargo run.

Turning around dependencies

Our goal would be to turn around the dependency, such that our business logic does not depend on the implementation of the storage. Rather it should depend on an interface. The implementation then should depend on that same interface as well. This way, we can decouple our business logic from the implementation of our storage.

In Rust you usually achieve this behaviour with Traits.

First we define our trait, that's our interface layer between the storage implementation and anything else (e.g. our business logic):

#![allow(unused)]
fn main() {
#[async_trait]
pub trait Repository {
    async fn add_user(&mut self, user: &UserToCreate) -> User;
    async fn remove_user(&mut self, id: i64) -> Option<User>;
    async fn get_users(&self) -> Vec<User>;
}
}

Next, we can then implement a struct, that wraps our details and implements the above trait. We implement an SqliteAdapter - wrapping an SqlitePool and implementing above interface.

#![allow(unused)]
fn main() {
#[derive(Clone)]
pub struct SqliteAdapter {
    pool: SqlitePool,
}
}
#![allow(unused)]
fn main() {
#[async_trait]
impl Repository for SqliteAdapter {
    async fn add_user(&mut self, user: &UserToCreate) -> User {..}
    async fn remove_user(&mut self, id: i64) -> Option<User> {..}
    async fn get_users(&self) -> Vec<User> {..}
}
}

How do you now use this trait in axum handlers?

To make use of our new trait, we have to make them known to our handlers (where we usually have our business logic). Instead of using a struct inside our State extractor, we can use the trait with static dispatch (generics):

#![allow(unused)]
fn main() {
pub async fn get_users<T: Repository>(State(repo): State<T>) -> impl IntoResponse {
    Json(repo.get_users().await)
}
}

Now our inner logic (turning things into a Json), does not depend on the implementation, but rather on the interface, that promises to return a list of users.

As axum heavily depends on generics, it's a bit harder to tell the compiler, that we want to make use of this trait. Structs, that implement our trait, also must implement more traits to be used in a "generic" router.

To be correct they must implement: Clone + Send + Sync + 'static. For example our Arc<Mutex<HashMap<_,_>>> would implement all of these above. For our SqliteAdapter Send + Sync is implement be the wrapped SqlitePool. This is why have to declare our SqliteAdapter to by Clone. Our structures are automatically 'static, as we will create them in the beginning of our program (see chapters below) - and we guarantee, they outlive all places where they are used.

TL;DR: Our router (that defines the routes, and uses our handlers) needs more guarantees to be generic We have to make more guarantees, so we can use the interface we defined. A generic router looks like this now:

#![allow(unused)]
fn main() {
pub fn routes<T: Repository + Clone + Send + Sync + 'static>() -> Router<T> {
    Router::new()
        .route("/users", get(get_users::<T>).post(add_user::<T>))
        .route("/users/:id", delete(remove_user::<T>))
}
}

How (and where) do you dependency inject the implementation?

After we have a trait layer between our storage implemention, the next question is, where do we now inject our SqliteAdapter into our program?

The answer is rather simple. We just give our with_state(...) method in the router builder our SqliteAdapter. The compiler is happy, as SqliteAdapter meets all guarantees, we promised our compiler.

We inject like this:

#![allow(unused)]
fn main() {
let pool = Sqlitepool:new()...;// create an sqlitepool from sqlx
let adapter = SqliteAdapter::new(pool).await;
let router = Router::new().merge(user::routes()).with_state(adapter);
}

Askama

Askama is a popular Rust templating engine inspired by Jinja2 and Twig. It focuses on simplicity, safety, and efficiency by compiling templates into pure Rust code at build time. In this chapter, we will explore what makes Askama unique and learn how to use it effectively.

<head>
    <title>{{ title|upper }}</title>
</head>
<body>
    <h1>Hello, {{ user.name }}!</h1>
    
    {% if user.is_admin %}
        <p>Welcome, Admin!</p>
    {% else %}
        <p>Welcome, User!</p>
    {% endif %}
    
    <ul>
        {% for role in user.roles %}
            <li>{{ role }}</li>
        {% endfor %}
    </ul>
</body>
</html>
example askama snippet displaying filter, conditional rendering and a loop

Template Engines

Template engines are tools used to generate dynamic content (mostly, but not exclusively HTML). They generate the content by combining static templates with runtime data. Historically, they evolved out of server side includes and PHP, where embedding logic directly in HTML became a standard practice for creating dynamic web pages. Over time, modern template engines emerged with cleaner syntax, better safety features, and more structured approaches to templating. They provide various properties that contribute to their usability and functionality:

Safety Through Auto-Escaping

Modern template engines like Askama ensure safety by escaping special characters in user input. This prevents common security issues like Cross-Site Scripting (XSS) in web applications. By default, Askama escapes potentially unsafe characters when rendering HTML templates.

#![allow(unused)]
fn main() {
let user = User {
    name: "Alice",
    bio: "<script>alert('Hacked!')</script>",
};
}

Scripts are by default escaped so they are not executed. {{ bio }} will be rendered like this:

&lt;script&gt;alert('Hacked!')&lt;/script&gt;

Minimal Set of Control Syntax

Template Engines provide a concise and minimal syntax for common control structures like loops, conditionals, and filters. This reduces boilerplate and improves template readability.

Simplicity and Composability

Templates are meant to be simple and composable. Template Engines typically support features like includes and inheritance, which make it easy to break large templates into smaller, reusable pieces.

Properties of Askama

Compiled Templates

Askama compiles templates into Rust code at build time. This approach ensures type safety, better performance, and early detection of syntax errors. Unlike interpreted engines like Tera, Askama doesn’t evaluate templates at runtime, making it faster and less error-prone at runtime. Tera, on the other hand, guarantees a more rapid development cycle especially for more complex projects, exactly because it can get around the compilation step.

Inspiration from Jinja2 and Twig

Askama’s syntax is heavily inspired by Jinja2 (Python) and Twig (PHP). Developers familiar with these engines will find Askama’s syntax intuitive and easy to adopt. The same is true for Tera.

FeatureAskamaJinja2
Variable interpolationHello, {{ name }}!Hello, {{ name }}!
Conditionals{% if is_logged_in %}Welcome!{% else %}Please log in.{% endif %}{% if is_logged_in %}Welcome!{% else %}Please log in.{% endif %}
Loops{% for item in items %}<li>{{ item }}</li>{% endfor %}{% for item in items %}<li>{{ item }}</li>{% endfor %}
Includes{% include "header.html" %}{% include "header.html" %}
Template inheritance{% extends "base.html" %}{% block content %}Hello{% endblock %}{% extends "base.html" %}{% block content %}Hello{% endblock %}
Filters{{ name\|upper }}{{ name \| upper }}
Macros{% macro greet(name) %}Hello, {{ name }}!{% endmacro %}{% macro greet(name) %}Hello, {{ name }}!{% endmacro %}
Match-like structures{% match status %}{% when "success" %}OK{% else %}Error{% endmatch %}
Rust-specific syntax{% if let Some(value) = optional %}{{ value }}{% endif %}

How-to

This section provides a step-by-step guide to using Askama in your projects.

Include Askama in Your App

Add Askama to your Cargo.toml file:

[dependencies]
askama = "0.12"

Locate Templates in Your project

Templates are stored in a templates directory by convention. File extensions like .html or .txt indicate the template's format.

For example, templates/hello.html:

<!DOCTYPE html>
<html>
<head>
    <title>Hello, {{ name }}!</title>
</head>
<body>
    <h1>Welcome, {{ name }}!</h1>
</body>
</html>

Use Variables / Render Templates

Pass data to templates using Rust structs:

use askama::Template;

#[derive(Template)]
#[template(path = "hello.html")]
struct HelloTemplate<'a> {
    name: &'a str,
}

fn main() {
    let hello = HelloTemplate { name: "Alice" };
    println!("{}", hello.render().unwrap());
}

Architect with Template Inheritance / Blocks

Template inheritance allows defining base layouts and extending them with specific blocks. For example:

<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}Default Title{% endblock %}</title>
</head>
<body>
    {% block content %}{% endblock %}
</body>
</html>
<!-- templates/home.html -->
{% extends "base.html" %}

{% block title %}Home{% endblock %}

{% block content %}
<h1>Welcome to the home page!</h1>
{% endblock %}

Compose with Includes

Use {% include %} to embed reusable components:

<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}Default Title{% endblock %}</title>
</head>
<body>
    {% include "header.html" %}
    {% block content %}{% endblock %}
    {% include "footer.html" %}
</body>
</html>

Use Loops and Conditionals

Render dynamic content using loops and conditionals:

<ul>
{% for item in items %}
    <li>{{ item }}</li>
{% endfor %}
</ul>

{% if condition %}
    <p>Condition is true!</p>
{% else %}
    <p>Condition is false!</p>
{% endif %}

Rust-Specific if let and match Askama supports Rust's if let and match expressions for more complex logic:

{% if let Some(value) = optional %}
    <p>Value: {{ value }}</p>
{% else %}
    <p>No value found.</p>
{% endif %}

{% match status %}
    {% when "success" %}<p>Success!</p>
    {% when "error" %}<p>Error occurred.</p>
    {% else %}<p>Unknown status.</p>
{% endmatch %}

Use Filters

Filters process variables for display:

<p>{{ message|upper }}</p>

Note that the pipe symbol must not be surrounded by spaces; otherwise, it will be interpreted as the BitOr operator.

Register Your Own Filters

Custom filters can be registered in Rust code:

#![allow(unused)]
fn main() {
mod filters {
    pub fn reverse(s: &str) -> askama::Result<String> {
        Ok(s.chars().rev().collect())
    }
}
}

Full Example: Integrating Askama with Axum

Step 1: Template Definition

Create an Askama template file, templates/hello.html:

<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
</head>
<body>
    <h1>Hello, {{ user.name }}!</h1>
</body>
</html>

Define the corresponding Rust struct for the template:

#![allow(unused)]
fn main() {
use askama::Template;

#[derive(Template)]
#[template(path = "hello.html")]
pub struct HelloTemplate<'a> {
    pub title: &'a str,
    pub user: User<'a>,
}

pub struct User<'a> {
    pub name: &'a str,
}
}

Step 2: Axum Handler with impl IntoResponse

Set up an Axum handler that renders the Askama template:

#![allow(unused)]
fn main() {
use axum::{
    response::{IntoResponse, Response},
    routing::get,
    Router,
};
use askama::Template; // Bring in the Template trait for rendering.
use crate::{HelloTemplate, User};

async fn hello_handler() -> impl IntoResponse {
    let user = User { name: "Alice" };
    let template = HelloTemplate {
        title: "Hello Page",
        user,
    };

    match template.render() {
        Ok(html) => Response::builder()
            .header("Content-Type", "text/html")
            .body(html)
            .unwrap(),
        Err(err) => {
            eprintln!("Template rendering error: {}", err);
            Response::builder()
                .status(500)
                .body("Internal Server Error".into())
                .unwrap()
        }
    }
}
}

Step 3: Set Up Axum Application

Create an Axum app and route the handler:

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(hello_handler));
    axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Why htmx?

You are doing REST wrong

I am getting frustrated by the number of people calling any HTTP-based interface a REST API. Today’s example is the SocialSite REST API. That is RPC. It screams RPC. There is so much coupling on display that it should be given an X rating. What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint? In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period. Is there some broken manual somewhere that needs to be fixed?

–Roy Fielding, Creator of the term REST

Users do not understand Json

No, but the browser can convert:

<button hx-post="/clicked"
    hx-trigger="click"
    hx-target="#parent-div"
    hx-swap="outerHTML"
>
    Click Me!
</button>

to a Button

Attributes over javascript

With javascript:

<button id="my-button" on-click="clicked()">
    Click Me!
</button>
<script>
function clicked(){
  const request = new XMLHttpRequest();

  xhr.onreadystatechange = () => {
    if (xhr.readyState === 4) {
       const element = document.getElementById("parent-div");
       element.outerHtml = xhr.response
    }
  };

  request.open('POST', "/clicked");
  request.send(null);
}

</script>

With Htmx:

<button hx-post="/clicked"
    hx-trigger="click"
    hx-target="#parent-div"
    hx-swap="outerHTML"
>
    Click Me!
</button>

This was a simple example, but we already see how concise Htmx can be.

Now infinite scroll:

With javascript:

<tr 
  <td>Agent Smith</td>
  <td>void29@null.org</td>
  <td>55F49448C0</td>
</tr>

<script>
//... yeah not going to do that
</script>

With Htmx:

<tr hx-get="/contacts/?page=2"
    hx-trigger="revealed"
    hx-swap="afterend">
  <td>Agent Smith</td>
  <td>void29@null.org</td>
  <td>55F49448C0</td>
</tr>

HTMX samples

Simple Button

    <h1>HTMX Example</h1>
    <button hx-get="/message" hx-target="#message">Click me!</button>
    <div id="message"></div>

Form

 <h1>HTMX Example</h1>
    <form
    hx-post="/message" 
    hx-target="#message"
    >
        
    <input type="text" name="message" placeholder="Enter your message">

    <button type="submit" >Click me!</button>
    </form>
    
    <div id="message"></div>

Composition

<h1>Example</h1>
<div hx-get="/component1" hx-trigger="load delay:2s">
   
</div>
<h1>HTMX Component</h1>
<div>
    
   <p>Component 1</p>
   
   <button hx-get="/message"  hx-target="#message" >
       Get message
   </button>

<div id="message"></div>

</div>