Demo Rust Axum by Joelparkerhenderson
Demo Rust Axum by Joelparkerhenderson
1
Contents
Demo of Rust and axum web framework 4
What is this? . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
What is axum? . . . . . . . . . . . . . . . . . . . . . . . . . . 7
What is tower? . . . . . . . . . . . . . . . . . . . . . . . . . . 9
What is hyper? . . . . . . . . . . . . . . . . . . . . . . . . . . 11
What is tokio? . . . . . . . . . . . . . . . . . . . . . . . . . . 13
What is Serde? . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Hello, World! 17
Create a handler function . . . . . . . . . . . . . . . . . . . . 19
Create a router fallback . . . . . . . . . . . . . . . . . . . . . 20
Graceful shutdown . . . . . . . . . . . . . . . . . . . . . . . . 21
The whole code . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Extractors 33
Extract path parameters . . . . . . . . . . . . . . . . . . . . . 34
Extract query parameters . . . . . . . . . . . . . . . . . . . . 35
Respond with a JSON payload . . . . . . . . . . . . . . . . . . 37
Extract a JSON payload . . . . . . . . . . . . . . . . . . . . . . 39
2
Put one book . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Get one book as a web form . . . . . . . . . . . . . . . . . . . 52
Post one book as a web form . . . . . . . . . . . . . . . . . . . 54
Delete one book . . . . . . . . . . . . . . . . . . . . . . . . . 56
Extras 58
Add a Tower tracing subscriber . . . . . . . . . . . . . . . . . 59
Use a host, port, and socket address . . . . . . . . . . . . . . . 61
Conclusion 62
What you learned . . . . . . . . . . . . . . . . . . . . . . . . 63
What’s next . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
axum examples . . . . . . . . . . . . . . . . . . . . . . . . . . 65
3
Demo of Rust and axum web framework
Demonstration of:
Thanks
Thanks to all the above projects and their authors. Donate to them if you
can.
Does this demo help your work? Donate here if you can via GitHub
sponsors.
Feedback
License
Contact
4
What is this?
This demo is a tutorial that teaches how to build features from the
ground up with axum and its ecosystem of tower middleware, hyper
HTTP library, tokio asynchronous platform, and Serde data conversions.
What is required?
5
What is helpful?
6
What is axum?
High level features:
The tower ecosystem is what sets axum apart from other frameworks:
• axum doesn’t have its own middleware system but instead uses
tower::Service.
• axum is combines the speed and security of Rust with the power of
battle‐tested libraries for middleware, asynchronous
programming, and HTTP.
Hello, World!
7
#[tokio::main]
async fn main() {
// Build our application with a single route.
let app = axum::Router::new().route("/",
axum::routing::get(|| async { "Hello, World!" }));
Link to examples/axum‐hello‐world
8
What is tower?
Tower is a library of modular and reusable components for building
robust networking clients and servers.
Service
fn poll_ready(
&mut self,
cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>>;
Call
use tower::{
Service,
ServiceExt,
};
9
// send the request
.call(request).await?;
10
What is hyper?
hyper is a fast HTTP implementation written in and for Rust.
Hyper is low‐level
If you are looking for a convenient HTTP client, then you may wish to
consider reqwest.
If you are looking for a convenient HTTP server, then you may wish to
consider warp.
Hello, World!
use std::convert::Infallible;
async fn handle(
_: hyper::Request<Body>
) -> Result<hyper::Response<hyper::Body>, Infallible> {
Ok(hyper::Response::new("Hello, World!".into()))
}
#[tokio::main]
async fn main() {
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
11
Ok::<_, Infallible>(hyper::service::service_fn(handle))
});
12
What is tokio?
tokio is an asynchronous runtime for the Rust programming language.
#[tokio::main]
async fn main() {
let listener = tokio::net::TcpListener::bind("localhost:3000")
.await
.unwrap();
loop {
let (socket, _address) = listener.accept().await.unwrap();
tokio::spawn(async move {
process(socket).await;
13
});
}
}
#[tokio::main]
async fn main() -> Result<()> {
let mut client = client::connect("localhost:3000").await?;
println!("connected);
Ok(())
}
14
What is Serde?
Serde is a framework for serializing and deserializing Rust data
structures efficiently and generically.
Serde provides the layer by which these two groups interact with each
other, allowing any supported data structure to be serialized and
deserialized using any supported data format.
Design
• Serde provides the Serialize trait and Deserialize trait for data
structures.
Demo of Serde
fn main() {
15
let point = Point { x: 1, y: 2 };
// Print {"x":1,"y":2}
println!("{}", serialized);
// Print Point { x: 1, y: 2 }
println!("{:?}", deserialized);
}
16
Hello, World!
Create a typical new Rust project:
[package]
name = "demo-rust-axum"
version = "1.3.0"
edition = "2021"
[dependencies]
# A serialization/deserialization framework.
serde = { version = "~1.0.193", features = ["derive"] }
#[tokio::main]
pub async fn main() {
// Build our application by creating our router.
17
let app = axum::Router::new()
.route("/",
axum::routing::get(|| async { "Hello, World!" })
);
Shell:
cargo run
Browse http://localhost:3000
18
Create a handler function
An axum route can call an axum handler, which is an async function that
returns anything that axum can convert into a response.
Our demos will often use the axum routing get function, so add code to
use it:
use axum::routing::get;
/// axum handler for "GET /" which returns a string and causes axum to
/// immediately respond with status code `200 OK` and with the string.
pub async fn hello() -> String {
"Hello, World!".into()
}
Shell:
cargo run
Browse http://localhost:3000
19
Create a router fallback
For a request that fails to match anything in the router, you can use the
function fallback.
use axum::handler::Handler;
Modify the Router to add the function fallback as the first choice:
/// axum handler for any request that fails to match the router routes.
/// This implementation returns HTTP status code Not Found (404).
pub async fn fallback(
uri: axum::http::Uri
) -> impl axum::response::IntoResponse {
(axum::http::StatusCode::NOT_FOUND, format!("No route {}", uri))
}
Shell:
cargo run
Browse http://localhost:3000/whatever
20
Graceful shutdown
We want our demo server to be able to do graceful shutdown.
21
The whole code
use axum::routing::get;
#[tokio::main]
pub async fn main() {
// Build our application by creating our router.
let app = axum::Router::new()
.fallback(
fallback
)
.route("/",
get(hello)
);
/// axum handler for any request that fails to match the router routes.
/// This implementation returns HTTP status code Not Found (404).
pub async fn fallback(
uri: axum::http::Uri
) -> impl axum::response::IntoResponse {
(axum::http::StatusCode::NOT_FOUND, format!("No route {}", uri))
}
/// axum handler for "GET /" which returns a string and causes axum to
/// immediately respond with status code `200 OK` and with the string.
pub async fn hello() -> String {
"Hello, World!".to_string()
}
22
Create axum routes and axum handlers
This section shows how to:
23
Respond with HTML text
Edit file main.rs.
Add a route:
.route("/demo.html",
get(get_demo_html)
);
Add a handler:
/// axum handler for "GET /demo.html" which responds with HTML text.
/// The `Html` type sets an HTTP header content-type of `text/html`.
pub async fn get_demo_html() -> axum::response::Html<&'static str> {
"<h1>Hello</h1>".into()
}
Shell:
cargo run
Browse http://localhost:3000/demo.html
24
Respond with an HTML file
Create file hello.html.
Add this:
<h1>Hello</h1>
This is our demo.
Add route:
.route("/hello.html",
get(hello_html)
)
Add handler:
/// axum handler that responds with typical HTML coming from a file.
/// This uses the Rust macro `std::include_str` to include a UTF-8 file
/// path, relative to `main.rs`, as a `&'static str` at compile time.
async fn hello_html() -> axum::response::Html<&'static str> {
include_str!("hello.html").into()
}
Shell:
cargo run
Browse http://localhost:3000/hello.html
You should see the headline “Hello” and text “This is our demo.”.
25
Respond with HTTP status code OK
Edit file main.rs.
Add a route:
.route("/demo-status",
get(demo_status)
);
Add a handler:
/// axum handler for "GET /demo-status" which returns a HTTP status
/// code, such as OK (200), and a custom user-visible string message.
pub async fn demo_status() -> (axum::http::StatusCode, String) {
(axum::http::StatusCode::OK, "Everything is OK".to_string())
}
Shell:
cargo run
Browse http://localhost:3000/demo‐status
26
Respond with the request URI
Edit file main.rs.
Add a route:
.route("/demo-uri",
get(demo_uri)
);
Add a handler:
/// axum handler for "GET /demo-uri" which shows the request's own URI.
/// This shows how to write a handler that receives the URI.
pub async fn demo_uri(uri: axum::http::Uri) -> String {
format!("The URI is: {:?}", uri)
}
Shell:
cargo run
Browse http://localhost:3000/demo‐uri
27
Respond with a custom header and
image
Edit file Cargo.toml.
Add dependencies:
Add a route:
.route("/demo.png",
get(get_demo_png)
)
Add a handler:
/// axum handler for "GET /demo.png" which responds with an image PNG.
/// This sets a header "image/png" then sends the decoded image data.
async fn get_demo_png() -> impl axum::response::IntoResponse {
use base64::Engine;
let png = concat!(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB",
"CAYAAAAfFcSJAAAADUlEQVR42mPk+89Q",
"DwADvgGOSHzRgAAAAABJRU5ErkJggg=="
);
(
axum::response::AppendHeaders([
(axum::http::header::CONTENT_TYPE, "image/png"),
]),
base64::engine::general_purpose::STANDARD.decode(png).unwrap(),
)
}
28
Try the demo…
Shell:
cargo run
Browse http://localhost:3000/demo.png
29
Respond to multiple HTTP verbs
axum routes can use HTTP verbs, including GET, PUT, PATCH, POST,
DELETE.
.route("/foo",
get(get_foo)
.put(put_foo)
.patch(patch_foo)
.post(post_foo)
.delete(delete_foo),
)
/// axum handler for "GET /foo" which returns a string message.
/// This shows our naming convention for HTTP GET handlers.
pub async fn get_foo() -> String {
"GET foo".to_string()
}
/// axum handler for "PUT /foo" which returns a string message.
/// This shows our naming convention for HTTP PUT handlers.
pub async fn put_foo() -> String {
"PUT foo".to_string()
}
/// axum handler for "PATCH /foo" which returns a string message.
/// This shows our naming convention for HTTP PATCH handlers.
pub async fn patch_foo() -> String {
"PATCH foo".to_string()
}
/// axum handler for "POST /foo" which returns a string message.
/// This shows our naming convention for HTTP POST handlers.
pub async fn post_foo() -> String {
30
"POST foo".to_string()
}
/// axum handler for "DELETE /foo" which returns a string message.
/// This shows our naming convention for HTTP DELETE handlers.
pub async fn delete_foo() -> String {
"DELETE foo".to_string()
}
Shell:
cargo run
Shell:
Output:
GET foo
Shell:
Output:
PUT foo
Shell:
Output:
31
PATCH foo
Shell:
Output:
POST foo
Shell:
Output:
DELETE foo
The command curl uses GET by default, i.e. these are equivalent:
curl 'http://localhost:3000/foo'
32
Extractors
An axum “extractor” is how you pick apart the incoming request in order
to get any parts that your handler needs.
33
Extract path parameters
Add a route using path parameter syntax, such as “:id”, in order to tell
axum to extract a path parameter and deserialize it into a variable
named id.
Add a route:
.route("/items/:id",
get(get_items_id)
);
Add a handler:
Shell:
cargo run
Shell:
curl 'http://localhost:3000/items/1'
Ouput:
34
Extract query parameters
Edit file main.rs.
use std::collections::HashMap;
Add a route:
.route("/items",
get(get_items)
);
Add a handler:
Shell:
cargo run
Shell:
curl 'http://localhost:3000/items?a=b'
Output:
35
Get items with query params: {"a": "b"}
36
Respond with a JSON payload
The axum extractor for JSON can help with a response, by formatting
JSON data then setting the response application content type.
Add a route:
.route("/demo.json",
get(get_demo_json)
);
Add a handler:
Shell:
cargo run
To request JSON with curl, set a custom HTTP header like this:
curl \
--header "Accept: application/json" \
37
--request GET 'http://localhost:3000/demo.json'
Output:
{"a":"b"}
38
Extract a JSON payload
The axum extractor for JSON deserializes a request body into any type
that implements serde::Deserialize. If the extractor is unable to parse
the request body, or if the request is missing the header
Content-Type: application/json, then the extractor returns HTTP
BAD_REQUEST (404).
.route("/demo.json",
get(get_demo_json)
.put(put_demo_json)
)
Add a handler:
Shell:
cargo run
curl \
--request PUT 'http://localhost:3000/demo.json' \
39
--header "Content-Type: application/json" \
--data '{"a":"b"}'
Output:
40
RESTful routes and resources
This section demonstrates how to:
41
Create a book struct
Suppose we want our app to have features related to books.
/// Use Deserialize to convert e.g. from request JSON into Book struct.
use serde::Deserialize;
Add code to create a book struct that derives the traits we want:
/// Demo book structure with some example fields for id, title, author.
#[derive(Debug, Deserialize, Clone, Eq, Hash, PartialEq)]
pub struct Book {
pub id: u32,
pub title: String,
pub author: String,
}
Add code to include the book module and use the Book struct:
42
Create the data store
For a production app, we could implement the data by using a database.
For this demo, we will implement the data by using a global variable
DATA.
use std::collections::HashMap;
/// Bring Book struct into scope
use crate::book::Book;
/// Use once_cell for creating a global variable e.g. our DATA data.
use once_cell::sync::Lazy;
/// Use Mutex for thread-safe access to a variable e.g. our DATA data.
use std::sync::Mutex;
/// Create a data store as a global variable with `Lazy` and `Mutex`.
/// This demo implementation uses a `HashMap` for ease and speed.
/// The map key is a primary key for lookup; the map value is a Book.
pub static DATA: Lazy<Mutex<HashMap<u32, Book>>> = Lazy::new(|| Mutex::new
HashMap::from([
(1, Book {
id: 1,
title: "Antigone".into(),
author: "Sophocles".into()
}),
(2, Book {
id: 2,
title: "Beloved".into(),
author: "Toni Morrison".into()
}),
43
(3, Book {
id: 3,
title: "Candide".into(),
author: "Voltaire".into()
}),
])
));
44
Use the data store
Edit file main.rs.
Add code to include the data module and use the DATA global variable:
/// See file data.rs, which defines the DATA global variable.
mod data;
use crate::data::DATA;
/// Use Thread for spawning a thread e.g. to acquire our DATA mutex lock.
use std::thread;
/// To access data, create a thread, spawn it, then get the lock.
/// When you're done, then join the thread with its parent thread.
async fn print_data() {
thread::spawn(move || {
let data = DATA.lock().unwrap();
println!("data: {:?}" ,data);
}).join().unwrap()
}
If you want to see all the data now, then add function to main:
async fn main() {
print_data().await;…
Shell:
cargo run
Output:
data: {
1: Book { id: 1, title: "Antigone", author: "Sophocles" },
2: Book { id: 2, title: "Beloved", author: "Toni Morrison" },
3: Book { id: 3, title: "Candide", author: "Voltaire" }
}
45
Get all books
Edit file main.rs.
use crate::book::Book;
Add a route:
.route("/books",
get(get_books)
);
Add a handler:
/// axum handler for "GET /books" which responds with a resource page.
/// This demo uses our DATA; a production app could use a database.
/// This demo must clone the DATA in order to sort items by title.
pub async fn get_books() -> axum::response::Html<String> {
thread::spawn(move || {
let data = DATA.lock().unwrap();
let mut books = data.values().collect::<Vec<_>>().clone();
books.sort_by(|a, b| a.title.cmp(&b.title));
books.iter().map(|&book|
format!("<p>{}</p>\n", &book)
).collect::<String>()
}).join().unwrap().into()
}
Shell:
cargo run
Shell:
curl 'http://localhost:3000/books'
46
Output:
<p>Antigone by Sophocles</p>
<p>Beloved by Toni Morrison</p>
<p>Candide by Voltaire</p>
47
Get one book
Edit file main.rs.
Add a route:
.route("/books/:id",
get(get_books_id)
);
Add a handler:
/// axum handler for "GET /books/:id" which responds with one resource HTM
/// This demo app uses our DATA variable, and iterates on it to find the i
pub async fn get_books_id(
axum::extract::Path(id): axum::extract::Path<u32>
) -> axum::response::Html<String> {
thread::spawn(move || {
let data = DATA.lock().unwrap();
match data.get(&id) {
Some(book) => format!("<p>{}</p>\n", &book),
None => format!("<p>Book id {} not found</p>", id),
}
}).join().unwrap().into()
}
Shell:
cargo run
Shell:
curl 'http://localhost:3000/books/1'
Output:
<p>Antigone by Sophocles</p>
48
Shell:
curl 'http://localhost:3000/books/0'
Output:
49
Put one book
Edit file main.rs.
.route("/books",
get(get_books)
.put(put_books)
);
Add a handler:
/// axum handler for "PUT /books" which creates a new book resource.
/// This demo shows how axum can extract JSON data into a Book struct.
pub async fn put_books(
axum::extract::Json(book): axum::extract::Json<Book>
) -> axum::response::Html<String> {
thread::spawn(move || {
let mut data = DATA.lock().unwrap();
data.insert(book.id, book.clone());
format!("Put book: {}", &book)
}).join().unwrap().into()
}
Shell:
cargo run
Shell:
curl \
--request PUT 'http://localhost:3000/books' \
--header "Content-Type: application/json" \
--data '{"id":4,"title":"Decameron","author":"Giovanni Boccaccio"}'
Output:
50
Put book: Decameron by Giovanni Boccaccio
Shell:
curl 'http://localhost:3000/books'
Output:
<p>Antigone by Sophocles</p>
<p>Beloved by Toni Morrison</p>
<p>Candide by Voltaire</p>
<p>Decameron by Giovanni Boccaccio</p>
51
Get one book as a web form
Edit file main.rs.
Add a route:
.route("/books/:id/form",
get(get_books_id_form)
);
Add a handler:
/// axum handler for "GET /books/:id/form" which responds with a form.
/// This demo shows how to write a typical HTML form with input fields.
pub async fn get_books_id_form(
axum::extract::Path(id): axum::extract::Path<u32>
) -> axum::response::Html<String> {
thread::spawn(move || {
let data = DATA.lock().unwrap();
match data.get(&id) {
Some(book) => format!(
concat!(
"<form method=\"post\" action=\"/books/{}/form\">\n",
"<input type=\"hidden\" name=\"id\" value=\"{}\">\n",
"<p><input name=\"title\" value=\"{}\"></p>\n",
"<p><input name=\"author\" value=\"{}\"></p>\n",
"<input type=\"submit\" value=\"Save\">\n",
"</form>\n"
),
&book.id,
&book.id,
&book.title,
&book.author
),
None => format!("<p>Book id {} not found</p>", id),
}
}).join().unwrap().into()
}
52
Try the demo…
Shell:
cargo run
Shell:
curl 'http://localhost:3000/books/1/form'
Output:
53
Post one book as a web form
Edit file main.rs.
.route("/books/:id/form",
get(get_books_id_form)
.post(post_books_id_form)
);
Add a handler:
/// axum handler for "POST /books/:id/form" which submits an HTML form.
/// This demo shows how to do a form submission then update a resource.
pub async fn post_books_id_form(
form: axum::extract::Form<Book>
) -> axum::response::Html<String> {
let new_book: Book = form.0;
thread::spawn(move || {
let mut data = DATA.lock().unwrap();
if data.contains_key(&new_book.id) {
data.insert(new_book.id, new_book.clone());
format!("Post book: {}", &new_book)
} else {
format!("Book id not found: {}", &new_book.id)
}
}).join().unwrap().into()
}
Shell:
cargo run
Shell:
curl \
54
--request POST 'localhost:3000/books/1/form' \
--header "Content-Type: application/x-www-form-urlencoded" \
--data "id=1" \
--data "title=Another Title" \
--data "author=Someone Else"
Output:
Shell:
curl 'http://localhost:3000/books'
Output:
55
Delete one book
Edit file main.rs.
.route("/books/:id",
get(get_books_id)
.delete(delete_books_id)
);
Add a handler:
Shell:
cargo run
Shell:
56
Output:
Shell:
curl 'http://localhost:3000/books'
Output:
57
Extras
This section shows how to:
58
Add a Tower tracing subscriber
Edit file Cargo.toml.
Add dependencies:
Shell:
cargo run
You should see console output that shows tracing initialization such as:
2022-03-08T00:13:54.483877Z
TRACE mio::poll:
registering event source with poller:
59
token=Token(1),
interests=READABLE | WRITABLE
60
Use a host, port, and socket address
To bind the server, our demo code uses a socket address string.
use std::net::SocketAddr;
61
Conclusion
62
What you learned
You learned how to:
63
What’s next
To learn more about Rust, axum, tower, hyper, tokio, and Serde:
Feedback
Contact
https://linkedin.com/in/joelparkerhenderson
https://github.com/joelparkerhenderson
64
axum examples
The axum source code repository includes many project examples, and
these examples are fully runnable.
• async‐graphql
• chat
• cors
• customize‐extractor‐error
• customize‐path‐rejection
• error‐handling‐and‐dependency‐injection
• form
• global‐404‐handler
• graceful‐shutdown
• hello‐world
• http‐proxy
• jwt
• key‐value‐store
• low‐level‐rustls
• multipart‐form
• oauth
• print‐request‐response
• prometheus‐metrics
• query‐params‐with‐empty‐strings
• readme
• reverse‐proxy
• routes‐and‐handlers‐close‐together
• sessions
• sqlx‐postgres
• sse
• static‐file‐server
• templates
• testing
• tls‐rustls
• todos
65
• tokio‐postgres
• tracing‐aka‐logging
• unix‐domain‐socket
• validator
• versioning
• websockets
66