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-thread
allows us to use the#[tokio::main]
macro for anasync
main 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 seeListening on: 127.0.0.1:8080
in 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
serde
crate:cargo add serde --features derive
Use it:
use serde::Serialize;
Add a new struct. Notice that we derive
Serialize
for the struct.#[derive(Serialize)] struct CustomMessage { message: String, }
Modify the
test
handler:async fn test() -> Json<CustomMessage> { let msg = CustomMessage { message: "Hello, World!".to_string(), }; Json(msg) }
Notice we replaced
Json<Value>
withJson<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
site
in your project root.Create a new file called
index.html
in thesite
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>
We’re now going to add
tower-http
and add thefs
feature so it can access the file system.cargo add tower-http --features fs
Add new
use
s 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!