5 popular Rust web frameworks—which one is right for you?

By Serdar Yegulalp

Over the last decade or so, a slew of Rust web frameworks have appeared, each built with slightly different users and feature needs in mind. All of them benefit from Rust's type safety, memory safety, speed, and correctness.

This article is a quick look at five of the most popular Rust web frameworks: Actix Web, Rocket, Warp, Axum, and Poem. All of them provide common elements for web services: routing, request handling, multiple response types, and middleware. Note that these frameworks do not provide templating, which is typically handled by separate crates.

Actix Web

Actix Web is easily the most popular web framework for Rust. It satisfies just about all the major needs: it's high-performance, supports a broad swath of server features, and requires little ceremony to put together a basic site.

The name "Actix Web" originally referred to the framework's dependency on the actix actor framework, but the framework mostly shed that dependency some time ago. All of Actix Web's features are available on the stable Rust branch.

Here's a basic "hello world" app in Actix Web:

use actix_web::{get, App, HttpResponse, HttpServer, Responder};#[get("/")]async fn hello() -> impl Responder { HttpResponse::Ok().body("Hello world!")}#[actix_web::main]async fn main() -> std::io::Result<()> { HttpServer::new(|| App::new().service(hello)) .bind(("127.0.0.1", 8080))? .run() .await}

The get() attribute on the hello() function indicates what route it's meant to service, but it isn't active until it's added to the App object with the .service() method. Actix Web also supports more advanced route construction—for instance, you can capture positional variables from the URL and use those to route requests to functions that don't use get().

Performance is a big draw for Actix Web. All requests and responses are handled as distinct types. The server uses a thread pool to handle requests, with nothing shared between the threads to maximize performance. You can manually share state if needed, by using an Arc<>, but Actix Web's maintainers urge against doing anything that blocks worker threads and would thus sabotage performance. For long-running non-CPU bound work, use futures or async.

Actix Web also provides type-based handlers for error codes, and it uses a built-in middleware system (which you can also use) to implement logging. The framework also includes a general-purpose user session management system with cookies as the default storage type, though you can add others if you wish. Static files and directories can also be served with their own dedicated handlers.

Many common web service functions come bundled with Actix Web, along with some that are less common. These include handling URL-encoded bodies for forms, automatic promotion to HTTPS/2, decompressing Brotli, gzip, deflate, and zstd-compressed data, and handling chunked encoding. For WebSockets, Actix Web requires the actix-web-actors crate, which is its one major dependency. Likewise, for multipart streams, you need the actix-multipart crate. (For converting to and from JSON, Actix Web uses serde and serde_json, which ought to be familiar to Rust users generally.)

Actix Web drew attention back in 2020 when its original maintainer quit the project, allegedly over criticism about its use of unsafe code. However, other lead maintainers continued developing the framework, and it has continued to grow in the years since. Much of the unsafe code has been removed.

Rocket

Rocket's big distinction among Rust web frameworks is that it lets you get the most results with the least code. Writing a basic web application in Rocket takes relatively few lines and little ceremony. Rocket accomplishes this by using Rust's type system to describe many behaviors, so they can be enforced and encoded at compile time.

Here's a basic "hello world" app in Rocket:

#[macro_use] extern crate rocket;#[get("/")]fn hello_world() -> &'static str { "Hello, world!"}#[launch]fn rocket() -> _ { rocket::build().mount("/", routes![hello_world])}

Rocket works so tersely through its use of attributes. Routes are decorated with attributes for the methods and URL patterns they utilize. As you see in this example, the #[launch] attribute indicates the function used to mount the routes and set up the application to listen for requests.

Although the routes in the "hello world" example are synchronous, routes can be asynchronous in Rocket, and they generally ought to be when possible. By default, Rocket uses the tokio runtime to handle things like converting sync operations to async.

Rocket provides many of the usual features for handling requests—extracting variables from URL elements, for instance. One unique feature is "request guards," where you use Rust types, implementing Rocket's FromRequest trait, to describe a validation policy for a route.

For instance, you could create a custom type to prevent a route from firing unless certain information was present in the request headers and could be validated—such as a cookie with a certain permission associated with it. This lets you build things like permissions into Rust's compile-time type safety.

Another useful and distinct Rocket feature is fairings, Rocket's version of middleware. Types that implement the Fairing trait can be used to add callbacks to events, like requests or responses. But fairings can't change or halt requests (although they can access copies of the request data).

To that end, fairings are best for things that have global behavior—logging, gathering performance metrics, or overall security policies. For actions like authentication, use a request guard.

Warp

Warp's big distinction from other Rust web frameworks is the way it uses composable components—"filters," in Warp's lingo—that can be chained together to create services.

