---
title: 'Rails developers write some Rust: a review of Axum 0.6'
teaser: 'Is Rust Web yet? We used our Rails knowledge to try writing a Web application
  in Axum in two days.

  '
tags: web,rust
author:
- Mike Burns
- Dimiter Petrov
- George Kosmopoulos
published_on: 2023-01-16
---

For our company hackathon this year, three of us tried out the [Axum][] crate to
see what we could do with it and feel out its limitations. We loved a lot about
it, but there's still much more growth left for Rust libraries for backend Web
development.

[Axum]: https://github.com/tokio-rs/axum

This post is going to be technical and assume some level of Rust familarity.
Jump to the conclusion for our non-technical summary.

## Project Structure

Coming from Rails, we seldom consider how to structure a Web app. That simply
isn't the business goal, so Rails decided it for us. Axum dictated no such
convention.

We ended up with a pattern where `lib.rs` has one function, `router()`, that
returns a [`Router<AppState>`][] but without the state. Then the `main.rs` would
call that function, add a state to it, and turn that into a service.

[`Router<AppState>`]: https://docs.rs/axum/latest/axum/struct.Router.html

This gave us the flexibility of creating the state as needed for the app
runner. As I'll explain in a moment, we had different app runners that created
state in different ways.

We divided handlers by resource under a `controllers/` directory:
`controllers/posts.rs`,`controllers/comments.rs`. Each module exposed its own
`router()` function that was then used by the outer `router()` in `lib.rs`.
This way each module knows how to route to itself.

We additionally had `models`, `views`, `filters` (for templates), `forms`,
`errors`, and `state` modules.

While it was possible to assemble all of this, and it was nice to be able to
see the details, we were just trying to build an app. Minute details like
configuring the logger to show line numbers or telling Axum that an `AppError`
is a 500 were certainly not part of our business logic.

### Environments

<blockquote>
  <p>Everybody has a testing environment. Some people are lucky enough enough to
  have a totally separate environment to run production in.</p>
  <footer>&mdash; <cite><a href="https://twitter.com/stahnma/status/634849376343429120">stahnma</a></cite></footer>
</blockquote>

We want at least three environments: test, development, and production. We know
that the production server is going to be configured differently from a dev
server. Moreover, we know that they should all use different databases.

To achieve this we used three cargo workspaces: `production`, `development`,
and `app`. The first two only have a `src/main.rs` and the minimal `Cargo.toml`
needed to get those running. They exist to make an `AppState` (from env, from a
TOML file, etc.) and then to run the server. Meanwhile, the `app` workspace
contains the actual business logic.

This division would have helped discussions with our devops team -- they could
iterate on the `production` workspace without slowing development for the rest
of the team using the `development` workspace.

This technique could use some love -- it lead to leaky abstractions. And again,
we had to find and glue together all the logging, env reading, database
pooling, etc. ourselves.

### Substates

Zooming in a bit on the `AppState`, we made use of Axum 0.6's [substates][] and
[`FromRef`][]. Axum follows a classic callback pattern: give the runner ([Router][]) an
object, and the runner will pass that to the callbacks (Handler). In 0.6 they
made it easy to have a state struct with sub-structs, and to extract those
sub-structs within handlers.

[substates]: https://docs.rs/axum/latest/axum/extract/struct.State.html#substates
[`FromRef`]: https://docs.rs/axum/latest/axum/extract/derive.FromRef.html
[Router]: https://docs.rs/axum/latest/axum/struct.Router.html
[Handler]: https://docs.rs/axum/latest/axum/handler/index.html

We stuffed our `DatabaseConnection` in the state, and extracted it nicely
within the handlers. This was a nice compromise between global state/magic
variables and verbose dependency injection.

This technique of extracting the data from a state struct as you need it is
quite handy. That said, `#[derive(FromRef)]` is borderline magic, and when it
failed it left cryptic typecheck messages.

## Tasks

Axum, being a Web library, does not ship with a command runner. Fair enough.
But we still need to easily run project-specific commands!

We turned to two ideas for this: [Just][], and [xtask][].

[Just]: https://github.com/casey/just
[xtask]: https://github.com/matklad/cargo-xtask

