Ergonomic and modular web framework built with Tokio, Tower, and Hyper https://crates.io/crates/axum
Find a file
2021-06-08 22:27:38 +02:00
examples More docs and expand key_value_store example 2021-06-08 12:43:16 +02:00
src Remove some unnecessary dependencies 2021-06-08 22:27:38 +02:00
.gitignore Initial pile of hacks 2021-05-29 21:13:06 +02:00
Cargo.toml Remove some unnecessary dependencies 2021-06-08 22:27:38 +02:00
README.md More docs 2021-06-08 22:04:28 +02:00

tower-web

tower-web (name pending) is a tiny web application framework that focuses on ergonomics and modularity.

Goals

  • Ease of use. Building web apps in Rust should be as easy as async fn handle(Request) -> Response.
  • Solid foundation. tower-web is built on top of tower and makes it easy to plug in any middleware from the tower and tower-http ecosystem.
  • Focus on routing, extracting data from requests, and generating responses. Tower middleware can handle the rest.
  • Macro free core. Macro frameworks have their place but tower-web focuses on providing a core that is macro free.

Compatibility

tower-web is designed to work with tokio and hyper. Runtime and transport layer independence is not a goal, at least for the time being.

Example

The "Hello, World!" of tower-web is:

use tower_web::prelude::*;
use hyper::Server;
use std::net::SocketAddr;
use tower::make::Shared;

#[tokio::main]
async fn main() {
    // build our application with a single route
    let app = route("/", get(|request: Request<Body>| async {
        "Hello, World!"
    }));

    // run it with hyper on localhost:3000
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    Server::bind(&addr)
        .serve(Shared::new(app))
        .await
        .unwrap();
}

Routing

Routing between handlers looks like this:

use tower_web::prelude::*;

let app = route("/", get(get_slash).post(post_slash))
    .route("/foo", get(get_foo));

async fn get_slash(req: Request<Body>) {
    // `GET /` called
}

async fn post_slash(req: Request<Body>) {
    // `POST /` called
}

async fn get_foo(req: Request<Body>) {
    // `GET /foo` called
}

Routes can also be dynamic like /users/:id. See "Extracting data from requests" for more details on that.

Responses

Anything that implements IntoResponse can be returned from a handler:

use tower_web::{body::Body, response::{Html, Json}, prelude::*};
use http::{StatusCode, Response};
use serde_json::{Value, json};

// We've already seen returning &'static str
async fn plain_text(req: Request<Body>) -> &'static str {
    "foo"
}

// String works too and will get a text/plain content-type
async fn plain_text_string(req: Request<Body>) -> String {
    format!("Hi from {}", req.uri().path())
}

// Bytes will get a `application/octet-stream` content-type
async fn bytes(req: Request<Body>) -> Vec<u8> {
    vec![1, 2, 3, 4]
}

// `()` gives an empty response
async fn empty(req: Request<Body>) {}

// `StatusCode` gives an empty response with that status code
async fn empty_with_status(req: Request<Body>) -> StatusCode {
    StatusCode::NOT_FOUND
}

// A tuple of `StatusCode` and something that implements `IntoResponse` can
// be used to override the status code
async fn with_status(req: Request<Body>) -> (StatusCode, &'static str) {
    (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong")
}

// `Html` gives a content-type of `text/html`
async fn html(req: Request<Body>) -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}

// `Json` gives a content-type of `application/json` and works with any type
// that implements `serde::Serialize`
async fn json(req: Request<Body>) -> Json<Value> {
    Json(json!({ "data": 42 }))
}

// `Result<T, E>` where `T` and `E` implement `IntoResponse` is useful for
// returning errors
async fn result(req: Request<Body>) -> Result<&'static str, StatusCode> {
    Ok("all good")
}

// `Response` gives full control
async fn response(req: Request<Body>) -> Response<Body> {
    Response::builder().body(Body::empty()).unwrap()
}

let app = route("/plain_text", get(plain_text))
    .route("/plain_text_string", get(plain_text_string))
    .route("/bytes", get(bytes))
    .route("/empty", get(empty))
    .route("/empty_with_status", get(empty_with_status))
    .route("/with_status", get(with_status))
    .route("/html", get(html))
    .route("/json", get(json))
    .route("/result", get(result))
    .route("/response", get(response));

