Introduction
This tutorial will explain how to build web applications with axum fast while teaching basic concepts along the way. Props to David Pedersen for developing this great framework.
If you are new to rust, you should read the book before continuing.
Lets get started!
Hello World
A simple hello world application looks like this:
Cargo.toml
[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"
[dependencies]
# "?" is shorter than ".unwrap()"
anyhow = "1"
# full features because of laziness
tokio = { version = "1", features = ["full"] }
# import the greatest framework
axum = "0.7"
main.rs
use axum::{routing::get, Router}; use std::net::SocketAddr; use tokio::net::TcpListener; #[tokio::main] async fn main() -> anyhow::Result<()> { // pass incoming GET requests on "/hello-world" to "hello_world" handler. let app = Router::new().route("/hello-world", get(hello_world)); // write address like this to not make typos let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); let listener = TcpListener::bind(addr).await?; axum::serve(listener, app.into_make_service()).await?; Ok(()) } async fn hello_world() -> &'static str { "Hello, world!" }
If we enter "http://localhost:3000/hello-world" to browser, we can see "Hello, world!" text coming up.
What is going on in this example?
#![allow(unused)] fn main() { let app = Router::new().route("/hello-world", get(hello_world)); }
This part is easy. Does the request have "/hello-world" in its URL? Okay route it to method router (in this case "get"). Does the request have "GET" method? Okay route it to "hello_world" handler.
NOTE: You can add more routes by calling .route
again on Router
.
Lets look at hello_world
function which we will now refer as hello_world
handler.
#![allow(unused)] fn main() { async fn hello_world() -> &'static str { "Hello, world!" } }
This function can be used as a handler because it is async and its return
type implements IntoResponse
trait. We will look into IntoResponse
trait in detail later. For now lets use this powerful knowledge to add a
functionality to our app.
It would be cool if we could redirect all URLs coming to our application to
hello_world
right?
By randomly traveling in axum docs (which is how you will operate from now
on) I found us Router::fallback
method and Redirect
type we can use.
#![allow(unused)] fn main() { use axum::{response::Redirect, routing::get, Router}; // ^ import this ^ let app = Router::new() .route("/hello-world", get(hello_world)) .fallback(anything_else); // <-- add that fallback // create an easy handler async fn anything_else() -> Redirect { Redirect::to("/hello-world") } }
Do these changes, run the app then go to "http://localhost:3000/plz_no_404" and boom! It redirects us to "/hello-world".
Extractors
Axum provides extractors to get/extract data about requests to our handlers. To
qualify as a handler, parameters of a function must implement
FromRequestParts
last parameter can implement FromRequest
(meaning it
also implements FromRequestParts
). There are two traits because some parts
of request for example body can only be extracted once (can be cloned later).
In previous hello world example we didn't need any info about requests for our app to run. I was thinking, "Okay, I need to build some random app to explain readers how extractors work." What did I do? Of course I found something by randomly traveling in axum docs.
Every developer needs a way to get their clients ip address and sell it to big corporations. But of course as ethical developers, we will not do that. Instead we will explain the client we got their ip address and they can possibly be identified by harmful parties and to prevent that they need to buy our VPN service.
Here is a basic example of our app:
use axum::{extract::ConnectInfo, routing::get, Router}; use std::net::SocketAddr; use tokio::net::TcpListener; #[tokio::main] async fn main() -> anyhow::Result<()> { let app = Router::new().route("/", get(index)); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); let listener = TcpListener::bind(addr).await?; axum::serve( listener, app.into_make_service_with_connect_info::<SocketAddr>(), ) .await?; Ok(()) } async fn index(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String { format!( "Your ip address is \"{addr}\".\n\ You are in immediate danger of getting identified by bad people.\n\ Thankfully we have a VPN service to hide your ip. \n\ Visit this link to download it \"http://localhost:3000/average_joe_absolutely_needs_vpn\"" ) }
To be able to get ip address with ConnectInfo
extractor we need this
boilerplate code:
#![allow(unused)] fn main() { // copypasta for future projects axum::serve( listener, app.into_make_service_with_connect_info::<SocketAddr>(), ) .await?; }
Lets take a look at function signature:
#![allow(unused)] fn main() { async fn index(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String }
You might not be familiar of pattern matching done for function parameter "addr" here. Lets explain shortly.
ConnectInfo
is a struct. It is defined something like struct ConnectInfo<T>(T);
. It has paranthesis instead of braces and can take any type
like a tuple with single item. (example let something = ConnectInfo(String);
)
By using tuple structs in function parameters you can match inner item. If we
used addr: ConnectInfo<SocketAddr>
instead we would have to do let addr = addr.0;
in the code below to achieve the same thing.
To simplify, for now lets talk about it using the other syntax.
#![allow(unused)] fn main() { async fn index(addr: ConnectInfo<SocketAddr>) -> String }
addr
parameter type needs to implement FromRequest
or
FromRequestParts
for this function to qualify as a handler. ConnectInfo
implements it if we use it like how it is documented.
We are dynamically creating a string to add ip address inside. So we need to
use String
type which implements IntoResponse
.
In the handler code, its basically just format!
macro to create a string so
nothing new there.
Our app works fine but its just plaintext. Lets improve it by doing some html.
#![allow(unused)] fn main() { // change return type to `Html<String>` to let browser know we are sending html async fn index(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> Html<String> { let html = format!( "<h1>Your ip address is: \"{addr}\"</h1>\n\ <h2>You are in immediate danger of getting identified by bad people.</h2>\n\ <h2>Thankfully we have a VPN service to hide your ip.</h2>\n\ <h2>Visit <a href=\"http://localhost:3000/average_joe_absolutely_needs_vpn\">THIS</a> link to download it.</h2>" ); // create `Html` type like this Html(html) } }
Challenge
Create a /average_joe_absolutely_needs_vpn
page returning html to convince
the poor guy client to buy our VPN service.
Contributors
- Eray Karatay (@programatik29)