Just is a command runner. It has a handy feature where it can load in an `.env`
file first. We made use of this to run a server with full backtracing
(`RUST_BACKTRACE`) and logging (`RUST_LOG`). We ended up with two tasks:
`dev-server` (alias: `s`) and `migrate`.

```rust
set dotenv-load

default:
	@just --list --justfile {{justfile()}}

alias s := dev-server
dev-server:
	cargo run -p development

migrate:
	cargo xtask migrate:up
```

We also needed to run tasks written in Rust, such as migrations. For this we
tried the xtask pattern, with an `xtask` workspace and one `main.rs` that
parses a subcommand and runs it.

Once we figured out the tooling, it really did not get in our way.
The Just tool works nicely, and xtask provides an easy way to add "scripts"
that use Rust libraries.

In the future we may stick with just xtask so simplify the competing processes,
at the expense of losing out on easily running anything that is not Rust such
as shell scripts. Those in the Rails world might be reminded of the `rake` vs
`rails` command runner split, which was eventually merged to save us the
headache of remembering which command offers which task.

All that said, collecting all the tasks needed for a Web app proved tedious. As
a hurdle atop of that, SQLx's docs lead to [confusion] about which tools we
need to build ourself -- we learned about [SQLx CLI][] while writing this post!
Additionally, dotenv management differs between Just and xtask ([dotenvy][]):
Just allows you to [override][] `.env` via the command line, but dotenvy does
not.

[confusion]: https://github.com/launchbadge/sqlx/issues/1934
[SQLx CLI]: https://github.com/launchbadge/sqlx/tree/main/sqlx-cli
[dotenvy]: https://github.com/allan2/dotenvy
[override]: https://github.com/casey/just/issues/642

## Tests

Rust's unit tests are good, and we had no question that they'd help us verify
our Askama filters. So we instead concentrated our two-day hackathon time on
integration tests.

For this we wanted to spin up a server then control a Web browser as it
navigates around. We started with [the example from the Axum repo][], which was
easy enough to get running (once we figured out how our real app differed from
their trivial example handler), but left us wanting more. An assertion that
the response code is 200 was easy enough, but verifying anything about the
response itself was more cumbersome.

[the example from the Axum repo]: https://github.com/tokio-rs/axum/blob/main/examples/testing/src/main.rs

So we turned our attention to the [thirtyfour][] crate, which allows us
WebDriver (Selenium) access. Unfortunately we ran out of time before we got it
to a state we were happy with. One thing we noticed was the lack of high-level
helpers: we'd be in charge of finding our own buttons and then clicking them,
instead of using a high level `click_button("Create post")` function. The idea
of re-implementing all of Ruby's [Capybara][] felt out of scope for the hackathon.

[thirtyfour]: https://docs.rs/thirtyfour/latest/thirtyfour/
[Capybara]: http://teamcapybara.github.io/capybara/

## Database

Coming from Rails, we were particularly interested in how to use Rust to access
the DB, and how to migrate the schema.

We did not expect to spend any time configuring a connection, but we did end up
doing just that. From setting a `DATABASE_URL` environment variable to making
our own [`ConnectOptions`][] struct so we could log statements to building our
own DB pool to passing the pool in the state, we had to look up and write some
15 lines of common infrastructure code.

[`ConnectOptions`]: https://docs.rs/sqlx/latest/sqlx/trait.ConnectOptions.html

### ORM

Despite [Diesel][] being written by [a thoughtbot alum][], we tried [SQLx][].
SQLx is [not an object-relational mapper][], which left much up to us. Next
time we may evaluate [Diesel], [SeaORM], and [Canyon] to see if they give us more to
grab onto.

[Diesel]: https://diesel.rs/
[Canyon]: https://zerodaycode.github.io/canyon-book/
[SeaORM]: https://www.sea-ql.org/SeaORM/
[a thoughtbot alum]: https://thoughtbot.com/blog/authors/sage-griffin
[SQLx]: https://github.com/launchbadge/sqlx
[not an object-relational mapper]: https://github.com/launchbadge/sqlx#sqlx-is-not-an-orm

In practice we came up with a framework that allowed us to iterate quickly. As
described earlier, our `AppState` contains a `struct
DatabaseConnection(sqlx::PgPool)`. We used that to hang table-like methods on
the `DatabaseConnection` itself, reducing the need for our Handlers to care
about PgPools.

