Axum Server

Install required dependencies

cargo add axum
cargo add hyper --features=full
cargo add tokio --features=full
cargo add toker

Lets create a new function that will return the axum::routing::Router. This will be the core of our server.

// src/main.rs
fn app() -> Router {
    Router::new().route("/", get(|| async { "Hello, World!" }))
}

Creating a function that returns the Router will help us later to test our application.

Add the tokio config to the main function.

// src/main.rs
#[tokio::main]
async fn main() {
// ...

Create the socket address we will listen on.

// src/main.rs fn main()
let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8080);

And bind the address to our application.

// src/main.rs fn main()
axum::Server::bind(&address)
    .serve(app().into_make_service())
    .await
    .unwrap();

Lets test our server out, in one terminal lets start the server and in another we will use curl to call the endpoint we just created.

# one terminal
cargo run
# the other terminal
curl 127.0.0.1:8080 # Hello, world!

Lets add an automatic test for the / route. Lets start by adding the test that will create our application.

// src/main.rs
#[cfg(test)]
mod test {
    use super::app;

    #[tokio::test]
    async fn hello_world() {
        let app = app();
    }
}

The #[tokio::test] will let us create and async test. Lets make the request for the endpoint inside the test.

// src/main.rs mod test
// dependencies for the response
use axum::{body::Body, http::Request};
use tower::ServiceExt; // for oneshot

#[tokio::test]
async fn hello_world() {
    let app = app();

    let response = app
        .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
        .await
        .unwrap();
// ...

Finally lets add the assertions to the test.

// src/main.rs mod test fn hello_world()
let response = ...;

assert_eq!(response.status(), StatusCode::OK);

let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
assert_eq!(&body[..], b"Hello, World!");

Now we have a working and tested web server. Lets add some more useful utilities.

The first is color-eyre to have a better panic handler.

cargo add color_eyre

Then, we have to initialize the panic handler.

// src/main.rs
use color_eyre::Result;

#[tokio::main]
async fn main() -> Result<()> {
    color_eyre::install()?;

    // ...

Now we can use the ? in the main function instead of unwrap for errors. You can try to run two instances of the program at the same time to see an example of the eyre errors.

Finally, we want to log what our server is doing. To do that we will use the tracing crate, which is already integrated with axum and eyre.

So lets install the tracing dependencies.

cargo add tracing
cargo add tracing_subscriber --features=env-filter
cargo add tower_http --features=trace

Then, in our main.rs after the eyre install we have to add the tracing subscriber.

// src/main.rs fn main()
tracing_subscriber::registry()
    .with(
        tracing_subscriber::EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| "example_testing=debug,tower_http=debug".into()),
    )
    .with(tracing_subscriber::fmt::layer())
    .init();

Where example_testing is the name of our application. This will try to parse the env filter or use debug as a default for either our application and tower.

Next we add the tracing layer to the tower stack:

// src/main.rs
fn app() -> Router {
    Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .layer(TraceLayer::new_for_http())
}

Finally we can start tracing, lets add a debug event for the server address.

// src/main.rs fn main()
let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8080);

tracing::debug!("listing on address {}", address);