Carlos Armando Marcano Vargas
Posted on July 20, 2022
I'm a newbie in Rust and I was looking for a web framework to use and build a server or an API, and I found Axum on Github, and I want to start to use it. I wanted to learn about Axum, so I started to write this article while exploring it to solidify some of its concepts and features.
DISCLAIMER: This is not a comprehensive guide about Axum, if you want to know every feature and how to use them, here is the documentation.
In this article, we just going to use the get()
and post()
methods and serve an HTML file.
According to its documentation:
axum is a web application framework that focuses on ergonomics and modularity.High level features
- Route requests to handlers with a macro-free API.
- Declaratively parse requests using extractors.
- Simple and predictable error handling model.
- Generate responses with minimal boilerplate.
- Take full advantage of the tower and tower-http ecosystem of middleware, services, and utilities.
In particular the last point is what sets axum apart from existing frameworks. axum doesn't have its own middleware system but instead uses tower::Service. This means axum gets timeouts, tracing, compression, authorization, and more, for free. It also enables you to share middleware with applications written using hyper or tonic.
Let's start importing our dependencies.
Cargo.toml
[dependencies]
axum = "0.5.11"
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1.35"
tracing-subscriber = "0.3.14"
serde = { version = "1.0.138", features = ["derive"] }
serde_json = "1.0"
main.rs
use axum::{
routing::{get, post},
http::StatusCode,
response::IntoResponse,
Json, Router};
use std::net::SocketAddr;
use serde::{Deserialize, Serialize};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/", get(root));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::info!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn root() -> &'static str {
"Hello, World!"
}
Now let's talk about the code above. First, we import what we are going to use. And write #[tokio::main]
above our main function to be allowed to use async. Then, we create an instance of Router and call the route
method with the path and the service that will be called if the path matches, in this case, root
, and it is wrapped in the method get
.
Then we use SocketAddr
to define the port we will use, and pass it the localhost IP address and a port number, in this case, '3000'.
We use the Server
and pass the reference of addr
to the bind
function
According to its doc:
The Server is the main way to start listening for HTTP requests. It wraps a listener with a MakeService, and then should be executed to start serving requests.
We pass app
as an argument to the serve
method but we need to use the into_make_service
method because serve
receives make_service
as a parameter and app
is a router instance.
Here is what its doc says about into_make_service
:
pub fn into_make_service(self) -> IntoMakeService< Self >
Convert this router into a MakeService, which is a Service whose response is another service.
This is useful when running your application with hyper’s Server
And MakeService
:
Creates new Service values.
Acts as a service factory. This is useful for cases where new Service values must be produced. One case is a TCP server listener. The listener accepts new TCP streams, obtains a new Service value using the MakeService trait, and uses that new Service value to process inbound requests on that new TCP stream.
This is essentially a trait alias for a Service of Services.
Here is the link if you want to know more about the MakeService trait.
Now we run the code
cargo run
It should print "listening on 127.0.0.1:3000" in our console, and if we copy the number and paste it into our browser we should see this page:
Extract a parameter from the path
Axum has many extractors, see the docs here
In this example, we will use Path
to extract a name from the path and use it to send a JSON message.
...
use axum::extract::Path;
...
async fn json_hello(Path(name): Path<String>) -> impl IntoResponse {
let greeting = name.as_str();
let hello = String::from("Hello ");
(StatusCode::OK, Json(json!({"message": hello + greeting })))
}
In the code block above, we use impl IntoResponse
as the return value of the function json_hello()
. According to the documentation:
Anything that implements IntoResponse can be returned from handlers. You generally shouldn’t have to implement IntoResponse manually, as axum provides implementations for many common types.
If you want to implement a custom error type, here is the documentation.
Now, let's update our main()
function.
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/", get(root))
.route("/hello/:name", get(json_hello));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::info!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
If we write in our browser localhost:3000/hello/Carlos
, it will show this:
Now, let's use the post()
method, to create a user.
...
#[derive(Deserialize)]
struct CreateUser {
username: String,
}
#[derive(Debug, Serialize,Deserialize, Clone, Eq, Hash, PartialEq)]
struct User {
id: u64,
username: String,
}
...
async fn create_user(Json(payload): Json<CreateUser>,) -> impl IntoResponse {
let user = User {
id: 1337,
username: payload.username
};
(StatusCode::CREATED, Json(user))
}
In the code above, we create two structs: CreateUser and User. We use the JSON extractor, to extract the payload, that is the data from a JSON, pass it as a value to the field username
, and return the user
variable as a JSON.
We add post()
route to main()
function.
...
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/", get(root))
.route("/hello/:name", get(json_hello))
.route("/user", post(create_user);
...
Serving Files
To serve files we have to add tower-http
to our project's dependencies.
Cargo.toml
...
[dependencies]
...
tower-http = { version = "0.3.0", features = ["fs", "trace"] }
We create a directory and create an HTML file in it.
src
static/
hello.html
Cargo.toml
hello.html
<h1>Hello everyone</h1>
<h2>This is a static file</h2>
main.rs
Now, we update our main()
function to add the route that serves our HTML file.
...
#[tokio::main]
async fn main() {
...
.route("/hello/:name", get(json_hello))
.route("/static", get_service(ServeFile::new("static/hello.html"))
.handle_error(|error: io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
}));
...
}
Run the code and go to localhost:3000/static
, we should see this page:
Complete code
main.rs
use axum::{
routing::{get, post, get_service},
http::StatusCode,
response::IntoResponse,
Json, Router};
use axum::extract::Path;
use tower_http::services::ServeFile;
use std::net::SocketAddr;
use serde::{Deserialize, Serialize};
use serde_json::{json};
use std::{io};
#[derive(Deserialize)]
struct CreateUser {
username: String,
}
#[derive(Debug, Serialize,Deserialize, Clone, Eq, Hash, PartialEq)]
struct User {
id: u64,
username: String,
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/", get(root))
.route("/user", post(create_user))
.route("/hello/:name", get(json_hello))
.route("/static", get_service(ServeFile::new("static/hello.html"))
.handle_error(|error: io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
}));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::info!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn root() -> &'static str {
"Hello, World!"
}
async fn create_user(Json(payload): Json<CreateUser>) -> impl IntoResponse {
let user = User {
id: 1337,
username: payload.username
};
(StatusCode::CREATED, Json(user))
}
async fn json_hello(Path(name): Path<String>) -> impl IntoResponse {
let greeting = name.as_str();
let hello = String::from("Hello ");
(StatusCode::OK, Json(json!({"message": hello + greeting })))
}
Conclusion
Axum is the first web framework I try in Rust, and I like it. The documentation is well written and has many examples on its Github page. One of the aspects a liked about it is its Macro-free API, another aspect is the possibility to chain the routes, so I can wrap all my handlers in the same code block.
The only thing that is stopping me to use it frequently and share more aspects of it is my lack of knowledge in Rust, but it is up to me to be more disciplined and I will because I want to contribute to this project.
Axum has a discord channel, the community is really great and helpful, here is the link.
Thank you for taking your time and read this article.
If you have any recommendations, advice, tips about how to improve my code, my English, or anything; please leave a comment or contact me through Twitter or LinkedIn.
The source code is here.
Reference
Posted on July 20, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.