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.

Of course you can mix and match server frameworks and rendering engines. But if you just want to get started on implementing features and using htmx, we recommend you to use our skeleton.

It already has all the boilerplate code - and you can focus on using htmx.

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

Server Side Rendering

TODO: Examples on how askama works

Possible subchapters with examples:

  • Showing Askama and html templates.

  • Show how Rust structs binds those templates

  • Show how one can return those "askama structs" from an axum handler with -> impl IntoResponse {...}

  • Use a small example, where people can change hardcoded things

  • Show, how you can e.g. use for loops or conditionals directly in askama templates.

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:

// todo add picture of the 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

TODO: This chapter is a bit on hands on. People should use htmx in their small rust webserver with the askama templates.

Possible samples:

  • Clicking on a button - exchange the fetched content with the button itself.
  • A form with a target / swap (maybe also showing different kind of targets?)
  • ...