Extract route path param into Axum handler using tower::service_fn?

2 min read 04-10-2024
Extract route path param into Axum handler using tower::service_fn?


Extracting Route Path Parameters in Axum Handlers with tower::service_fn

Problem: You're building a REST API with Axum and need to access dynamic route parameters, like IDs, within your handler functions. However, you're using tower::service_fn to define your handler and it's not immediately clear how to extract these parameters.

Solution: This article explores how to seamlessly extract route path parameters within your Axum handlers using tower::service_fn.

Scenario: Imagine we're building a simple API with an endpoint that accepts a user ID as a path parameter:

use axum::{
    routing::{get, Router},
    http::StatusCode,
    response::IntoResponse,
    Json,
};
use tower::ServiceBuilder;

#[tokio::main]
async fn main() {
    // Define our handler
    let app = Router::new()
        .route("/users/:id", get(user_handler));

    // Bind and run the server
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn user_handler(
    // We need to access 'id' here!
) -> impl IntoResponse {
    // Logic to fetch user based on 'id'
    Ok((StatusCode::OK, Json("Hello, user!")))
}

The Challenge: Currently, our user_handler function doesn't have access to the id parameter from the route path. We need a way to bridge the gap between Axum's routing system and our handler function.

The Solution: tower::service_fn

Let's see how to use tower::service_fn effectively:

use axum::{
    routing::{get, Router},
    http::StatusCode,
    response::IntoResponse,
    Json,
};
use tower::ServiceBuilder;
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/users/:id", get(user_handler));

    // Bind and run the server
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn user_handler(
    // Extract the 'id' parameter
    Arc::<String>(id): Arc<String>,
) -> impl IntoResponse {
    // Logic to fetch user based on 'id'
    Ok((StatusCode::OK, Json(format!("Hello, user with ID: {}", id))))
}

Explanation:

  1. tower::service_fn: This function takes a closure and converts it into a Tower service. This service can then be integrated with Axum's routing system.
  2. Parameter Extraction: When using tower::service_fn, we can access the route parameters within the closure by pattern-matching against the Arc<String> representation of the parameter.

Key Considerations:

  • Type Safety: tower::service_fn ensures that parameters are correctly typed and can be used within your handler logic.
  • Flexibility: This method allows you to structure your handler functions in a way that's both clean and efficient.
  • Error Handling: You can use Result or other error handling mechanisms within your closure to gracefully manage potential errors.

Additional Tips:

  • Custom Data Types: If you have more complex route parameters, you can define custom data types and destructure them within your tower::service_fn closure.
  • Multiple Parameters: You can access multiple route parameters by defining them in the tower::service_fn closure.

Example with Custom Data Type:

use axum::{
    routing::{get, Router},
    http::StatusCode,
    response::IntoResponse,
    Json,
};
use tower::ServiceBuilder;
use std::sync::Arc;

#[derive(serde::Deserialize)]
struct UserId {
    id: u32,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/users/:id", get(user_handler));

    // Bind and run the server
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn user_handler(
    // Extract the 'id' parameter as a custom type
    Arc::<UserId>(user_id): Arc<UserId>,
) -> impl IntoResponse {
    // Logic to fetch user based on 'id'
    Ok((StatusCode::OK, Json(format!("Hello, user with ID: {}", user_id.id))))
}

Conclusion:

By understanding how to leverage tower::service_fn, you can easily extract route path parameters into your Axum handler functions, ensuring a seamless and efficient development experience.