See the [response] module for more details.

Extracting data from requests

A handler function must always take Request<Body> as its first argument but any arguments following are called "extractors". Any type that implements FromRequest can be used as an extractor.

For example, [extract::Json] is an extractor that consumes the request body and deserializes it as JSON into some target type:

use tower_web::prelude::*;
use serde::Deserialize;

let app = route("/users", post(create_user));

#[derive(Deserialize)]
struct CreateUser {
    email: String,
    password: String,
}

async fn create_user(req: Request<Body>, payload: extract::Json<CreateUser>) {
    let payload: CreateUser = payload.0;

    // ...
}

[extract::UrlParams] can be used to extract params from a dynamic URL. It is compatible with any type that implements [std::str::FromStr], such as Uuid:

use tower_web::prelude::*;
use uuid::Uuid;

let app = route("/users/:id", post(create_user));

async fn create_user(req: Request<Body>, params: extract::UrlParams<(Uuid,)>) {
    let user_id: Uuid = (params.0).0;

    // ...
}

There is also UrlParamsMap which provide a map like API for extracting URL params.

You can also apply multiple extractors:

use tower_web::prelude::*;
use uuid::Uuid;
use serde::Deserialize;

let app = route("/users/:id/things", get(get_user_things));

#[derive(Deserialize)]
struct Pagination {
    page: usize,
    per_page: usize,
}

impl Default for Pagination {
    fn default() -> Self {
        Self { page: 1, per_page: 30 }
    }
}

async fn get_user_things(
    req: Request<Body>,
    params: extract::UrlParams<(Uuid,)>,
    pagination: Option<extract::Query<Pagination>>,
) {
    let user_id: Uuid = (params.0).0;
    let pagination: Pagination = pagination.unwrap_or_default().0;

    // ...
}

See the [extract] module for more details.

Applying middleware

tower-web is designed to take full advantage of the tower and tower-http ecosystem of middleware:

Applying middleware to individual handlers

A middleware can be applied to a single handler like so:

use tower_web::prelude::*;
use tower::limit::ConcurrencyLimitLayer;

let app = route(
    "/",
    get(handler.layer(ConcurrencyLimitLayer::new(100))),
);

async fn handler(req: Request<Body>) {}

Applying middleware to groups of routes

Middleware can also be applied to a group of routes like so:

use tower_web::prelude::*;
use tower::limit::ConcurrencyLimitLayer;

let app = route("/", get(get_slash))
    .route("/foo", post(post_foo))
    .layer(ConcurrencyLimitLayer::new(100));

async fn get_slash(req: Request<Body>) {}

async fn post_foo(req: Request<Body>) {}

Error handling

tower-web requires all errors to be handled. That is done by using [std::convert::Infallible] as the error type in all its [Service] implementations.

For handlers created from async functions this is works automatically since handlers must return something that implements IntoResponse, even if its a Result.

However middleware might add new failure cases that has to be handled. For that tower-web provides a handle_error combinator:

use tower_web::prelude::*;
use tower::{
    BoxError, timeout::{TimeoutLayer, error::Elapsed},
};
use std::{borrow::Cow, time::Duration};
use http::StatusCode;

let app = route(
    "/",
    get(handle
        .layer(TimeoutLayer::new(Duration::from_secs(30)))
        // `Timeout` uses `BoxError` as the error type
        .handle_error(|error: BoxError| {
            // Check if the actual error type is `Elapsed` which
            // `Timeout` returns
            if error.is::<Elapsed>() {
                return (StatusCode::REQUEST_TIMEOUT, "Request took too long".into());
            }

            // If we encounter some error we don't handle return a generic
            // error
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                // `Cow` lets us return either `&str` or `String`
                Cow::from(format!("Unhandled internal error: {}", error)),
            );
        })),
);

async fn handle(req: Request<Body>) {}

The closure passed to handle_error must return something that implements IntoResponse.

handle_error is also available on a group of routes with middleware applied:

