DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 963,503 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Carlos Armando Marcano Vargas
Carlos Armando Marcano Vargas

Posted on • Originally published at carlosmv.hashnode.dev

Getting Started with Axum | Rust

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"
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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, '8000'.

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
Enter fullscreen mode Exit fullscreen mode

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:

hello_world_image

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

Enter fullscreen mode Exit fullscreen mode

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();
}

Enter fullscreen mode Exit fullscreen mode

If we write in our browser localhost:3000/hello/Carlos, it will show this:

Extracting parameter image

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

Enter fullscreen mode Exit fullscreen mode

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);

...

Enter fullscreen mode Exit fullscreen mode

post method image

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

Enter fullscreen mode Exit fullscreen mode

We create a directory and create an HTML file in it.

src
static/
    hello.html
Cargo.toml

Enter fullscreen mode Exit fullscreen mode

hello.html

<h1>Hello everyone</h1>
<h2>This is a static file</h2>

Enter fullscreen mode Exit fullscreen mode

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),
            )
        }));

...

}

Enter fullscreen mode Exit fullscreen mode

Run the code and go to localhost:3000/static , we should see this page:

html page image

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

Enter fullscreen mode Exit fullscreen mode

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

Examples

Axum documentation

Tower_http documentation

Hyper documentation

Top comments (0)

What image format should you use in your next project? πŸ€”