I previously announced Tower and mentioned that a web framework was in the works. It took longer than I had hoped (as it sometimes does with software), but today, I am opening up Tower Web.
Tower Web is an asynchronous HTTP web framework that focuses on removing boilerplate. It is built on top of Tokio, Hyper, and of course Tower. It works today on stable Rust.
Here is “hello world”.
#[macro_use]
extern crate tower_web;
use tower_web::ServiceBuilder;
#[derive(Clone, Debug)]
struct HelloWorld;
impl_web! {
impl HelloWorld {
/// @get("/")
fn hello_world(&self) -> Result<String, ()> {
Ok("Hello world".to_string())
}
}
}
pub fn main() {
let addr = "127.0.0.1:8080".parse().expect("Invalid address");
println!("Listening on http://{}", addr);
ServiceBuilder::new()
.resource(HelloWorld)
.run(&addr)
.unwrap();
}
Tower Web uses a macro based approach. The impl_web!
macro looks at the impl
blocks and generates the necessary glue code to serve HelloWorld
as an HTTP
service.
The @get("/")
doc comment is meaningful. This tells Tower Web that GET
requests to /
should be mapped to the hello_world
method. Tower Web opted to
use doc comments as annotations to enable supporting stable Rust today. Once
attribute macros land on the stable channel, Tower Web will switch to attribute
macros.
This hello world example is cool, but it doesn’t illustrate where Tower Web really shines.
Decoupling HTTP and application logic
Tower Web strives to enable applications to be written without including any HTTP related concerns. That means, methods should be written using only “plain old Rust types” (PORT?). To illustrate, lets look at an example that uses a path capture.
#[derive(Clone, Debug)]
struct HelloWorld;
impl_web! {
impl HelloWorld {
/// @get("/hello/:name")
fn greet(&self, name: String) -> Result<String, ()> {
Ok(format!("Hello, {}", name))
}
}
}
This example extracts a request path segment and uses that as part of the
response. What is interesting is that the greet
method is implemented without
any special types. It just takes a String
as the argument.
Tower Web uses convention over configuration. The convention is that the
argument identifier will match the capture identifier. In this case, both the
argument and capture are named name
. Because of this, Tower Web knows that the
name
argument should be populated with the value captured from the path.
Because the method is written without any special types, testing also becomes very easy.
#[test]
fn test_greet() {
let web = HelloWorld;
assert_eq!(
web.greet("carl".to_string()).unwrap(),
"Hello, carl");
}
When it comes to more complicated types, such as JSON request bodies, Tower Web also has you covered.
#[derive(Clone, Debug)]
struct HelloWorld;
#[derive(Debug, Extract)]
struct MyData {
foo: usize,
bar: Option<String>,
}
impl_web! {
impl HelloWorld {
/// @post("/data")
fn greet(&self, body: MyData) -> Result<String, ()> {
Ok(format!("Hello, {:?}", body))
}
}
}
The #[derive(Extract)]
generates an extract implementation for the type. It
uses Serde under the hood, so all the various Serde annotations work here too.
When used as an argument named body
, Tower Web will deserialize the request
body into the struct. Once again, the method has been implemented without any
HTTP specific types.
The following curl request will succeed:
curl --vv --request POST -H 'content-type: application/json' --data '{"foo":1,"bar":"baz"}' http://localhost:8080/data
Tower Web is able to do validation on the type as well. The foo
field is
defined to be numeric and required. Omitting it results in an HTTP 400, bad
request, response.
Responding
A resource method can return any type that implements Response
. String
is
one of these types. Some others include serde_json::Value
, http::Response
,
and tokio::fs::File
(for serving a file from disk). Tower Web also provides a
#derive(Response)
macro enabling custom return types.
#[derive(Debug, Response)]
#[web(status = "201")]
struct MyData {
foo: usize,
bar: Option<String>,
}
impl_web! {
impl HelloWorld {
/// @get("/data")
/// @content_type("json")
fn greet(&self) -> Result<MyData, ()> {
Ok(MyData {
foo: 123,
bar: None,
})
}
}
}
In this example, we respond with a custom type. The resource method is also
annotated with @content_type("json")
. This indicates that MyData
should be
serialized using the JSON format.
Also note that MyData
has an attribute: #[web(status = "201")]
. This tells
Tower Web to set the HTTP response status code to 201. There are a number of
other ways to customize the response with #[derive(Response)]
and these are
documented in the API docs and examples.
Asynchronous
So far, all the requests have been processed synchronously. However, resource
methods may return futures as well. Here is an example using impl Future
.
impl_web! {
impl HelloWorld {
/// @get("/async")
fn hello_future(&self) -> impl Future<Item = String, Error = ()> + Send {
// async implementation
}
}
}
Note that impl Future
is bound with Send
. Tower Web uses Hyper to serve the
application and Hyper requires the response future to be bound by Send
.
Just a taste
This is just a very quick tour of what Tower Web has to offer (I didn’t even touch on middleware). To get started, check out the examples and API docs found at the Tower Web repository. Also, there are a lot more features planned, so try things out, provide feedback, and get your hands dirty with some PRs.
Warp — the future of asynchronous web for Rust
As you might have seen, my esteemed colleague, seanmonstar recently announced Warp, a framework he has been working on. We both have been working in parallel and have been experimenting with different approaches to define web services in Rust.
The goal is to avoid fragmentation and rally behind a single crate. There is no need to duplicate effort and re-implement the world each time.
In a month or so, Tower Web will become a part of Warp. Warp will then provide both the filter based API and the macro API offered by Tower Web. This will let us focus on a single library.
Merging with Warp will also enable mixing and matching the various API styles.
For example, you could build a single service using both filters and macros. Or,
you could respond from a filter with a #[derive(Response)]
struct.
In the short term, while I gather feedback from the initial release, feature development will happen in the Tower Web repository.
Acknowledgements
I want to call out Jake Goulding (shepmaster). Jake provided a lot of valuable early feedback and has already contributed quite a good number of PRs ❤️.
Conclusion
Give Tower Web a shot, provide feedback and PRs. Also, I will be blogging again soon to talk more about the Tower / Web stack!