Choosing a Rust web framework, 2020 edition
- 2466 words
- 13 min
This article is a spin-off from Zero To Production In Rust, an opinionated introduction to backend development in Rust.
You can pre-order the book on https://zero2prod.com.
Discuss the article on HackerNews or r/rust.
As of July 2020, the main web frameworks in the Rust ecosystem are:
Which one should you pick if you are about to start building a new production-ready API in Rust?
I will break down where each of those web frameworks stands when it comes to:
- Community and adoption;
- Sync vs Async, as well as their choice of futures runtime;
- Documentation, tutorials and examples;
- API and ergonomics.
I will in the end make my recommendation. Worth remarking that there are no absolutes: different circumstances (and taste) might lead you to a different pick.
warp are slim web frameworks: they offer you an HTTP web server, routing logic, middleware infrastructure and basic building blocks and abstractions to parse, manipulate and respond to HTTP requests.
rocket takes a different approach - it aims to be batteries-included: the most common needs should be covered by functionality provided out-of-the-box by
rocket itself, with hooks for you to extend
rocket if your usecase needs it.
It should not come as a surprise then that
rocket ships an easy-to-use integration to manage connection pools for several popular database (e.g. Postgres, Redis, Memcache, etc.) as well as its own configuration system in
rocket-contrib, an ancillary crate hosted in
rocket's own repository.
We can compare them to frameworks available in other ecosystems:
warpare closer in spirit to
Flaskfrom Python or
rocketis closer to
Djangofrom Python or
Symphonyfrom PHP: a stable and solid core with a set of high-quality in-tree components to fulfill your every day needs when building a solid web application.
rockethas still a long way to go to match its peers in breadth and scope, but it is definitely off to a good start.
Of course this is a snapshot of the landscape as of today, but the situation is continuously shifting according to the maintainers' intentions - e.g.
actix-web has slowly been accumulating more and more supporting functionality (from security to session management) in
actix-extras, under the umbrella of the
actix GitHub organization.
Furthermore, using a slim web framework does not force you to write everything from scratch as soon as the framework is falling short of your needs: you can leverage the ecosystem built by the community around it to avoid re-inventing the wheel on every single project.
2. Community and adoption
Numbers can be misleading, but they are a good conversation starting point. Looking at crates.io, we have:
|Framework||Total Downloads||Daily Downloads|
The number of total downloads is obviously influenced by how long a framework has been around (e.g.
actix-web:0.1.0 came out at the end of 2017!) while daily downloads are a good gauge for the current level of interest around it.
You should care about adoption and community size for a couple of reasons:
- consistent production usage over years makes it way less likely that you are going to be the first one to spot a major defect. Others cried so that you could smile (most of the time);
- it correlates with the number of supporting crates for that framework;
- it correlates with the amount of tutorials, articles and helping hands you are likely to find if you are struggling.
The second point is particularly important for slim frameworks.
You can get a feel of the impact of community size, once again, by looking at the number of results popping up on crates.io when searching a framework name:
Will all those crates be relevant? Unlikely.
Will a fair share of them be outdated or unproven? Definitely.
Nonetheless it is a good idea, before starting a project, to have a quick look for functionality you know for a fact you will need. Let's make a couple of quick examples with features we will be relying on in the email newsletter implementation we are building in Zero To Production:
- if you need to add Prometheus' metrics to your API you can get off the ground in a couple of minutes with
rocket-prometheus, both with thousands of downloads. If you are using
tideyou will have to write the integration from scratch;
- if you want to add distributed tracing,
actix-web-opentelemetryhas your back. You will have to re-implement it if you choose any other framework.
Most of these features are not too much work to implement, but the effort (especially maintenance) compounds over time. You need to choose your framework with your eyes wide open on the level of commitment it is going to require.
3. Sync vs Async
Rust landed its
await syntax in version
1.39 - a game changer in terms of ergonomics for asynchronous programming.
It took some time for the whole Rust ecosystem to catch up and adopt it, but it's fair to say that crates dealing with IO-bound workloads are now generally expected to be async-first (e.g.
What about web frameworks?
await with its
0.2.x release, same as
tide was using
await before its stabilisation relying on the
nightly Rust compiler.
rocket, instead, still exposes a synchronous interface.
await support is expected as part of its next
0.5 release, in the making since last summer.
Should you rule out
rocket as a viable option because it does not yet support asynchronous programming?
If you are implementing an application to handle high volumes of traffic with strict performance requirements it might be better to opt for an async web framework.
If that is not the case, the lack of async support in
rocket should not be one of your primary concerns.
3.1. Futures runtime
await is not all sunshine and roses.
Asynchronous programming in Rust is built on top of the
Future trait: a future exposes a
poll method which has to be called to allow the future to make progress. You can think of Rust's futures as lazy: unless polled, there is no guarantee that they will execute to completion.
This is often been described as a pull model compared to the push model adopted by other languages1, which has some interesting implications when it comes to performance and task cancellation.
Wait a moment though - if futures are lazy and Rust does not ship a runtime in its standard library, who is in charge to call the
BYOR - Bring Your Own Runtime!
The async runtime is literally a dependency of your project, brought in as a crate.
This provides you with a great deal of flexibility: you could indeed implement your own runtime optimised to cater for the specific requirements of your usecase (see the Fuchsia project or
bastion's actor framework) or simply choose the most suitable on a case-by-case basis according to the needs of your application.
That sounds amazing on paper, but reality is a bit less glamorous: interoperability between runtimes is quite poor at the moment; mixing runtimes can be painful, often causing issues that are not straight-forward either to triage, detect or solve.
While most libraries should not depend on runtimes directly, relying instead on the interfaces exposed by the
futures crate, this is often not the case due to historical baggage (e.g.
tokio was for a long time the only available runtime in the ecosystem), practical needs (e.g. a framework has to be able to spawn tasks) or lack of standardisation (e.g. the ongoing discussion on the
AsyncWrite traits - see here and here).
Therefore picking an async web framework goes beyond the framework itself: you are choosing an ecosystem of crates, suddenly making it much more cumbersome to consume libraries relying on a different async runtime.
The current state of affairs is far from ideal, but if you are writing async Rust today I'd recommend you to make a deliberate choice when it comes to your async runtime.
The two main general-purpose async runtimes currently available in Rust are
tokio has been around for quite some time and it has seen extensive production usage. It is fairly tunable, although this results in a larger and more complex API surface.
async-std was released almost a year ago, around the time of
await stabilization. It provides great ergonomics, while leaving less room for configuration knobs.
crates.io can once again be used as a gauge for adoption and readiness:
|Runtime||Total Downloads||Daily Downloads|
How do frameworks map to runtimes?
4. Documentation, tutorials and examples
Having to dive into the source code to understand how something works can be fun (and educational!), but it should be a choice, not a necessity.
In most situations I'd rather rely on the framework being well-documented, including non-trivial examples of relevant usage patterns.
Good documentation, tutorials and fully-featured examples are mission-critical if you are working as part of a team, especially if one or more teammates are not experienced Rust developers.
Rust's tooling treats documentation as a first class concept (just run
cargo doc --open to get auto-generated docs for your project!) and it grew to be part of the culture of the Rust community itself. Library authors generally take it seriously and web frameworks are no exception to the general tendency: what you can find on docs.rs is quite thorough, with contextual examples where needed.
actix-web provide high-level guides on the respective websites and all frameworks maintain a rich collection of examples as part of their codebases2.
Tutorials outside of the project documentation are mostly a function of age: it's very easy to find material (articles, talks, workshops) on
rocket while the offering is somewhat more limited for
tide. On the flip side, some of what is out there for
rocket might target older versions, leaving room for confusion.
5. API and ergonomics
Well, difficult to give an opinion on API design that sounds legitimately objective.
We all have wildly different tastes when it comes to what we consider a pleasant API and there is no substitute for a quick hack-and-go to really get a feel for what it is like to use a certain web framework.
If you are short on time, you can have a look at worked out examples: actix-web's examples, warp's examples, tide's examples and rocket's examples.
If you are curious about
tide, Image decay as a service provides an in-depth analysis of their APIs.
6. Our choice
As of July 2020, I'd suggest picking
actix-web if you are writing a production API in Rust.
To recap what we covered,
- has seen extensive production usage;
- relies on
tokioas its async runtime, thus minimising the likelihood of compatibility issues with the most popular crates in the async ecosystem;
- boasts a significant collection of mature plugins as well as the largest community.
While some of its APIs are definitely not the most ergonomic (I am looking at you,
Transform trait), the inconvenience is definitely minor all things considered.
On the flip side, Rust itself would not be where it is today if nobody had been willing to take a bet on a promising but less proven technology:
warpare pushing the boundary of what is possible in terms of ergonomics using async Rust;
- the upcoming
rocketrelease is going to be massive, both for its adoption of
awaitas well as for the migration from
nightlyto the stable Rust compiler.
A rising tide lifts all boats.
The way of saying from which
tide takes its name, the way forward for the whole Rust async ecosystem.
See you again in a year for another overview!
Thanks to o0Ignition0o and vertexclique for taking the time to review the draft of this article.
If you want to be notified when new articles are released on this blog, subscribe to the email newsletter.