The Network Services Working Group aims to improve the story for web development this year in several respects: by bolstering foundations like async/await, by improving the ecosystem of web-related crates, and by pulling these pieces together into a framework and book called Tide. The name “Tide” refers to “a rising tide lifts all boats”; the intent is to improve sharing, compatibility, and improvements across all web development and frameworks in Rust.

The role of web frameworks

In a nutshell, the goal of a web framework is to let you develop web applications in a way that feels “native” to the host language. What exactly that means is a matter of huge debate, and most languages have multiple popular web frameworks taking different approaches.

In any case, the core problem is to solve various mismatches:

  • A web app ultimately needs to speak HTTP, but programmers would generally prefer to think about the core “business logic” in purely native terms. At the simplest level, you want your core logic to work directly with things like integers and structs, rather than raw string data. But this division also applies to things like authentication, parsing URLs, or even how you express the set of services provided by your app.
  • A particularly vital aspect of the above factoring is dealing with serialization and/or templating: what kind of data appears in the raw request/response bodies, and how does it connect to Rust types?
  • Many web apps also work with databases, where there is another set of mismatches between the “native” and database-centric view of things. An object-relational mapping like Diesel is one way of addressing this.

Beyond addressing these mismatches, web frameworks sometimes have further ambitions, like:

  • Good security by default.
  • Consistent app structuring, to make it easy to navigate a foreign codebase that uses the framework.
  • Standard versions of other concerns like thread and connection pools, etc.
  • Workflows for rapid code generation.

This is hard work! It requires a large set of underlying libraries to do the grunt work, and careful design and ergonomics work on top to achieve the desired separation of concerns.

The vision for Tide

Our goal with Tide is twofold:

  • First and foremost, to bolster the “grunt work” crates that provide core web functionality. Crates like http and url are examples of relatively mature core libraries in this space. We want more such mature crates! A big part of the effort is identifying and finding ways to improve, standardize on, and/or document such crates.
  • Secondly, to build a serious framework on top of these crates, ideally as a very minimal layer. That is, whenever feasible we want to use existing crates, and when not, we want to create small, separate crates rather than a monolithic framework.

All of this will also come together in a book, documenting both the underlying crates and the framework.

We want to do all of this in an open and collaborative way from the outset. This blog post is the first in an open-ended series exploring various design questions and seeking feedback and alternative ideas. If you want to get involved in any aspect of this work, reach out in the WG-net-web channel on Discord!

Kickoff topics

The rest of this post aims to kick off discussion on a few central topics by surveying the ecosystem. We plan on having a steady stream of follow-up posts raising questions, presenting strawman ideas, and generally developing the framework as a community project.

The basic service API

Most language ecosystems have ended up with a key interface for talking about web services in general: Ruby has Rack, Python has WSGI, Java has Servlets, and so on.

These interfaces specify what it means to be a web server, which generally looks like some kind of function that is given a request and produces a response (usually for HTTP). Having such an interface means you can decouple a web framework (which helps you produce such a function) from an underlying web server (which actually handles the work of implementing HTTP). It also makes it possible to provide generic low-level middleware that’s usable with any choice of server and framework.

Today in Rust most, but not all web frameworks use hyper directly to provide basic HTTP functionality, which prevents this decoupling and easy middleware application. However, the tower-service crate is poised to provide a more generic interface through its Service trait, and there is already a substantial amount of middleware as part of the Tower project, though it has not yet been published to crates.io.

In brief, the Service trait models an asynchronous request to response function, where the request and response types are themselves generic. For HTTP, these would be specialized to the types from the http crate. The trait is also set up to handle per-connection state (through the NewService factory trait) and backpressure (through the ready method).

There are some important open questions here:

  • The Service trait was designed before borrowing within async/await was proposed, and it’s possible that some changes would be desirable there. In particular, in some circumstances you may have borrowed data that you wish to write into a response without making an extra owned copy. It’s not clear whether it’s possible to do this with the current setup. There’s an issue here for further discussion.

  • While the http crate provides basic request and response types, the body data is treated generically. So, to establish a standard specialization of Service to HTTP, we also need to standardize those types (or constraints around them), which also means accounting for streaming bodies. A recent post about tower-web proposed one such approach, based on a new BufStream trait.

