Hello Rust Web with Axum

Using the knowledge we have gained so far on closures and async programming, we will create a small web server using the axum framework. There are other frameworks around, but axum has proven to fit our use cases. It supports HTTP/1, HTTP/2 and web sockets. For most use cases, it is extremely straight-forward to use.

Let's get started!

Include the Axum framework in Cargo.toml:

[dependencies]
tokio = { version = "1", features = ["full"] }
axum = "0.7"

Let's create a simple web server that greets the visitor. We'll use the Router to define our routes. The Router is a collection of routes that can be used to match incoming requests. We'll use the get method to define a route that matches GET requests to the /hello/ path. This route will call the greet_visitor function. We expect a 'visitor' path parameter, which we'll extract using the extract::Path extractor.

use axum::{extract::Path, Router, routing::get, serve};
use tokio::net::TcpListener;

#[tokio::main]
async fn main() {
    // set up our application with "hello world" route at "/
    let app = Router::new().route("/hello/:visitor", get(greet_visitor));

    // start the server on port 3000
    let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
    serve(listener, app).await.unwrap();
}

/// Extract the `visitor` path parameter and use it to greet the visitor.
async fn greet_visitor(Path(visitor): Path<String>) -> String {
    format!("Hello, {visitor}!")
}

Build and run this code on your local machine, then open http://localhost:3000/hello/world in your browser.

It does not get much simpler than this!

Currently, our route responds to GET requests. We can also respond to POST, PUT, DELETE, and other HTTP methods.

use axum::{
    extract::Path,
    Router,
    routing::{delete, get},
    serve,
};
use tokio::net::TcpListener;

#[tokio::main]
async fn main() {
    // set up our application with "hello world" route at "/
    let app = Router::new()
        .route("/hello/:visitor", get(greet_visitor))
        .route("/bye", delete(say_goodbye));

    // start the server on port 3000
    let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
    serve(listener, app).await.unwrap();
}

/// Extract the `visitor` path parameter and use it to greet the visitor.
async fn greet_visitor(Path(visitor): Path<String>) -> String {
    format!("Hello, {visitor}!")
}

/// Say goodbye to the visitor.
async fn say_goodbye() -> String {
    "Goodbye".to_string()
}

As you can see in the example above, we added a new route that responds to DELETE requests. You can append routes to the Router using the route method. The delete method is used to define a route that matches DELETE requests to the /bye path. This route will call the say_goodbye function.

You can test these requests with curl from the command line:

  • GET: curl http://localhost:3000/hello/world
  • DELETE: curl -X DELETE http://localhost:3000/bye

Dealing with JSON

Our example returns a plain text string, which is not very useful. A JSON response might be better suited for our web server. Serde is the de facto standard when dealing with marshalling and unmarshalling JSON in Rust. Let's include that crate in the Cargo.toml.

[dependencies]
tokio = { version = "1", features = ["full"] }
axum = "0.7"
serde = { version = "1", features = ["derive"] }

We'll include a Greeting struct and let Rust derive the Serialize and Deserialize traits from the serde crate. For convenience, we also include a new() function to easily create a Greeting.

use axum::{
    extract::Path,
    Json,
    Router, routing::{delete, get}, serve,
};
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;

#[derive(Serialize, Deserialize)]
struct Greeting {
    greeting: String,
    visitor: String,
}

impl Greeting {
    fn new(greeting: &str, visitor: String) -> Self {
        Greeting {
            greeting: greeting.to_string(),
            visitor,
        }
    }
}

#[tokio::main]
async fn main() {
    // set up our application with "hello world" route at "/
    let app = Router::new()
        .route("/hello/:visitor", get(greet_visitor))
        .route("/bye", delete(say_goodbye));

    // start the server on port 3000
    let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
    serve(listener, app).await.unwrap();
}

/// Extract the `visitor` path parameter and use it to greet the visitor.
/// We use `Json` to automatically serialize the `Greeting` struct to JSON.
async fn greet_visitor(Path(visitor): Path<String>) -> Json<Greeting> {
    Json(Greeting::new("Hello", visitor))
}

/// Say goodbye to the visitor.
async fn say_goodbye() -> String {
    "Goodbye".to_string()
}

GET result:

{
  "greeting": "Hello",
  "visitor": "world"
}

Reference material