The Async Ecosystem in Rust
The Rust web development ecosystem includes several powerful crates that work together to facilitate building robust web applications. This guide provides an overview of the tower
, http
, http_body
, and axum
crates and explains how they work together.
http
Crate
The http
crate provides common types for HTTP requests and responses, such as Request
, Response
, Method
, and StatusCode
. It defines types that are independent of any specific HTTP implementation, allowing different libraries and frameworks to interoperate by using these shared abstractions.
http_body
Crate
The http_body
crate defines the Body
trait, an abstraction over HTTP bodies, ensuring that different libraries can work with various body implementations interchangeably. The abstraction handles the streaming of bodies, which is a detail that is often overlooked.
axum
, hyper
, tower_http
and many others use the http_body
crate to handle bodies.
tower
Crate
The tower
crate is a library of modular components for building robust networking clients and servers. It provides the Service
trait:
// This single trait enables the entire middleware ecosystem pub trait Service<Request> { type Response; type Error; type Future: Future; fn call(&mut self, req: Request) -> Self::Future; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>; }
The Service
trait is the foundation of the tower
ecosystem, it represents an asynchronous function from a request to a response. The tokio blog has a great explanation of why and how the Service
trait was created.
tower-layer
Crate
The tower-layer
crate provides the Layer
trait, which is how middleware is composed in the Tower ecosystem:
pub trait Layer<S> { type Service; fn layer(&self, inner: S) -> Self::Service; }
This simple trait enables powerful middleware composition. For example, you can add timeout and tracing middleware to any service:
use tower::{ServiceBuilder, timeout::TimeoutLayer, trace::TraceLayer}; let service = ServiceBuilder::new() .layer(TimeoutLayer::new(Duration::from_secs(30))) .layer(TraceLayer::new_for_http()) .service(my_service);
Each layer wraps the previous service, forming a middleware stack. This is similar to how express
middlewares from the node ecosystem work.
tower_http
Crate
The tower_http
crate is a collection of middleware components for tower
. Unlike tower
, tower_http
is not a framework, but a set of utilities that can be reused, such as load balancing, timeouts, and retries.
The name could be more accurately named tower_http_utils
, but alas.
hyper
Crate
The hyper
crate is a fast HTTP library. It’s the foundation for the axum
framework that we’ll cover next. There’s not much to cover here, as it is a low level library that you probably won’t use directly.
But it is good to know that it uses http
and http_body
under the hood.
axum
Framework
The axum
framework is a web application framework built on top of tower
and hyper
. It leverages the Service
trait from tower
to compose routes and handlers in a modular fashion.
As such, you can use tower
middlewares, such as tower_http
utilities that we just discussed. However, axum
also provides its own set of middlewares, that are more ergonomic to use, but are specific to axum
and eat a slight performance hit.
Interoperability
Let’s review the interoperability between these crates:
- Shared Types: By utilizing the
http
crate’s common types, these libraries ensure compatibility at the fundamental level of HTTP messages. - Body Abstraction: The
http_body
crate’sBody
trait allows different body implementations to be used interchangeably acrosstower
services andaxum
handlers. - Service Composition:
axum
builds upontower
’sService
trait, enabling the use oftower
middleware withinaxum
applications for features like logging, authentication, and rate limiting. - Middleware and Utilities: Libraries in this ecosystem can share middleware components, thanks to the standardized interfaces provided by
tower
.
Case Study: Building an API Client
I wanted to build an API client, but I wanted to be able to plug in different HTTP implementations (like hyper
or reqwest
) or even explore different protocols (like grpc
) in the future.
I read Designing Rust bindings for REST APIs and while I liked the idea of a Query
trait, I didn’t like the fact that you essentially were re-inventing http
and tower::Service
.
Instead, I found out about the tower
ecosystem and realized I could build a Query
trait that is generic over the client, and even the protocol. So I embraced the ecosystem, leaving me to only implement a Query
trait:
pub trait Query<Input, Output, Message, Reply> { /// The error type for the query type Error; /// Query the service fn query(&mut self, input: Input) -> impl Future<Output = Result<Output, Self::Error>>; }
The trait is generic over Input
, Output
, Message
, and Reply
.
Input
is the type of the input to the query.Output
is the type of the output of the query.Message
is the type of the message to send to the service.Reply
is the type of the reply to expect from the service.
The trait is then implemented for any Service
that implements tower::Service<http::Request<http_body::Body>, http::Response<http_body::Body>>
. I can then implement my reqwest
service, or hyper
service, and plug it in without changing the client’s API.
Since the Query
trait’s input
is anything that implements Into<http::Request<http_body::Body>>
, I can plug in anything that can be converted into an HTTP request. This makes the client very flexible.
As an example, here’s a GetUser
struct that acts as a command to get a user by their ID:
struct GetUser { id: u64, } impl From<GetUser> for http::Request<String> { fn from(value: GetUser) -> Self { let url = format!("https://api.example.com/users/{}", value.id); http::Request::get(url) } }
The GetUser
struct can now be converted into an HTTP request:
let user = my_reqwest_client.query(GetUser { id: 1 }).await?; // Or with hyper: let user = my_hyper_client.query(GetUser { id: 1 }).await?; // Or through grpc: let user = my_grpc_client.query(GetUser { id: 1 }).await?;
Summary
The combination of tower
, http
, http_body
, and axum
creates an ecosystem for building asynchronous, modular applications in Rust. By adhering to common interfaces and abstractions, these libraries enable developers to build services that are easier to understand, test, and reuse.