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 Routes to Endpoints. When a Server is started it will handle incoming Requests 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 Endpoints

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 make sure we can safely share this even when simultanious requests come in.

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. point at an sqlx examle

Further reading

These articles from the Tide team give a nice peek behind the curtains into the motivations and ideas behind the framework;