Doubts about efficiency of Axum implementation that uses methods as handlers

So I wanted to use Axum to implement a web server, where the logic of the handler is implemented as methods on a struct and where the struct takes care of setting up the server and listening.

My initial attempt came up with code that looks like this:

#[derive(Debug, Serialize, Deserialize)]
struct Person {
    age: u32,
    name: String
}

mod http {
    use axum::{
        routing::get,
        http::StatusCode,
        response::IntoResponse,
        Json, Router,
    };
    use std::net::SocketAddr;
    use crate::Person;

    pub struct ServerLogic {
        port: u16,
    }

    impl ServerLogic {
        pub fn new(port: u16) -> Self {
            Self {
                port
            }
        }
        pub async fn serve(&self) {
            let port = self.port;
            let app = Router::new()
                .route("/get", axum::routing::get(self.hello));

            let addr = SocketAddr::from(([127, 0, 0, 1], port));
            axum::Server::bind(&addr)
                .serve(app.into_make_service())
                .await
                .unwrap();
        }
        pub async fn hello(&self) -> impl axum::response::IntoResponse {
              (axum::http::StatusCode::OK, axum::Json(Person { age: 42, name: "John".to_string() }))
        }
    }
}

#[tokio::main]
async fn main() -> () {
    // build our application with a route
    http::ServerLogic::new(3000).serve().await;
}

This did not compile right away. Fiddling with the compiler, here are some of the examples of errors I encountered:

error[E0615]: attempted to take value of method `hello` on type `&ServerLogic`
  --> src/main.rs:45:56
   |
45 |                 .route("/get", axum::routing::get(self.hello));
   |                                                        ^^^^^ method, not a field
   |
help: use parentheses to call the method
   |
45 |                 .route("/get", axum::routing::get(self.hello()));
   |                                                             ++


error: lifetime may not live long enough
  --> src/main.rs:45:59
   |
45 |                 .route("/get", axum::routing::get(move || self.hello()));
   |                                                   ------- ^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
   |                                                   |     |
   |                                                   |     return type of closure `impl Future<Output = impl IntoResponse>` contains a lifetime `'2`
   |                                                   lifetime `'1` represents this closure's body

error[E0277]: the trait bound `ServerLogic: Clone` is not satisfied in `[closure@src/main.rs:45:51: 45:58]`
   --> src/main.rs:45:51
    |
45  |                 .route("/get", axum::routing::get(move || self.hello()));
    |                                ------------------ -------^^^^^^^^^^^^^
    |                                |                  |
    |                                |                  within `[closure@src/main.rs:45:51: 45:58]`, the trait `Clone` is not implemented for `ServerLogic`
    |                                |                  within this `[closure@src/main.rs:45:51: 45:58]`
    |                                required by a bound introduced by this call
    |
    = help: the trait `Handler<T, S, B2>` is implemented for `Layered<L, H, T, S, B, B2>`

But I finally came up with a version that works, which is reproduced below:

use serde::{Serialize, Deserialize};
use serde::de::StdError;

#[derive(Debug, Serialize, Deserialize)]
struct Person {
    age: u32,
    name: String
}

mod http {
    use axum::{
        routing::get,
        http::StatusCode,
        response::IntoResponse,
        Json, Router,
    };
    use std::net::SocketAddr;
        use crate::Person;

    #[derive(Clone)]
    pub struct ServerLogic {
        port: u16,
    }

    impl ServerLogic {
        pub fn new(port: u16) -> Self {
            Self {
                port
            }
        }
        pub async fn serve(self) {
            let port = self.port;
            let app = Router::new()
                .route("/get", axum::routing::get(move || self.hello()));

            let addr = SocketAddr::from(([127, 0, 0, 1], port));
            axum::Server::bind(&addr)
                .serve(app.into_make_service())
                .await
                .unwrap();
        }
        pub async fn hello(self) -> impl axum::response::IntoResponse {
              (axum::http::StatusCode::OK, axum::Json(Person { age: 42, name: "John".to_string() }))
        }
    }
}

#[tokio::main]
async fn main() -> () {
    // build our application with a route
    http::ServerLogic::new(3000).serve().await;
}

Now I am not sure if this is the most idiomatic or the most efficient way. My doubts are because:

  1. I had to change the struct methods from &self to self meaning the ownership is being moved to the methods. I feel this might not be the way an experienced Rust developer will go about things
  2. I had to have the ServerLogic struct implement Clone which I interpret to mean there would be duplication of data which perhaps could be avoided.

So my question is, are these doubts valid? If so, are they valid for the reasons I think they are? Or perhaps there are other inefficiency in the approach? And finally what would be the more idiomatic approach?

Well the first thing to note is that your example isn't actually using any data from the ServerLogic struct in the route, so that doesn't even really need to be a method.

Secondly, ServerLogic only has an integer inside it, so cloning it will not be expensive. I'm not sure precisely where axum performs that clone though.


axum is generally oriented around using free functions as handlers. There's nothing wrong with using closures, but if you look at the example code you'll primarily see free functions. Version 0.6 was recently released which included some nice new features around passing shared state to handlers in free functions. Here's one way you could adapt your code to use that style.

use std::sync::Arc;

use axum::{extract::State, response::IntoResponse};
use http::ServerLogic;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Person {
    age: u32,
    name: String,
}

mod http {
    use axum::{
        extract::State, http::StatusCode, response::IntoResponse, routing::get, Json, Router,
    };
    use std::{net::SocketAddr, sync::Arc};

    pub struct ServerLogic {
        port: u16,
    }

    impl ServerLogic {
        pub fn new(port: u16) -> Self {
            Self { port }
        }

        pub async fn serve(self) {
            let port = self.port;
            let app = Router::new()
                .route("/get", axum::routing::get(hello))
                .route("/bye", axum::routing::get(goodbye))
                .with_state(Arc::new(self));

            let addr = SocketAddr::from(([127, 0, 0, 1], port));
            axum::Server::bind(&addr)
                .serve(app.into_make_service())
                .await
                .unwrap();
        }
    }

    // Note: the `State(logic)` syntax is just using a pattern
    // to unwrap the state value from the extractor.
    // See `goodbye` for the other way to do that
    async fn hello(State(logic): State<Arc<ServerLogic>>) -> impl IntoResponse {
        (
            axum::http::StatusCode::OK,
            axum::Json(format!("Hello from port {}!", logic.port)),
        )
    }

    async fn goodbye(state: State<Arc<ServerLogic>>) -> impl IntoResponse {
        let logic = state.0;
        (
            axum::http::StatusCode::OK,
            axum::Json(format!("Goodbye from port {}!", logic.port)),
        )
    }
}

#[tokio::main]
async fn main() {
    // build our application with a route
    http::ServerLogic::new(3000).serve().await;
}
1 Like

Looks like extract::State is what I need to read up on and put to use!

Piggy-backing on OP's question that I stumbled upon after hours of frustration >.<

axum is generally oriented around using free functions as handlers

This is really a pain point to me.

I feel this approach probably has some reason I don't understand yet since I am new to rust.

Is there any reason for this design choice?

I was looking to tap into the use of generics to remove a lot of boilerplate code for controllers / handlers and this constraint is giving me a headache.

I feel like the baby is thrown out with the bath water, but it's probably because I have not acquired the appropriate understanding yet.

I will probably end up making macros instead, but generics would have been great.

Can somebody give me a pointer to help me fill the comprehension gap?

The short answer is that Rust's ownership model doesn't really fit well with with having shared state inside a type the way you might do it in a language like Java[1].

If you have logic that needs to run before the handler, you may want to use a custom extractor. If you have logic that needs to run when the response is sent, IntoResponse may be useful. If your needs don't fit neatly into either of those categories, the general purpose middleware system may be able to do what you need.

It's also worth noting that you can use closures as routes in axum, which can potentially help make some metaprogramming-like tasks easier without reaching for something like a macro

If you have more specific questions about how to do something, feel free to create a new thread


  1. though it's worth noting that doing that in Java can easily lead to bugs and data races due to multiple threads fighting over fields ↩ī¸Ž

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.