In short, there’s already a very solid basis for a service abstraction in Rust, and hopefully the working group can help push toward standardization and improvements as needed. This abstraction would also act as the starting point for building Tide.

Routing strategies

Taking the above abstraction for granted, you can view a web framework as a very fancy way of helping you write what is ultimately a single request-to-response function. As explained at the beginning of the post, a lot of the work that a framework does is to separate out concerns so that you can write your core business logic in a natural style.

A starting point for most frameworks is some system for routing, which maps URLs and HTTP request types to specific functions that contain business logic — often called endpoints. The routing mechanism is often a place where some other HTTP-centric concerns are dealt with, e.g. validation. Hence, it can be a defining aspect of how a framework fits together.

Approaches to routing tend to fall into one of three categories:

Endpoint-centric routing

This is the approach taken by Rocket and Tower Web. You “decorate” a standard Rust function with attributes that include information about the URL that should map to that function, as well as how to extract various parameters and (for Rocket) to perform additional validation. Here’s a snippet from Tower Web:

#[get("/")]
#[content_type("json")]
fn hello_world(&self) -> Result<HelloResponse, ()> { .. }

The appeal is clear: this setup has a very low barrier to entry, and puts all of the emphasis on the “native Rust” functions implementing the endpoints. On the other hand, it generally requires a macro-based implementation, and can be more difficult to extend or customize than some of the other approaches.

Table-of-contents routing

This is the approach taken by Gotham, Rouille, and Actix Web. You construct routing logic separately from the endpoints by using builder-style or macro-based APIs. For example, in Gotham, there’s an explicit Router data type that you can build:

fn router() -> Router {
    build_simple_router(|route| {
        route.request(vec![Get, Head], "/").to(index);
        route.get_or_head("/products").to(products::index);
        route.get("/bag").to(bag::index);

        route.scope("/checkout", |route| {
            route.get("/start").to(checkout::start);

            route
                .post("/payment_details")
                .to(checkout::payment_details::create);

            route
                .put("/payment_details")
                .to(checkout::payment_details::update);

            route.post("/complete").to(checkout::complete);
        });
    })
}

This “table of contents” factoring makes it easier to see the high-level structure of an app separately from the endpoint logic. It also facilitates dealing with multi-endpoint concerns, like a set of routes that all share a common validation requirement. On the other hand, it’s heavier weight than the endpoint-centric approach, and dealing with extraction (pulling out information from the request) tends to be more awkward.

Routing via free-form composition

This is the approach taken by Warp. In a way, this could also be termed the “router-free” approach: no distinction is made between endpoints and other aspects of an app. Instead, everything is a “filter”, essentially an HTTP-aware function, and you build up your app by composing filters. Routing is thus handled by specific filters:

// Just the path segment "todos"...
let todos = warp::path("todos");

// Combined with `index`, this means nothing comes after "todos".
// So, for example: `GET /todos`, but not `GET /todos/32`.
let todos_index = todos.and(warp::path::index());

// `GET /todos`
let list = warp::get2()
    .and(todos_index)
    .and(db.clone())
    .map(list_todos);

// `POST /todos`
let create = warp::post2()
    .and(todos_index)
    .and(warp::body::json())
    .and(db.clone())
    .and_then(create_todo);

// Combine our endpoints, since we want requests to match any of them:
let api = list
    .or(create);

// View access logs by setting `RUST_LOG=todos`.
let routes = api.with(warp::log("todos"));

It’s too early to say much about the pros and cons of this approach in Rust.

Routing in Tide

Given the lay of the land above, the question is what approach is best suited for Tide — a balance of giving maximal benefit to the ecosystem, and fitting in modularly (and hence not being specific to Tide).

At the moment, “table of contents”-style routing seems like the best fit. It’s well-established in other languages, and has been pioneered already in Rust — but could use some standardization and API review there as well. Compared to endpoint-centric routing, it’s more easily made modular/extensible. Compared to free-form composition, the tradeoffs are better understood.

To fully kick off this discussion, though, a strawman is helpful. The next post in this series will present one such strawman, delving into some of the API design questions that arise in table-of-contents-style routing in Rust, and examining avenues for modularity and sharing.