Introduction

This tutorial will explain how to build web applications with axum fast while teaching basic concepts along the way. Props to David Pedersen for developing this great framework.

If you are new to rust, you should read the book before continuing.

Lets get started!

Hello World

A simple hello world application looks like this:

Cargo.toml

[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

[dependencies]
# "?" is shorter than ".unwrap()"
anyhow = "1"
# full features because of laziness
tokio = { version = "1", features = ["full"] }
# import the greatest framework
axum = "0.7"

main.rs

use axum::{routing::get, Router};
use std::net::SocketAddr;
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // pass incoming GET requests on "/hello-world" to "hello_world" handler.
    let app = Router::new().route("/hello-world", get(hello_world));

    // write address like this to not make typos
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = TcpListener::bind(addr).await?;

    axum::serve(listener, app.into_make_service()).await?;

    Ok(())
}

async fn hello_world() -> &'static str {
    "Hello, world!"
}

If we enter "http://localhost:3000/hello-world" to browser, we can see "Hello, world!" text coming up.

What is going on in this example?

#![allow(unused)]
fn main() {
let app = Router::new().route("/hello-world", get(hello_world));
}

This part is easy. Does the request have "/hello-world" in its URL? Okay route it to method router (in this case "get"). Does the request have "GET" method? Okay route it to "hello_world" handler.

NOTE: You can add more routes by calling .route again on Router.

Lets look at hello_world function which we will now refer as hello_world handler.

#![allow(unused)]
fn main() {
async fn hello_world() -> &'static str {
    "Hello, world!"
}
}

This function can be used as a handler because it is async and its return type implements IntoResponse trait. We will look into IntoResponse trait in detail later. For now lets use this powerful knowledge to add a functionality to our app.

It would be cool if we could redirect all URLs coming to our application to hello_world right?

By randomly traveling in axum docs (which is how you will operate from now on) I found us Router::fallback method and Redirect type we can use.

#![allow(unused)]
fn main() {
use axum::{response::Redirect, routing::get, Router};
//          ^ import this ^

let app = Router::new()
    .route("/hello-world", get(hello_world))
    .fallback(anything_else); // <-- add that fallback

// create an easy handler
async fn anything_else() -> Redirect {
    Redirect::to("/hello-world")
}
}

Do these changes, run the app then go to "http://localhost:3000/plz_no_404" and boom! It redirects us to "/hello-world".

Extractors

Axum provides extractors to get/extract data about requests to our handlers. To qualify as a handler, parameters of a function must implement FromRequestParts last parameter can implement FromRequest (meaning it also implements FromRequestParts). There are two traits because some parts of request for example body can only be extracted once (can be cloned later).

In previous hello world example we didn't need any info about requests for our app to run. I was thinking, "Okay, I need to build some random app to explain readers how extractors work." What did I do? Of course I found something by randomly traveling in axum docs.

Every developer needs a way to get their clients ip address and sell it to big corporations. But of course as ethical developers, we will not do that. Instead we will explain the client we got their ip address and they can possibly be identified by harmful parties and to prevent that they need to buy our VPN service.

Here is a basic example of our app:

use axum::{extract::ConnectInfo, routing::get, Router};
use std::net::SocketAddr;
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let app = Router::new().route("/", get(index));

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = TcpListener::bind(addr).await?;

    axum::serve(
        listener,
        app.into_make_service_with_connect_info::<SocketAddr>(),
    )
    .await?;

    Ok(())
}

async fn index(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String {
    format!(
        "Your ip address is \"{addr}\".\n\
         You are in immediate danger of getting identified by bad people.\n\
         Thankfully we have a VPN service to hide your ip. \n\
         Visit this link to download it \"http://localhost:3000/average_joe_absolutely_needs_vpn\""
    )
}

To be able to get ip address with ConnectInfo extractor we need this boilerplate code:

#![allow(unused)]
fn main() {
// copypasta for future projects
axum::serve(
    listener,
    app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await?;
}

Lets take a look at function signature:

#![allow(unused)]
fn main() {
async fn index(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String
}

You might not be familiar of pattern matching done for function parameter "addr" here. Lets explain shortly.

ConnectInfo is a struct. It is defined something like struct ConnectInfo<T>(T);. It has paranthesis instead of braces and can take any type like a tuple with single item. (example let something = ConnectInfo(String);) By using tuple structs in function parameters you can match inner item. If we used addr: ConnectInfo<SocketAddr> instead we would have to do let addr = addr.0; in the code below to achieve the same thing.

To simplify, for now lets talk about it using the other syntax.

#![allow(unused)]
fn main() {
async fn index(addr: ConnectInfo<SocketAddr>) -> String
}

addr parameter type needs to implement FromRequest or FromRequestParts for this function to qualify as a handler. ConnectInfo implements it if we use it like how it is documented.

We are dynamically creating a string to add ip address inside. So we need to use String type which implements IntoResponse.

In the handler code, its basically just format! macro to create a string so nothing new there.

Our app works fine but its just plaintext. Lets improve it by doing some html.

#![allow(unused)]
fn main() {
// change return type to `Html<String>` to let browser know we are sending html
async fn index(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> Html<String> {
    let html = format!(
        "<h1>Your ip address is: \"{addr}\"</h1>\n\
         <h2>You are in immediate danger of getting identified by bad people.</h2>\n\
         <h2>Thankfully we have a VPN service to hide your ip.</h2>\n\
         <h2>Visit <a href=\"http://localhost:3000/average_joe_absolutely_needs_vpn\">THIS</a> link to download it.</h2>"
    );

    // create `Html` type like this
    Html(html)
}
}

Challenge

Create a /average_joe_absolutely_needs_vpn page returning html to convince the poor guy client to buy our VPN service.

Contributors

  • Eray Karatay (@programatik29)