diff --git a/README.md b/README.md
index 0fd49595..3972b91b 100644
--- a/README.md
+++ b/README.md
@@ -1,283 +1,462 @@
# tower-web
-This is *not* https://github.com/carllerche/tower-web even though the name is
-the same. Its just a prototype of a minimal HTTP framework I've been toying
-with. Will probably change the name to something else.
+tower-web (name pending) is a tiny web application framework that focuses on
+ergonimics and modularity.
-# What is this?
+### Goals
-## Goals
+- Ease of use. Build 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, extracing 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.
-- As easy to use as tide. I don't really consider warp easy to use due to type
- tricks it uses. `fn route() -> impl Filter<...>` also isn't very ergonomic.
- Just `async fn(Request) -> Response` would be nicer.
-- Deep integration with Tower meaning you can
- - Apply middleware to the entire application.
- - Apply middleware to a single route.
- - Apply middleware to subset of routes.
-- Just focus on routing and generating responses. Tower can do the rest.
- Want timeouts? Use `tower::timeout::Timeout`. Want logging? Use
- `tower_http::trace::Trace`.
-- Work with Tokio. tide is cool but requires async-std.
-- Not macro based. Heavy macro based APIs can be very ergonomic but comes at a
- complexity cost. Would like to see if I can design an API that is ergonomic
- and doesn't require macros.
+### Non-goals
-## Non-goals
+- Runtime independent. tower-web is designed to work with tokio and hyper
+and focused on bringing a good to experience to that stack.
+- Speed. tower-web is a of course a fast framework, and wont be the
+bottleneck in your app, but the goal is not to top the benchmarks.
-- Runtime independent. If becoming runtime independent isn't too much then fine
- but explicitly designing for runtime independence isn't a goal.
-- Speed. As long as things are reasonably fast that is fine. For example using
- async-trait for ergonomics is fine even though it comes at a cost.
+## Example
-# Example usage
-
-NOTE: Error handling has changed quite a bit and these examples are slightly out
-of date. See the examples for working examples.
-
-Defining a single route looks like this:
+The "Hello, World!" of tower-web is:
```rust
-let app = tower_web::app().at("/").get(root);
+use tower_web::prelude::*;
+use hyper::Server;
+use std::net::SocketAddr;
+use tower::make::Shared;
-async fn root(req: Request
) -> &'static str {
- "Hello, World!"
+#[tokio::main]
+async fn main() {
+ // build our application with a single route
+ let app = route("/", get(handler));
+
+ // run it with hyper on localhost:3000
+ let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
+ let server = Server::bind(&addr).serve(Shared::new(app));
+ server.await.unwrap();
}
-```
-Adding more routes follows the same pattern:
-
-```rust
-let app = tower_web::app()
- .at("/")
- .get(root)
- .at("/users")
- .get(users_index)
- .post(users_create);
-```
-
-Handler functions are just async functions like:
-
-```rust
async fn handler(req: Request) -> &'static str {
"Hello, World!"
}
```
-They must take the request as the first argument but all arguments following
-are called "extractors" and are used to extract data from the request (similar
-to rocket but without macros):
+## Routing
+
+Routing between handlers looks like this:
```rust
-#[derive(Deserialize)]
-struct UserPayload {
- username: String,
+use tower_web::prelude::*;
+
+let app = route("/", get(get_slash).post(post_slash))
+ .route("/foo", get(get_foo));
+
+async fn get_slash(req: Request) {
+ // `GET /` called
}
+async fn post_slash(req: Request) {
+ // `POST /` called
+}
+
+async fn get_foo(req: Request) {
+ // `GET /foo` called
+}
+```
+
+Routes can also be dynamic like `/users/:id`. See ["Extracting data from
+requests"](#extracting-data-from-requests) for more details on that.
+
+## Responses
+
+Anything that implements [`IntoResponse`] can be returned from a handler:
+
+```rust
+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) -> &'static str {
+ "foo"
+}
+
+// String works too and will get a text/plain content-type
+async fn plain_text_string(req: Request) -> String {
+ format!("Hi from {}", req.uri().path())
+}
+
+// Bytes will get a `application/octet-stream` content-type
+async fn bytes(req: Request) -> Vec {
+ vec![1, 2, 3, 4]
+}
+
+// `()` gives an empty response
+async fn empty(req: Request) {}
+
+// `StatusCode` gives an empty response with that status code
+async fn empty_with_status(req: Request) -> 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) -> (StatusCode, &'static str) {
+ (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong")
+}
+
+// `Html` gives a content-type of `text/html`
+async fn html(req: Request) -> Html<&'static str> {
+ Html("Hello, World!
")
+}
+
+// `Json` gives a content-type of `application/json` and works with my type
+// that implements `serde::Serialize`
+async fn json(req: Request) -> Json {
+ Json(json!({ "data": 42 }))
+}
+
+// `Result` where `T` and `E` implement `IntoResponse` is useful for
+// returning errors
+async fn result(req: Request) -> Result<&'static str, StatusCode> {
+ Ok("all good")
+}
+
+// `Response` gives full control
+async fn response(req: Request) -> Response {
+ 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` as its first argument
+but any arguments following are called "extractors". Any type that
+implements [`FromRequest`](crate::extract::FromRequest) can be used as an
+extractor.
+
+[`extract::Json`] is an extractor that consumes the request body and
+deserializes as as JSON into some target type:
+
+```rust
+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, payload: extract::Json) {
+ 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`]:
+
+```rust
+use tower_web::prelude::*;
+use uuid::Uuid;
+
+let app = route("/users/:id", post(create_user));
+
+async fn create_user(req: Request, params: extract::UrlParams<(Uuid,)>) {
+ let (user_id,) = params.0;
+
+ // ...
+}
+```
+
+There is also [`UrlParamsMap`](extract::UrlParamsMap) which provide a map
+like API for extracting URL params.
+
+You can also apply multiple extractors:
+
+```rust
+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,
}
-async fn handler(
+impl Default for Pagination {
+ fn default() -> Self {
+ Self { page: 1, per_page: 30 }
+ }
+}
+
+async fn get_user_things(
req: Request,
- // deserialize response body with `serde_json` into a `UserPayload`
- user: extract::Json,
- // deserialize query string into a `Pagination`
- pagination: extract::Query,
-) -> &'static str {
- let user: UserPayload = user.0;
- let pagination: Pagination = pagination.0;
+ params: extract::UrlParams<(Uuid,)>,
+ pagination: Option>,
+) {
+ let user_id: Uuid = (params.0).0;
+ let pagination: Pagination = pagination.unwrap_or_default().0;
// ...
}
```
-The inputs can also be optional:
+See the [`extract`] module for more details.
+
+[`Uuid`]: https://docs.rs/uuid/latest/uuid/
+
+## Applying middleware
+
+tower-web is designed to take full advantage of the tower and tower-http
+ecosystem of middleware:
+
+### To individual handlers
+
+A middleware can be applied to a single handler like so:
```rust
-async fn handler(
- req: Request,
- user: Option>,
-) -> &'static str {
- // ...
-}
+use tower_web::prelude::*;
+use tower::limit::ConcurrencyLimitLayer;
+
+let app = route(
+ "/",
+ get(handler.layer(ConcurrencyLimitLayer::new(100))),
+);
+
+async fn handler(req: Request) {}
```
-You can also get the raw response body:
+### To groups of routes
+
+Middleware can also be applied to a group of routes like so:
```rust
-async fn handler(
- req: Request,
- // buffer the whole request body
- body: Bytes,
-) -> &'static str {
- // ...
-}
+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) {}
+
+async fn post_foo(req: Request) {}
```
-Or limit the body size:
+### 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:
```rust
-async fn handler(
- req: Request,
- // max body size in bytes
- body: extract::BytesMaxLength<1024>,
-) -> &'static str {
- // ...
-}
+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::() {
+ 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) {}
```
-Params from dynamic routes like `GET /users/:id` can be extracted like so
+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:
```rust
-async fn handle(
- req: Request,
- // get a map of key value pairs
- map: extract::UrlParamsMap,
-) -> &'static str {
- let raw_id: Option<&str> = map.get("id");
- let parsed_id: Option = map.get_typed::("id");
+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)))
+ .handle_error(|error: BoxError| {
+ // ...
+ });
-async fn handle(
- req: Request,
- // or get a tuple with each param
- params: extract::UrlParams<(i32, String)>,
-) -> &'static str {
- let (id, name) = params.0;
-
- // ...
-}
+async fn handle(req: Request) {}
```
-If you wanna go all out you can even deconstruct the extractor directly in the
-function signature:
+### Applying multiple middleware
+
+[`tower::ServiceBuilder`] can be used to combine multiple middleware:
```rust
-async fn handle(
- req: Request,
- UrlParams((id, name)): UrlParams<(i32, String)>,
-) -> &'static str {
- // ...
-}
-```
+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;
-Anything that implements `FromRequest` can work as an extractor where
-`FromRequest` is an async trait:
-
-```rust
-#[async_trait]
-pub trait FromRequest: Sized {
- type Rejection: IntoResponse;
-
- async fn from_request(req: &mut Request) -> Result;
-}
-```
-
-This "extractor" pattern is inspired by Bevy's ECS. The idea is that it should
-be easy to pick apart the request without having to repeat yourself a lot or use
-macros.
-
-The return type must implement `IntoResponse`:
-
-```rust
-async fn empty_response(req: Request) {
- // ...
-}
-
-// gets `content-type: text/plain`
-async fn string_response(req: Request) -> String {
- // ...
-}
-
-// gets `content-type: appliation/json`. `Json` can contain any `T: Serialize`
-async fn json_response(req: Request) -> response::Json {
- // ...
-}
-
-// gets `content-type: text/html`. `Html` can contain any `T: Into`
-async fn html_response(req: Request) -> response::Html {
- // ...
-}
-
-// or for full control
-async fn response(req: Request) -> Response {
- // ...
-}
-
-// Result is supported if each type implements `IntoResponse`
-async fn response(req: Request) -> Result, StatusCode> {
- // ...
-}
-```
-
-This makes error handling quite simple. Basically handlers are not allowed to
-fail and must always produce a response. This also means users are in charge of
-how their errors are mapped to responses rather than a framework providing some
-opaque catch all error type.
-
-You can also apply Tower middleware to single routes:
-
-```rust
-let app = tower_web::app()
- .at("/")
- .get(send_some_large_file.layer(CompressionLayer::new()))
-```
-
-Or to the whole app:
-
-```rust
-let service = tower_web::app()
- .at("/")
- .get(root)
- .into_service()
-
-let app = ServiceBuilder::new()
+let middleware_stack = ServiceBuilder::new()
+ // Return an error after 30 seconds
.timeout(Duration::from_secs(30))
- .layer(TraceLayer::new_for_http())
+ // 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())
- .service(app);
+ .into_inner();
+
+let app = route("/", get(|_: Request| async { /* ... */ }))
+ .layer(middleware_stack)
+ .handle_error(|error: BoxError| {
+ if error.is::() {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ "Try again later".into(),
+ );
+ }
+
+ if error.is::() {
+ return (
+ StatusCode::REQUEST_TIMEOUT,
+ "Request took too long".into(),
+ );
+ };
+
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Cow::from(format!("Unhandled internal error: {}", error)),
+ );
+ });
```
-And of course run it with Hyper:
+## 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:
```rust
-#[tokio::main]
-async fn main() {
- tracing_subscriber::fmt::init();
+use tower_web::{AddExtensionLayer, prelude::*};
+use std::sync::Arc;
- // build our application with some routes
- let app = tower_web::app()
- .at("/")
- .get(handler)
- // convert it into a `Service`
- .into_service();
+struct State {
+ // ...
+}
- // add some middleware
- let app = ServiceBuilder::new()
- .layer(TraceLayer::new_for_http())
- .service(app);
+let shared_state = Arc::new(State { /* ... */ });
- // run it
- let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
- tracing::debug!("listening on {}", addr);
- let server = Server::bind(&addr).serve(Shared::new(app));
- server.await.unwrap();
+let app = route("/", get(handler)).layer(AddExtensionLayer::new(shared_state));
+
+async fn handler(
+ req: Request,
+ state: extract::Extension>,
+) {
+ let state: Arc = state.0;
+
+ // ...
}
```
-See the examples directory for more examples.
+## Routing to any [`Service`]
-# TODO
+tower-web also supports routing to general [`Service`]s:
-- `RouteBuilder` should have an `async fn serve(self) -> Result<(),
- hyper::Error>` for users who just wanna create a hyper server and not care
- about the lower level details. Should be gated by a `hyper` feature.
-- Each new route makes a new allocation for the response body, since `Or` needs
- to unify the response body types. Would be nice to find a way to avoid that.
-- It should be possible to package some routes together and apply a tower
- middleware to that collection and then merge those routes into the app.
+```rust
+use tower_web::{
+ service, prelude::*,
+ // `ServiceExt` adds `handle_error` to any `Service`
+ ServiceExt,
+};
+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| 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
+
+TODO
+
+[tower]: https://crates.io/crates/tower
+[tower-http]: https://crates.io/crates/tower-http