Coding Babble  Code it once more, with feeling.

How to Create an Axum Server

By Luke Simpson

Introduction

In this post we’ll learn how to use Axum to serve a static website.

Creating a Basic Server

  1. In your preferred directory, enter:

    cargo new axum-server
    
  2. Open the generated directory and add axum:

    cargo add axum
    

    This will create an entry under your dependencies in cargo.toml.

  3. Add tokio:

    cargo add tokio --features rt-multi-thread
    

    rt-multi-thread allows us to use the #[tokio::main] macro for an async main function. You could also do --features full, but I prefer to add things in as I need them.

  4. Now we are ready to add our server code with a simple test route to main.rs:

    use std::error::Error;
    use std::net::SocketAddr;
    
    use axum::routing::get;
    use axum::Router;
    
    async fn test() -> &'static str {
        "Hello, World!"
    }
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn Error>> {
        let app = Router::new().route("/api/test", get(test));
        let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
        println!("Listening on: {}", addr);
        let listener = tokio::net::TcpListener::bind(addr).await?;
        axum::serve(listener, app).await?;
        Ok(())
    }
    

    Notice that we tied a specific route, /api/test, to a specific function, test. We also specified this to be a GET request handler.

  5. Run the server with cargo run. You should see Listening on: 127.0.0.1:8080 in the terminal.

  6. Visit localhost:8080/api/test to verify that you see the message.

Testing the Handler with curl

To test, we’ll use this command:

curl -X GET -v localhost:8080/api/test

-X lets us specify the method and -v means “verbose” so we can see more info about the request/response. We can see that our handler’s return type, &'static str, was converted to the following:

HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8

If you try to POST to this route using Postman or curl,

curl -X POST -v localhost:8080/api/test

Axum will return HTTP/1.1 405 Method Not Allowed since there is no route to handle a POST request.

Handling a POST Request

To handle a POST request for our /api/test route, just chain it onto the end:

 let app = Router::new().route("/api/test", get(test).post(test));
  1. Build and rerun the server.
  2. Test the POST with curl. The response should look identical to the GET with 13 bytes received.

Returning a JSON Response

Let’s change our test method to return some JSON instead.

  1. We’ll need another crate:
    cargo add serde_json
    
  2. Import the necessary macro and types:
    use axum::Json;
    use serde_json::{json, Value};
    
  3. Change the method to look like the following:
    async fn test() -> Json<Value> {
     Json(json!({ "message": "Hello, World!" }))
    }
    
  4. Rebuild and run.

If we now test our route with curl, we can see that our response is:

HTTP/1.1 200 OK
content-type: application/json
content-length: 27
{ "message": "Hello, World!" }

Return a Custom Type as JSON

Any type that derives serde::Serialize can be turned into a JSON response. Let’s try that.

  1. We’ll need the serde crate:

    cargo add serde --features derive
    
  2. Use it:

    use serde::Serialize;
    
  3. Add a new struct. Notice that we derive Serialize for the struct.

    #[derive(Serialize)]
    struct CustomMessage {
     message: String,
    }
    
  4. Modify the test handler:

    async fn test() -> Json<CustomMessage> {
     let msg = CustomMessage {
         message: "Hello, World!".to_string(),
     };
     Json(msg)
    }
    

    Notice we replaced Json<Value> with Json<CustomMessage> for the return type.

  5. Rebuild and run.

The response should be the same as before.

Return HTML

Now let’s return an index page. We could add an index route and use the return type Html<&'static str>, but I’ll show you how to serve a project directory instead. This way we can serve more than just an HTML file. We’ll also be able to serve CSS or SVGs or anything else our site needs.

  1. Create a new folder called site in your project root.

  2. Create a new file called index.html in the site folder with the following content:

    <!DOCTYPE html>
     <html lang="en">
     <head>
       <meta charset="utf-8" />
       <meta name="viewport" content="width=device-width, initial-scale=1" />
    
       <title>Hello From Axum</title>
     </head>
    
      <body>
        <h1>It's almost too easy.</h1>
      </body>
    </html>
    
  3. We’re now going to add tower-http and add the fs feature so it can access the file system.

    cargo add tower-http --features fs
    
  4. Add new uses that we’ll need:

    use axum::routing::get_service;
    use tower_http::services::ServeDir;
    
  5. Modify our router to serve the directory:

     let app = Router::new()
         .nest_service("/", get_service(ServeDir::new("./site")))
         .route("/api/test", get(test).post(test));
    
  6. Rebuild and run.

Now, if you go to localhost:8080, you’ll see your HTML page. Shweet.

Favicon

If you opened up dev tools and checked the network tab, you might have noticed the browser is looking for a favicon.ico, but can’t find one. That’s upsetting. However, adding one is easy since all we have to do is add it to our site directory.

Exercise

Try adding a CSS file and linking it to your HTML. You can also view the complete code for the project.

Conclusion

We just scratched the surface of what Axum can do. There are many more examples you can browse in their GitHub repo. We might cover some more of those in the future. Until next time!

Support the blog! Buy a t-shirt or a mug!

Tags: