An introduction to web backends in Rust - Part 2

This is the second article of the An introduction to web backends in Rust collection, where we implement a full backend server in Rust, including database communication, API handling and HTML templating.

You can read the first part here

Serde

Serde is a crate allowing us to derialize and deserialize data (SERialize, DEserialize, see what they did there?). It supports JSON, YAML and a lot of other stuffs. It is mostly straightforward to use and we will need it to handle JSON bodies and is used by Axum to format JSON responses.

Let's add it to our dependencies in cargo.toml

[package]
name = "webby"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
axum = "0.5.17"
serde = { version = "1.0.147", features = ["derive"] }
tokio = { version = "1.21.2", features = ["full"] }

Serde mainly works by using attributes on structs. Structs are a way to define data structures in Rust, we can initialize them. As Rust is not Object Oriented it is usually simpler to initialize structs.

Let's see what strutcs look like:

struct User {
    first_name: String,
    age: u8
}

let user = User { first_name: String::from("Michaël"), age: 36 }

We name it and define fields, we can then instanciate our struct. The order of the fields does not matter.

In order to be able to serialize and deserialize our User struct from and to diffrent format we can add a Serde attribute to it :

#[derive(Deserialize, Serialize)]
struct User {
    first_name: String,
    age: u8
}

Here we are using a container attribute, it will allow Serde to handle the struct in its entirety, but we will get back to Serde later.

URL parameters

Our webserver is functionnal and simple, but what if we want to handle URL parameters?

First let's organize our handlers and routes a little bit. Let's define a route function that will wrap the Router::new function from Axum.

fn route(path: &str, method_router: MethodRouter<Body>) -> Router {
    Router::new().route(path, method_router)
}

Nothing fancy, but it will allow us to group routes and handler.

Let's use it with our greet() route:

fn greet() -> Router {
    async fn handler() -> &'static str {
        "Hello you!"
    }
    route("/hello", get(handler))
}

Let's update our app by using merge on the main router:

let app = Router::new()
        .merge(greet());

Let's move on our route with parameters. We will define a struct that will hold our query parameters:

#[derive(Deserialize)]
struct GreetParameter {
    name: String,
}

We are using Deserialize as we will just have to parse it, not output it.

and the route implementation:

fn greet_enhanced() -> Router {
    async fn handler(Query(params): Query<GreetParameter>) -> Html<String> {
        Html(format!("<h1>Hello {}</h1>", params.name))
    }
    route("/greet", get(handler))
}

Query is provided by Axum and we tell our handler that the params variable will be instantiated with our struct GreetParameter.

You can also see that we are outputing Html, here again provided by Axum.

Merge it in our main Router:

let app = Router::new()
        .merge(greet())
        .merge(greet_enhanced());

And try it with

cargo run

and visit localhost:4000/greet?name=michael

JSON and post handling

In order to handle a JSON body and to respond with JSON, we will need two structs, one for the body payload on the post request, and another to let Serde deserialize ou struct response to JSON:


#[derive(Deserialize)]
struct CreateGreeting {
    name: String,
}
#[derive(Deserialize, Serialize)]
struct Greeting {
    name: String,
    hello: String,
}

And here is our route implementation:

fn greet_json() -> Router {
    async fn handler(extract::Json(payload): extract::Json<CreateGreeting>) -> Json<Greeting> {
        let greeting = Greeting {
            name: payload.name,
            hello: "world".to_string(),
        };
        Json(greeting)
    }
    route("/greet_json", post(handler))
}

The type system is doing its magic in combination with Serde. Let's update our routes:

 let app = Router::new()
        .merge(greet())
        .merge(greet_enhanced())
        .merge(greet_json());

We have seen how to organize our routes a little better and how to handle query parameters, Post requests with JSON bodies and how to output HTML and/or JSON.

In the next article we will cover Databases and how to split our server in multiple files. Stay tuned.

You can find the code presented in this article on Github.