An introduction to web backends in Rust - Part 1

This is the first 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.

Rust is a system language mainly developped at Mozilla, it's designed to limit errors leading to security issues and it aims to be as fast as possible while maintaining a good devXP.

As a system programming language you might think that it's reserved for low level stuff (and it's pretty good at it) but it's ergonomics make it a pretty good choice for higher level stuff like a webserver.

I won't go down the web frameworks comparison route, it has already been done and it's mainly a matter of tastes, but if you want one here you go :)

Today we will use Axum, it's a good mix between being in control of the stack and still having nice utilities built in. It doesn't ship an ORM or an auth system, you can think of it as an Express like if you are coming from nodeJS, or Sinatra for my fellow Rubyists.

Installation and setup

The golden path for installing rust is using rustup, I invite you to follow the instruction on the rustup page. You're mostly one command away of having the full rust toolchain installed on your machine.

Rust is a compiled language, you can invoke the Rust compiler and check the version with this command:

$ rustc -V

If it displays something like (depending on your Rust version):

rustc 1.63.0 (4b91a6ea7 2022-08-08)

Then you are good to go.

Cargo is the package manager and sort of task runner bundled with rust. It's a command line utility and you can initialize new projects with it.

We will name our webserver webby, but you can name it as you prefer:

cargo new webby && cd ./webby

You are now inside the webby directory which contains a few files generated by cargo.

.
├── Cargo.toml
└── src
    └── main.rs

Cargo.toml is a declarative file containing metadatas and where you can list dependencies. Mine is looking like this :

[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]

src/main.rs is a basic Hello world displayed in the console.

fn main() {
    println!("Hello, world!");
}

You can run it using the command:

cargo run

It will compile and run it. The compiler will output a dev build with the command:

cargo build

If you want a production build you can use the command:

cargo build --release

Which will output the production build inside the ./target/release folder, you can run it by invoking the binary directly:

./target/release/webby

This is the one that you would deploy on your server. Note that it is linked to your OS and architecture. If you compile it on a Mac m1 using OSX, you won't be able to run it on a x86 linux machine, you will have to recompile it.

Now that we are set up, let's go.

Installing dependencies

I won't go too much into the details of what consitutes a web server, but the big picture is that it accepts requests, forms a response and send it back. Some responses are a bit long to construct, imagine you're processing some data and communicating with a database, the response is taking 1 second to form. What happens if 1000's of users are hitting the webserver at the same time?

The response time adds up and for some users the response can take 1000 seconds to come back, because the server is treating the requests one at a time.

We could spawn threads each time a request is received, but it's generally more efficient and accepted to use an async model. If you're coming from NodeJS you can think of it as the event loop.

The most common way to implement this at the moment is by using a library called Tokio.

Cargo is Rust's package manager and https://crates.io/ the repository where you can browse community packages, it is used by Cargo to download them. Here is the tokio's page

To add tokio to your project you can run:

cargo add tokio

Let's modify the Cargo.toml file so it looks like this:

[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]
tokio = { version = "1.21.2", features = ["full"] }

Here the last line indicates that we want to load the full tokio runtime, it's a modular library and we can pick what we want from it. We want everything.

We can modify ou ./src/main.rs to load the Tokio runtime, we annonate our main function with #[tokio::main], to simplify (Tokio is a hell of a beast) it allows us to use the async/await syntax. Our main() function can be prefixed by async.

#[tokio::main]
async fn main() {
    println!("Hello, world!");
}

Run it with :

cargo run

Nothing changed, our hello world is still functionnal, it is enhanced with the Tokio runtime and offers us many new possibilities via async functionnalities.

Our next dependency is Axum, add it with:

cargo add axum

This is our web library, it includes a router, a web server and lots of utilities like middlewares. It is backed by the Tokio runtime so you don't have to manage threads or an event loop manually.

Let's do an Hello world as a service, you go on the /hello url and it greets you. How nice is this?

Fist in our main.rs file, we will create a greet() function:

#[tokio::main]
async fn main() {}

async fn greet() -> &'static str {
    "Hello you!"
}

greet() is a basic function returning a static string. This will be the handler for our /hello route.

Let's build the axum server:

use std::net::SocketAddr;

use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/hello", get(greet));

    let addr = SocketAddr::from(([127, 0, 0, 1], 4000));

    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn greet() -> &'static str {
    "Hello you!"
}

First run it with :

cargo run

and then visit the url localhost:4000, you should see the text "Hello you!". Nice, we have a useless but functionnal server written in Rust.

The lines at the top are used to import the libraries or modules. The keyword use allows us to use functions, struct, types... defined and exposed in some other files or libraries.

use axum::{routing::get, Router};
 // is a shortcut for
use axum::routing::get;
use axum::Router;

You can now use get and Router in your code. Yay!

std::net::SocketAddr std stands for standard, Rust has an extensive standard library which we need to import in order to use. Here we want to use the SocketAddr enum included in the network category of the standard library.

We start with this line :

let app = Router::new().route("/hello", get(greet));

The Module::function syntax is a way to call functions from a module, you can see it as namespacing. Here we want a new Router on which we will attach our actual routes.

Let's start with one, /hello which will be accessible by sending a GET request and which will respond with our greet() function as a handler.

let addr = SocketAddr::from(([127, 0, 0, 1], 4000));

This line let us assign the local adress 127.0.0.1:4000 or localhost:4000 (both are the same usually) to the variable addr.

Then we bind the local address to the axum constructor and serve our router as the main Axum service :

axum::Server::bind(&addr)
    .serve(app.into_make_service())
    .await
    .unwrap();

You can see the keyword await, as we are in an async context, Axum is built with Tokio in mind and we await the server initialization.

This is a simple webserver but that was a lot to cover. Congratulation, you now have a functionnal webserver written in Rust.

In the next article we will cover JSON serialization and responses and how to handle query parameters. Stay tuned.

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