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"
}