use tower_web::prelude::*;
use tower::{BoxError, timeout::TimeoutLayer};
use std::time::Duration;

let app = route("/", get(handle))
    .route("/foo", post(other_handle))
    .layer(TimeoutLayer::new(Duration::from_secs(30)))
    .handle_error(|error: BoxError| {
        // ...
    });

async fn handle(req: Request<Body>) {}

async fn other_handle(req: Request<Body>) {}

Applying multiple middleware

[tower::ServiceBuilder] can be used to combine multiple middleware:

use tower_web::prelude::*;
use tower::{
    ServiceBuilder, BoxError,
    load_shed::error::Overloaded,
    timeout::error::Elapsed,
};
use tower_http::compression::CompressionLayer;
use std::{borrow::Cow, time::Duration};
use http::StatusCode;

let middleware_stack = ServiceBuilder::new()
    // Return an error after 30 seconds
    .timeout(Duration::from_secs(30))
    // Shed load if we're receiving too many requests
    .load_shed()
    // Process at most 100 requests concurrently
    .concurrency_limit(100)
    // Compress response bodies
    .layer(CompressionLayer::new())
    .into_inner();

let app = route("/", get(|_: Request<Body>| async { /* ... */ }))
    .layer(middleware_stack)
    .handle_error(|error: BoxError| {
        if error.is::<Overloaded>() {
            return (
                StatusCode::SERVICE_UNAVAILABLE,
                "Try again later".into(),
            );
        }

        if error.is::<Elapsed>() {
            return (
                StatusCode::REQUEST_TIMEOUT,
                "Request took too long".into(),
            );
        };

        return (
            StatusCode::INTERNAL_SERVER_ERROR,
            Cow::from(format!("Unhandled internal error: {}", error)),
        );
    });

Sharing state with handlers

It is common to share some state between handlers for example to share a pool of database connections or clients to other services. That can be done using the [AddExtension] middleware (applied with [AddExtensionLayer]) and the [extract::Extension] extractor:

use tower_web::{AddExtensionLayer, prelude::*};
use std::sync::Arc;

struct State {
    // ...
}

let shared_state = Arc::new(State { /* ... */ });

let app = route("/", get(handler)).layer(AddExtensionLayer::new(shared_state));

async fn handler(
    req: Request<Body>,
    state: extract::Extension<Arc<State>>,
) {
    let state: Arc<State> = state.0;

    // ...
}

Routing to any [Service]

tower-web also supports routing to general [Service]s:

use tower_web::{
    // `ServiceExt` adds `handle_error` to any `Service`
    service::{self, ServiceExt}, prelude::*,
};
use tower_http::services::ServeFile;
use http::Response;
use std::convert::Infallible;
use tower::{service_fn, BoxError};

let app = route(
    // Any request to `/` goes to a service
    "/",
    service_fn(|_: Request<Body>| async {
        let res = Response::new(Body::from("Hi from `GET /`"));
        Ok::<_, Infallible>(res)
    })
).route(
    // GET `/static/Cargo.toml` goes to a service from tower-http
    "/static/Cargo.toml",
    service::get(
        ServeFile::new("Cargo.toml")
            // Errors must be handled
            .handle_error(|error: std::io::Error| { /* ... */ })
    )
);

See the [service] module for more details.

Nesting applications

Applications can be nested by calling nest:

use tower_web::{prelude::*, routing::BoxRoute, body::BoxBody};
use tower_http::services::ServeFile;
use http::Response;
use std::convert::Infallible;

fn api_routes() -> BoxRoute<BoxBody> {
    route("/users", get(|_: Request<Body>| async { /* ... */ })).boxed()
}

let app = route("/", get(|_: Request<Body>| async { /* ... */ }))
    .nest("/api", api_routes());

nest can also be used to serve static files from a directory:

use tower_web::{prelude::*, service::ServiceExt, routing::nest};
use tower_http::services::ServeDir;
use http::Response;
use std::convert::Infallible;
use tower::{service_fn, BoxError};

let app = nest(
    "/images",
    ServeDir::new("public/images").handle_error(|error: std::io::Error| {
        // ...
    })
);