```rust
#[derive(Clone, FromRef)]
pub struct DatabaseConnection(pub PgPool);

impl DatabaseConnection {
    pub fn posts(&self) -> PostsTable {
        PostsTable(self.0.clone())
    }
}
```

This gave us a `PostsTable` struct that could contain all the database
abstractions related to the `posts` table.

```rust
pub struct Post {
    pub id: sqlx::types::Uuid,
    pub title: String,
    pub content: String,
    pub created_at: OffsetDateTime,
}

pub struct PostsTable(pub PgPool);

impl PostsTable {
   pub async fn get(&self, id: uuid::Uuid) -> Result<Post, sqlx::Error> {
        sqlx::query_as!(Post, "select * from posts where id = $1", id)
            .fetch_one(&self.0)
            .await
    }
}
```

We decided to expose the [`sqlx::Error`][] return values from these methods
instead of wrapping them in `AppError::SqlxError` structs. We instead wrapped
them in the handler as needed. More on that below.

[`sqlx::Error`]: https://docs.rs/sqlx/latest/sqlx/enum.Error.html

This _ad hoc_ framework made it quick to add new DB methods (`all`,
`create_from_form`, etc.). Note, however, that we need to maintain a `Post`
struct that mirrors the DB exactly. SQLx does conveniently detect abnormalities
in static analysis, but it is just another thing to do.

While these new DB methods were convenient, they were not a rich, composable
language like we are used to in ActiveRecord scopes and ARel objects. SQLx is
not an ORM, though, so that's on us.

SQLx also has a lot of components with a scattered set of docs, which made the
process a little slow. That said, an ORM would also have a large set of docs to
work through.

### Migrations

One of the extremely cool features that SQLx offers is static analysis of your
queries, using the [`query!`][] and [`query_as!`][] macros. This means your
build will fail if you reference a table that doesn't exist, for example.

[`query!`]: https://docs.rs/sqlx/latest/sqlx/macro.query.html
[`query_as!`]: https://docs.rs/sqlx/latest/sqlx/macro.query_as.html

Our original setup, inspired by [examples around the Web][], was to run the
migrations when the app first boots. This means that we would compile the app,
run it, and then the migrations would run.

[examples around the Web]: https://github.com/launchbadge/realworld-axum-sqlx/blob/main/src/main.rs#L43-L45

However, if a query depends on a migration having been run, then it won't
compile, and thus the migration won't run.

We solved this by running migrations manually, first via a simple Rust script,
then via an xtask, and finally via the SQLx CLI program.

By following the SQLx docs, we ended up without support for down migrations --
all our migrations go in one direction: up. This happened because down
migrations require a completely different structure. Discovering this was
[tricky][] -- we didn't actually learn this until writing this post.

[tricky]: https://github.com/launchbadge/sqlx/issues/1306

Regardless, we especially liked the `_sqlx_migrations` schema with the
`checksum` and `execution_time` columns.

## HTML

We were building a traditional Web program: request comes in, response with
HTML goes back. For that we wanted to use a templating language. We went with
[Askama][] over [Tera][] because it came with Axum support and was very simple
to use.

[Askama]: https://djc.github.io/askama/
[Tera]: https://tera.netlify.app/

We defined structs deriving [`Template`][] in a `views` module, and each of our
rendering handlers returned one of these structs. This was a lovely way to
communicate the goal of the handler: each handler exists to make that struct,
and everything in the handler works towards that cause.

[`Template`]: https://docs.rs/askama/latest/askama/derive.Template.html

The Askama syntax itself is fine. I like that they support matching on enums
right in the syntax. I have two wishes for improvement.

The first is that since so much depends on the filters, I wish Askama shipped
with more filters. We ended up writing our own for showing inline form errors
with the validator crate (discussed below). Ultimately we'd have liked
something that could render structs representing forms, though we see how that
would require a lot of iteration to get right.

The second is for a development mode where templates are rendered at runtime,
not compile-time. Re-compiling any time we wanted to see HTML changes was a bit
of a bummer.

But overall, Askama did its job and stayed out of the way.

### I18n

We typically build every app with internationalization from the start. This is
primarily a concern of the templates, the forms, and the error messages.

