Introduction
Tide is a minimal and pragmatic Rust web application framework built for rapid development. It comes with a robust set of features that make building async web applications and APIs easier and more fun.
This Tide-book is still a work in progress, and will be expanded on over time.
All examples in the text are available as working Tide-projects
Getting started
In order to build a web app in Rust you need an HTTP server, and an async
runtime. After running cargo new --bin web-app
add the following lines to your
Cargo.toml
file:
# Example, use the version numbers you need
tide = "0.15.0"
async-std = { version = "1.6.5", features = ["attributes"] }
Example
Create an HTTP server that receives a JSON body, validates it, and responds with a confirmation message.
use tide::prelude::*; use tide::Request; #[derive(Debug, Deserialize)] struct Animal { name: String, legs: u8, } #[async_std::main] async fn main() -> tide::Result<()> { tide::log::start(); let mut app = tide::new(); app.at("/orders/shoes").post(order_shoes); app.listen("127.0.0.1:8080").await?; Ok(()) } async fn order_shoes(mut req: Request<()>) -> tide::Result { let Animal { name, legs } = req.body_json().await?; Ok(format!("Hello, {}! I've put in an order for {} shoes", name, legs).into()) }
$ curl localhost:8080/orders/shoes -d '{ "name": "Chashu", "legs": 4 }'
Hello, Chashu! I've put in an order for 4 shoes
Let's try now with an invalid number of legs (Note that we use the -v
flag to use curl in verbose mode).
$ curl -v localhost:8080/orders/shoes -d '{ "name": "Mary Millipede", "legs": 750 }'
< HTTP/1.1 422 Unprocessable Entity
< content-length: 0
< date: Fri, 26 Feb 2021 13:31:17 GMT
We get an http error, 422 to be more specific, and that is because we are using the body_json
method to deseriaize
the body into the Animal
struct and the legs
fields type is u8
. We will cover body_json
and the other available methods for deserialize
the body in the Request/Response chapter.
The server, Routes and Endpoints
The central part of a Tide application is the Server
struct. A Tide application is started by creating a
Server
and configuring it with Route
s to Endpoint
s.
When a Server
is started it will handle incoming Request
s by matching their URLs with Routes. Requests that match a route are then dispatched to the corresponding Endpoint
.
Set up a Server
A basic Tide Server
is constructed with tide::new()
.
#[async_std::main] async fn main() -> tide::Result<()> { let server = tide::new(); Ok(()) }
The server can then be started using the asynchronous listen
method.
#[async_std::main] async fn main() -> tide::Result<()> { let server = tide::new(); server.listen("127.0.0.1:8080").await?; Ok(()) }
While this is the simpelest Tide application that you can build, it is not very useful. It will return a 404 HTTP response to any request. To be able to return anything useful we will need to handle requests using one or more Endpoint
s
Handle requests with endpoints
To make the Server
return anything other than an HTTP 404 reply we need to tell it how to react to requests. We do this by adding one or more Endpoints;
#[async_std::main] async fn main() -> tide::Result<()> { let mut server = tide::new(); server.at("*").get(|_| async { Ok("Hello, world!") }); server.listen("127.0.0.1:8080").await?; Ok(()) }
We use the at
method to specify the route to the endpoint. We will talk about routes later. For now we'll just use the "*"
wildcard route that matches anything we throw at it. For this example we will add an async closure as the Endpoint
. Tide expects something that implements the Endpoint
trait here. But this closure will work because Tide implements the Endpoint
trait for certain async functions with a signature that looks like this;
async fn endpoint(request: tide::Request) -> tide::Result<impl Into<Response>>
In this case Into<Response>
is implemented for &str
so our closure is a valid Endpoint. Because Into<Response>
is implemented for several other types you can quickly set up endpoints. For example the next endpoint uses the json!
macro provided by use tide::prelude::*
to return a serde_json::Value
.
use tide::prelude::*; #[async_std::main] async fn main() -> tide::Result<()> { let mut server = tide::new(); server.at("*").get(|_| async { Ok(json!({ "meta": { "count": 2 }, "animals": [ { "type": "cat", "name": "chashu" }, { "type": "cat", "name": "nori" } ] })) }); server.listen("127.0.0.1:8080").await?; Ok(()) }
Returning quick string or json results is nice for getting a working endpoint quickly. But for more control a full Response
struct can be returned.
server.at("*").get(|_| async {
Ok(Response::new(StatusCode::Ok).set_body("Hello world".into()))
});
The Response
type is described in more detail in the next chapter.
More than one endpoint can be added by chaining methods. For example if we want to reply to a delete
request as well as a get
request endpoints can be added for both;
server.at("*")
.get(|_| async { Ok("Hello, world!") })
.delete(|_| async { Ok("Goodbye, cruel world!") });
Eventually, especially when our endpoint methods grow a bit, the route definitions will get a crowded. We could move our endpoint implementations to their own functions;
#[async_std::main] async fn main() -> tide::Result<()> { let mut server = tide::new(); server.at("*").get(endpoint); server.listen("127.0.0.1:8080").await?; Ok(()) } async fn endpoint(_req: tide::Request<()>) -> Result<Response> { Ok(Response::new(StatusCode::Ok).set_body("Hello world".into())) }
Defining and composing routes
The server we built is still not very useful. It will return the same response for any URL. It is only able to differentiate between requests by HTTP method. We already used the .at
method of the Server
to define a wildcard route. You might have guessed how to add endpoints to specific routes;
#[async_std::main] async fn main() -> tide::Result<()> { let mut server = tide::new(); server.at("/hello").get(|_| async { Ok("Hello, world!") }); server.at("/bye").get(|_| async { Ok("Bye, world!") }); server.listen("127.0.0.1:8080").await?; Ok(()) }
Here we added two routes for two different endpoints. Routes can also be composed by chaining the .at
method.
server.at("/hello").at("world").get(|_| async { Ok("Hello, world!") });
This will give you the same result as:
server.at("/hello/world").get(|_| async { Ok("Hello, world!") });
We can store the partial routes and re-use them;
#[async_std::main] async fn main() -> tide::Result<()> { let mut server = tide::new(); let hello_route = server.at("/hello"); hello_route.get(|_| async { Ok("Hi!") }); hello_route.at("world").get(|_| async { Ok("Hello, world!") }); hello_route.at("mum").get(|_| async { Ok("Hi, mum!") }); server.listen("127.0.0.1:8080").await?; Ok(()) }
Here we added two sub-routes to the hello
route. One at /hello/world
and another one at hello/mum
with different endpoint functions. We also added an endpoint at /hello
. This gives an idea what it will be like to build up more complex routing trees
When you have a complex api this also allows you to define different pieces of your route tree in separate functions.
#[async_std::main] async fn main() -> tide::Result<()> { let mut server = tide::new(); set_v1_routes(server.at("/api/v1")); set_v2_routes(server.at("/api/v2")); server.listen("127.0.0.1:8080").await?; Ok(()) } fn set_v1_routes(route: Route) { route.at("version").get(|_| async { Ok("Version one") }); } fn set_v2_routes(route: Route) { route.at("version").get(|_| async { Ok("Version two") }); }
This example shows for example an API that exposes two different versions. The routes for each version are defined in a separate function.
Wildcards
There are two wildcard characters we can use :
and *
. We already met the *
wildcard. We used it in the first couple of endpoint examples.
Both wildcard characters will match route segments. Segments are the pieces of a route that are separated with slashes. :
will match exactly one segment while '*' will match one or more segments.
"/foo/*/baz"
for example will match against "/foo/bar/baz"
or "/foo/bar/qux/baz"
"foo/:/baz" will match "/foo/bar/baz" but not "/foo/bar/qux/baz", the latter has two segments between foo and baz, while :
only matches single segments.
Named wildcards
It is also possible to name wildcards. This allows you to query the specific strings the wildcard matched on. For example "/:bar/*baz"
will match the string "/one/two/three"
. You can then query which wildcards matched which parts of the string. In this case bar
matched one
while baz
matched two/three
. We'll see how you can use this to parse parameters from urls in the next chapter.
Wildcard precedence
When using wildcards it is possible to define multiple different routes that match the same path.
The routes "/some/*"
and "/some/specific/*"
will both match the path "/some/specific/route"
for example. In many web-frameworks the order in which the routes are defined will determine which route will match. Tide will match the most specific route that matches. In the given example the "/some/specific/*"
route will match the path.
Request and Response
In the previous chapter we saw how endpoints are simply functions that take a Request
and return a Response
, or more accurately they return a Result
of something that implements Into<Response>
.
#![allow(unused)] fn main() { async fn endpoint(request: tide::Request) -> tide::Result<impl Into<Response>> }
The Request
struct contains the parsed HTTP request; the URL, HTTP headers, cookies and query string parameters. Additionally the Request
object in Tide is used to pass information about the application state and the request state to the endpoint. We will look into this in the next chapter about how Tide manages State
.
The Response
struct in turn allows us to craft a complete HTTP response. It contains the Response
body, but also a set of HTTP headers and a response code. While the Response
struct can be created, accessed and modified directly, it can be convenient to create a Response
through the Tide ResponseBuilder
.
Request
The Tide Request
struct is the input to your endpoint handler function. It contains all the data from the HTTP request but it is also used by Tide to pass in the application and request State
. We will look at this in more detail in the next chapter. For now it is enough to know that the State
generic type parameter of the Request<State>
type you will see everywhere is the application state.
Request body
The Request
provides a set of methods to access the Request
body. body_string
, body_bytes
and body_json
allow you to read the request body either as a string, as binary data or parse it as json data.
There are a couple of things to keep in mind here. First of all, because a request body can be a sizable piece of data, these methods work asynchronously and can only be called once.
The body_json
is generic over its return type. Json data can be parsed into any type that implements (or derives) serde::Deserialize
.
Accessing Url parameters
In the previous chapter we showed how to use wildcards to match routes. These wildcards can be used to pass parameters into an endpoint as part of the url. To do this you first need to define a route with one or more named wildcards like this;
app.at("/url_params/:some/:parameters").get(url_params);
This route defines the some and parameters wildcards. These can then be retrieved from the Request
using the Request::param
method like this;
async fn url_params(request: Request<()>) -> tide::Result {
Ok(format!(
"Hello, the url contained {} and {}",
request.param("some").unwrap(),
request.param("parameters").unwrap()
)
.into())
}
Query parameters
Another way of passing parameters in an HTTP request is in the query string. The query string follows the url and consists of a set of key and value pairs. A question mark signals the start of the query string. The key-value pairs are separated by &
signs and the keys and values are separated by an equils sign.
http://www.example.com/query?value1=my_value&value2=32
Tide allows you to parse the keys and values of the query string in your URL into a structure, it uses the serde-qs
crate for this. To do this you first need to define a struct to receive the values from the query string;
#[derive(Deserialize)]
struct Query {
pub parameter1: String,
pub parameter2: i32,
}
Note that this struct derives serde::Deserialize
to enable parsing query strings into it.
You can then use the query()
method on the request to parse the query string from the URL into this struct like in the following endpoint;
async fn query_params(request: Request<()>) -> tide::Result {
let query: Query = request.query()?;
Ok(format!(
"Hello, the query parameters were {} and {}",
query.parameter1, query.parameter2,
)
.into())
}
One trick you can use when you don't want to define a whole new type to receive your query string values, Or when you don't know what keys will be present in your query string, is to parse your query-string into a HashMap<String, String>
like this;
async fn simple_query(request: Request<()>) -> tide::Result {
let query: HashMap<String, String> = request.query()?;
Ok(format!(
"Hello, the query parameters were {} and {}",
query["parameter1"], query["parameter2"],
)
.into())
}
HTTP headers
Response and the ResponseBuilder
State
Tide allows us to use two types of state. The Server
state is instantiated when the application is started. It can be used to maintain the application state and is available in all Middleware and Endpoints. This is the ideal place to keep database connection pools, application configuration, cached data or session stores. The Server
state is passed to all middleware and endpoint calls, and those might happen on different threads so there are some restrictions to what types can be used, expressed in a couple of trait bounds.
Tide also provides Request
state. As the name implies this state is unique for each Request
and is lost once a request is handled. Why this type of state might be useful will become clear in the next chapter about Middleware
Request
state is available as a type-map on the Request
struct. Variables can be stored in the Request
and can be retrieved by their type.
Multiple pieces of Request
state can be stored in a request, as long as they have different types. The application state is always one instance of a type. Of course this type can have many fields to store as much application state as you need.
Server State
Until now endpoints were simple stateless functions that process requests into a responses. But for any serious application we need to be able to maintain state somewhere. In a real life application we need to have a place to store things like sessions, database connection pools, configuration etc. And we would rather not use global variables for this.
Tide gives us Server state to do just this. If you look at the definition of the Server struct you see that it has one generic type parameter called State
. We've been using Server::new
to construct our Server
instances, which returns a server without state; Server<()>
. But the Server
type also has a Server::with_state
constructor where you can pass in our own state instance when creating the Server. This State will then be passed to all endpoint handlers through the Request
parameter.
Setup state for an application
To set up our application state we first need to have a type to store the application data that will be shared between requests.
#[derive(Clone)]
struct AppState {
pub datastore: Arc<AtomicU32>,
}
In this example we will share a simple counter. We use an Arc
To set the state in the tide::server
we need to use a different constructor than the server::new()
we used previously. We can use server::with_state(...)
to set up a server with state.
let mut app = tide::with_state(AppState {
datastore: Arc::new(AtomicU32::new(0)),
});
Accessing state
The state can then be accessed using the state
method on your Request
inside your endpoints;
async fn read_state(request: Request<AppState>) -> tide::Result {
Ok(format!(
"datastore has been updated {} times",
request.state().datastore.load(Ordering::SeqCst)
)
.into())
}
async fn update_state(request: Request<AppState>) -> tide::Result {
request.state().datastore.fetch_add(1, Ordering::SeqCst);
Ok("datastore updated".into())
}
Server state limitations
As you can see in the previous example the State
object does have some limitations. First of all Tide sets some trait bounds. The State
needs to be Clone
, Send
and Sync
. This is because it will be passed into all your endpoints, these might be running concurrently on different threads than where you created the State
When accessing State
in your endpoints it's only available as a non-mutable reference.
To get around these limitations for our counter we used an AtomicU32 that provides the internal mutability for our counter and we wrapped it in an Arc
to be able to copy it around.
In practice this is often not that much of a problem. Many database-access libraries for example already provide connection pools components that are written to be able to be passed around inside an application like this.
Further reading
These articles from the Tide team give a nice peek behind the curtains into the motivations and ideas behind the framework;