drshapeless


My combo for Rust Web Development

Tags: htmx | rust

Create: 2023-10-10, Update: 2023-10-10

TLDR: Axum, Minijinja, sqlx, htmx

HTMX is goat

Before adapting to HTMX, whenever I want a somewhat interactive website, I used Svelte. Svelte is great, Svelte 5 was released some days ago. It introduced runes, which I have no idea what it is. Using Svelte is fun, and it does feel great, because I don't have to touch any of the React shit. Svelte, just like any other frontend frameworks, it needs to be on another project.

All of my previous projects using a backend (usually Go) together with Svelte are like this.

tree

├── my-server
├── my-frontend

They lay on different directories, use different languages.

I always think of doing all the things with one lauguage, so I don't have to encode and decode the same structure using JSON in different environments.

htmx makes it possible to do everything in the server, and do some obvious optimizations which is cumbersome in JavaScript. (Using a clone or a reference by my will is nightmare.)

Everything is in the server. Everything renders on the server, without stupid hydration and shit. (Till this day, I still find hydration really difficult to justify its use.)

I would say, htmx is the last puzzle to let me enjoy web programming.

Axum and sqlx

Axum and sqlx is pretty much the most popular libraries. One for routing and one for database. (Saying Axum is just for routing is kind of silly, but whatever.)

There are two web foundation libraries in Rust, Actix web and Axum. But Axum is by the Tokio team, and its integration with other http libraries is so nice to have. It is more ergonomic in my opinion. It may not be the fastest, but it is handy.

sqlx is the go-to library in Rust to deal with database. Although I only use Postgres, using a universal adaptor seems pointless, but sqlx is beautifully written. So forget about the database specific adaptor.

By the way, I do NOT use an ORM, and I never did. It is stupid if you know how to write SQL statement. I am making projects for fun, there aren't that many statements to write.

Minijinja

In my last blog post, I said I uses Tera. Tera is great, but after I read about the template fragment post by htmx team. Tera becomes not so great. In that link, it recommends using Minijinja. In my case, all my templates can be interchangable between Tera and Minijinja with a little modifications. I happily move from Tera to Minijinja and never look back.

First, template fragments. As the essay from htmx says, there is a render block function in Minijinja which allow rendering a fragment of the whole template, which is, I would say, essential when using htmx. Nobody would want to have free flowing fragmented html files all over the place. The most important thing is, when I put together pieces from different files to form a complete web page, I don't even see what the web page is at the first glance.

Second, there is a built-in function to watch changes in directory without manually updating templates. This is so great in rapid development. I can instant see the change. I would not want to recompile in Rust if I just want to modify a tailwind color. (To me, Askama is a joke, even I can have a 10x performance gain, I still would not want to recompile everytime.)

Funny enough, I eventually stop using the render block function in Minijinja. The built-in render block function in Minijinja has a serious limitation. It actually renders to whole page and cut to the fragment you want. If there is a variable outside the block you define, you still have to pass the struct responsible for that. In my use case, I render a top navbar which always displays the username by reading the cookie session. I want to render a block in situation like adding entry into a list, the block would be a <li> block. Why the fuck would I pass the username to render this block?

Luckily, there is another library to save the day, template-fragments. What it does is to load the templates and searches for any template fragments inside, registers it like a separate file. Although it gives an example of using together with Minijinja, I am pretty sure it can be used with Tera. Then why the hell did I switch? Well, I convinced myself I was switching for watch directory.

Conclusion

Here are my combo of using Rust for web development. Choosing between libraries take more time then writing code. If you want to use htmx in Rust, I think the above libraries are a good starting point.

If you want some cookie session management, axum-session is a great package. (Be careful, no trailing s, the one with axumis much worse.)

Here is my personal opinion. Use Go instead of Rust to write a web server. I am not Discord, you probably aren't either. Rust has way too many things to worry about for a simple web server. The binary size is not small. The borrow checker drives human crazy. Making things thread-safe requires weird container or unnecessary cloning. Error handling becomes your enemy when using htmx.

The things I prefer Rust over Go is actually the libraries, Axum and sqlx make writing handler and database function so much more convenience than in Go. Especially the query() in sqlx, it removes the need for me to hand matching field one by one. (There is something call scany in Go, which does similar thing.) But the real MVP is serde in Rust. But for the language itself, Rust is not so good for simple web server.

Another reason I would like to use Rust over Go is the lack of runtime. In every Go application, it ships with a Go runtime, although it is slim, it is still a runtime with garbage collector. Normally, Rust does not ship a runtime, but async Rust with Tokio does, I even doubt that the runtime of Tokio is fatter than the Go runtime. This point might not be convincing.

Rust error handling and htmx

In htmx, the way we handle error is not by sending a bad status code like 404 or 500. We send hypertext. The approach I like is to send a htmx-swap-oob to a block with id "msg" or insert it to the top of the page. But sometime, I want some fine grind control, like in a failed insertion, I put red messages to the corresponding bad fields. In this situation, the Rust error handling (I am talking about the Result enum) is pointless, especially in a handler.

Almost all of my handlers return a Result type, and the Error implement IntoResponse in Axum. It works great in JSON api, because the error message format is universal. But not so great in htmx. Sometimes, I just want to return a 200 OK to indicate successful deletion, or a html block indicating something wrong. Then, the whole handler has to return an impl IntoResponse. Guess what?

// Original
async fn handler() -> impl IntoResponse {
    Html("good")
}

// Status code or html
async fn handler() -> impl IntoResponse {
    if success {
        StatusCode::OK.into_response()
    }

    Html("deletion failed").into_response()
}

into() hell

Fuck the Rust type system.