We looked at a few options and landed on [Fluent][], which looks incredibly well thought out.
We tried to make headway with [fluent-rs][], then [fluent-resmgr][], then
[fluent-templates][], but ultimately we tabled that for when we have more time.

[Fluent]: https://projectfluent.org/
[fluent-rs]: https://docs.rs/fluent/latest/fluent/
[fluent-resmgr]: https://crates.io/crates/fluent-resmgr
[fluent-templates]: https://github.com/XAMPPRocky/fluent-templates

It does look like the basic tooling is there, but the integration could use a
lot of love.

## Errors

All of our handlers returned an [`axum::response::Result<T>`][], where `T` is
one of the Askama template structs mentioned previously. We defined our own
`AppError` enum, deriving the [`thiserror::Error`][] trait. Our custom Axum
glue for this was a simple 500:

[`axum::response::Result<T>`]: https://docs.rs/axum/latest/axum/response/type.Result.html
[`thiserror::Error`]: https://docs.rs/thiserror/latest/thiserror/derive.Error.html

```rust
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
    }
}
```

As mentioned previously, our models returned `sqlx::Error` on failure. This
meant that we needed to [`map_err`][] our way into the right type. We could not
figure out the right [`From`][] incantation to avoid using `map_err`.

[`map_err`]: https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err
[`From`]: https://doc.rust-lang.org/std/convert/trait.From.html

However, being able to use `?` everywhere was extremely nice. The happy path
was easy to read while the error path had just normal Rust magic.

### Forms

We used the Axum [`Form`][] extractor, with minimal fuss. We did have to do our
own research when it came to validations, though.

[`Form`]: https://docs.rs/axum/latest/axum/struct.Form.html

We started down a path where our Serde deserialization for the form structs had
validation built in, using the `try_from` attribute. This was fun, but dealing
with extractor errors felt like we were splitting a concept ("create an
article") too far.

Instead we used the [validator][] crate on the form structs, and then handled
invalid data manually in our handlers. This felt like the right level of
abstraction to us.

[validator]: https://github.com/Keats/validator

While we did enjoy the validator library, it was a little verbose to render the
errors on the form. We ended up converting the errors into a HashMap and then
working with that.

## Glue

A few more general notes:

### Verbosity

Axum makes a lot of sense, but is also a bit verbose. For example, to redirect:

```rust
Ok(Redirect::to(&format!("posts/{}", post.id)).into_response())
```

Compare in Rails:

```ruby
redirect_to post
```

Or to render a user error:

```rust
Ok((
     StatusCode::UNPROCESSABLE_ENTITY,
     views::PostsNewTemplate {
         errors: errors.into_errors(),
     },
 ).into_response())
```

Again, compare Rails:

```ruby
render :new, status: :unprocessable_entity
```

It would be nice to get such common handler code slimmed down while still
maintaining the flexibility.

### Logging

Out of the box, I want to develop in an environment where I have a log of when
a request is processed, a response is sent, a DB query run, and verbose error
messages, all to a simple file on my own laptop. In production, we likely want
something much more complex with filtering. That we had to set this up
ourselves was a bit of a time sink.

On top of that, learning about which libraries could be influenced by
`RUST_LOG` often involved reading library code. That said, Rust's logging and
tracing facilities are quite nice.

## Types

Rust's type system is known to be fantastic. This hackathon team included
someone who has been writing Rust for a few years now all the way down to
someone who has made a "hello, world" and not much else. At all levels, the
typechecker was in conversation with us and part of the mob and pair
programming sessions, pointing out things we overlooked. The types seldom got
in the way.

At one point we started to refactor in "the Ruby style": tiny, tiny changes
that we could keep entirely in our head. Partway through we switched to a "Rust
style": moving around large swaths of code with guidance from rust-analyzer and
`cargo check`. The Rust style worked perfectly.

## Conclusion

Three Rails devs with mixed Rust knowledge were able to get an ugly blog
running in Axum over two days, building out the app's framework for
extensibility while we went. It was fun the entire time, and Rust truly is a
pleasant language to use.

Two days is slow going for something as simple as a database of `(title, body)`
pairs. But Axum, Hyper, and Rust laid a solid foundation that can be built upon
quickly. When more libraries, more functionality, and more glue hits the
ecosystem, Web development in Rust will be another rapid application
development option. We look forward to that possibility!
