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!