How to Create an Axum Server
Introduction
In this post we’ll learn how to use Axum to serve a static website.
Creating a Basic Server
- In your preferred directory, enter: - cargo new axum-server
- Open the generated directory and add axum: - cargo add axum- This will create an entry under your dependencies in - cargo.toml.
- Add tokio: - cargo add tokio --features rt-multi-thread- rt-multi-threadallows us to use the- #[tokio::main]macro for an- asyncmain function. You could also do- --features full, but I prefer to add things in as I need them.
- 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.
- Run the server with - cargo run. You should see- Listening on: 127.0.0.1:8080in the terminal.
- 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));
- Build and rerun the server.
- 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.
- We’ll need another crate:cargo add serde_json
- Import the necessary macro and types:use axum::Json; use serde_json::{json, Value};
- Change the method to look like the following:async fn test() -> Json<Value> { Json(json!({ "message": "Hello, World!" })) }
- 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.
- We’ll need the - serdecrate:- cargo add serde --features derive
- Use it: - use serde::Serialize;
- Add a new struct. Notice that we derive - Serializefor the struct.- #[derive(Serialize)] struct CustomMessage { message: String, }
- Modify the - testhandler:- 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.
- 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.
- Create a new folder called - sitein your project root.
- Create a new file called - index.htmlin the- sitefolder 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>
- We’re now going to add - tower-httpand add the- fsfeature so it can access the file system.- cargo add tower-http --features fs
- Add new - uses that we’ll need:- use axum::routing::get_service; use tower_http::services::ServeDir;
- 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));
- 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! Or just buy me a beverage🧋. Thanks!
Check out my other projects:
- Emerald Geography - Learn geography on a 3D globe.
- Word Rummage - Random word generator, word finder, and dictionary.
- Color Changer web extension for Firefox and Chrome - Change colors of websites to make them easier to read.