Part of the “Rustober” series.

A web service in Rust is much more than it seems. Whereas in Go you can just srv := &http.Server{} and srv.ListenAndServe(), in Rust you have to bring your own async runtime. I remember when async was very new in Rust, and apparently since then the project leadership never settled on a specific runtime. Now you can pick and choose (good), but also you have to pick and choose (not so good).

In practical, real-world terms it means you use Tokio. And tower. And hyper. And axum. And sqlx. I think that’s it? My default intention is to use as few dependencies as possible — preferably none. For Typingvania I wrote the engine from scratch in C and the only library I ship with the game is SDL. DDPub that runs this website only really needs Markdown support. However, with Rust I want to try something different and commit to the existing ecosystem. I expect there will be plenty of challenges as it is.

So how does all of the above relate to each other and why is it necessary?

┌──────────────────────────┐
│   axum (web framework)   │
└────────┬────────────┬────┘
         │            │
    ┌────▼───┐    ┌───▼────────────────┐
    │ hyper  │    │ tower (middleware) │
    └────┬───┘    └───┬────────────────┘
         │            │
         └────────┬───┘
                  │
            ┌─────▼─────┐
            │   tokio   │
            └───────────┘

In short, as far as I understand it now:

  • tokio, being the source (all three of the others are subprojects) provides the runtime I mentioned above and defines how everything else on top of it works.
  • tower provides the Service trait that defines how data flows around, with abstractions and utilities around it, but is protocol-agnostic. Here’s the trait:
// An asynchronous function from a Request to a Response.
pub trait Service<Request> {
    type Response;
    type Error;
    type Future: Future<Output = Result<Self::Response, Self::Error>>;

    fn poll_ready(
        &mut self,
        cx: &mut Context<'_>,
    ) -> Poll<Result<(), Self::Error>>;
    fn call(&mut self, req: Request) -> Self::Future;
}
  • hyper implements HTTP on top of tower’s Service trait, but as a building block, not as a framework.
  • axum is a framework for building web applications — an abstraction over tower and hyper. Effectively, I will be using mostly axum facilities implemented using the other three libraries.
  • Finally, sqlx is an asynchronous SQL toolkit using tokio directly.

Because all of the libraries are so integrated with each other, it enables (hopefully) a fairly ergonomic experience where I don’t have to painstakingly plug things together. They are all mature libraries tested in production, so even though it’s a lot of API surface, I consider it a worthwhile time investment: my project will work, and more likely than not I’ll have to work with them again if/when I get to write Rust in a commercial setting — or just work on more side projects.