A basic "hello world" in Warp does not demonstrate this feature particularly well, but it's worth showing how concise the framework can be:

use warp::Filter;#[tokio::main]async fn main() { let hello = warp::path!().map(|| "Hello world"); warp::serve(hello).run(([127, 0, 0, 1], 8080)).await;}

Filters implement the Filter trait, each filter capable of passing output to another filter to modify behaviors. In this example, warp::path is a filer that can be chained into other operations, such as .map() to apply a function.

Another example from Warp's documentation shows off the filter system in more detail:

use warp::Filter;let hi = warp::path("hello") .and(warp::path::param()) .and(warp::header("user-agent")) .map(|param: String, agent: String| { format!("Hello {}, whose agent is {}", param, agent) });

Here, several filters are chained together to create the following behavior, in this order:

Developers fond of the compositional approach will like how Warp complements their way of working.

One consequence of the compositional approach is that you can do the same thing in a variety of different ways, not all of them intuitive. It's worth looking at theexamples in Warp's repository to see the different ways to solve common programming scenarios using Warp.

Another consequence comes from the way filters work at compile time. Composing many routes from many different filters can make compile time longer, although the routes are speedy at runtime. Another option is to use dynamic dispatch for sites with many routes, at a slight cost to runtime performance. One example shows how to do this with a BoxedFilter type.

Axum

The Axum framework builds atop the tower crate ecosystem for client/server applications of all kinds, as well as tokio for async. This makes it easier to use Axum if you already have experience with tower or use it in allied projects.

Here's a basic Axum "hello world" app found in Axum's documentation. You'll note it doesn't look all that different from the likes of Actix:

use axum::{ routing::get, Router,};#[tokio::main]async fn main() { let app = Router::new().route("/", get(|| async { "Hello, World!" })); let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap(); axum::serve(listener, app).await.unwrap();}

Axum uses many of the same patterns as Actix for how routes and handlers work. Route-handler functions are added to a Router object with the .route() method, and the axum::extract module contains types for extracting URL components or POST payloads. Responses implement the IntoResponse trait and errors are handled through tower's own tower::Service Error type.

This last behavior, relying on tower for key Axum components, also includes how Axum handles middleware. Routers, methods, and individual handlers can all have middleware applied by way of different .layer methods in tower objects. One can also use tower::ServiceBuilder to create aggregates of multiple layers and apply them together.

Axum provides tools of its own for other common patterns in web services. Sharing state between handlers, for instance, can be done in a type-safe way with a State type. Ways to implement typical scenarios like graceful shutdowns or setting up database connectivity can be found in Axum's examples directory.

Poem

Most languages have at least one full-featured, "maximalist" web framework (e.g., Django in Python), and one tiny, concise, "minimalist" web framework (e.g., Bottle, again in Python). Poem is at the minimal end of the spectrum for Rust, offering just enough features by default to stand up a basic web service.

Here's a "hello world" example echoing the username when included in the URL:

use poem::{get, handler, listener::TcpListener, web::Path, Route, Server};#[handler]fn hello(Path(name): Path<String>) -> String { format!("hello: {}", name)}#[tokio::main]async fn main() -> Result<(), std::io::Error> { let app = Route::new().at("/hello/:name", get(hello)); Server::new(TcpListener::bind("0.0.0.0:3000")) .run(app) .await}

Many of the features in this app should be familiar from the other frameworks and examples you've seen so far: setting up routes, binding URLs and handlers to them, extracting elements from the request, and so on.

To keep compile time down, Poem by default does not install support for certain features. Cookies, CSRF projection, HTTP over TLS, WebSockets, internationalization, and request/response compression and decompression (to name a few) all need to be enabled manually.

For all its simplicity, Poem still comes with plenty of utility. It includes a slew of common, useful middleware pieces, and you can also pretty easily implement your own. One thoughtful convenience is NormalizePath, a mechanism for making request paths consistent. This includes a universal handler for handling trailing slashes in a URL. With the handler, you can implement your preferred format once and consistently across the application.

Poem's examples directory is smaller than some of the other frameworks you've seen here, but it focuses mostly on examples that require detailed documentation—such as using Poem with AWS Lambda, or generating APIs that conform to the OpenAPI spec.

Which Rust framework is best for you?

Actix Web works as a good, balanced solution overall, especially if performance is a goal. Rocket lets you keep your code short but expressive, and its "fairings" system provides a powerful metaphor for implementing middleware behavior.

Programmers who like working with composable elements will want to try out Warp, as it lets you programmatically build routes and workflows with great expressiveness. Axum will appeal most directly to Rust users who are already familiar with the tower ecosystem, but it's useful enough that it's not limited to that audience, either. Poem is simple by default, and great that way if all you need is the most basic routing and request handling. You can also install additional features if you need them.

© Info World