Axum および Actix Web における依存性注入戦略
Ethan Miller
Product Engineer · Leapcell

`--- title: "Dependency Injection Strategies in Axum and Actix Web" summary: "This article explores practical dependency injection patterns for Rust web frameworks like Axum and Actix Web, enhancing testability, maintainability, and modularity through common DI techniques and code examples."
Introduction
Building robust and maintainable web services in Rust, particularly with frameworks like Axum and Actix Web, often involves managing complex application states and external dependencies. As applications grow, explicitly passing every needed resource through multiple layers of functions can become cumbersome, leading to boilerplate code and tightly coupled components. This is where dependency injection (DI) shines. Dependency injection is a powerful design pattern that promotes loose coupling and improves the testability and maintainability of your codebase by decoupling the creation and usage of objects. In the context of web development, this translates to effortlessly managing database connections, API clients, configuration settings, or even other services that your handlers depend on. This article will delve into the various approaches to implementing dependency injection within Axum and Actix Web, providing practical examples to illustrate how you can leverage these patterns to build more flexible and scalable Rust web applications.
Core Concepts for Dependency Injection
Before diving into the implementation details, let's clarify some fundamental concepts that are central to understanding dependency injection.
- Dependency: In software development, a dependency refers to an object or module that another object or module needs to function correctly. For instance, a user service might depend on a database connection pool to store and retrieve user data.
- Inversion of Control (IoC): IoC is a design principle where the control of object creation and lifecycle is transferred from the application code to a framework or a container. Dependency injection is a specific form of IoC. Instead of an object creating its dependencies, the dependencies are provided to it by an external entity.
- Dependency Injection (DI): DI is a technique where an object receives other objects that it depends on, rather than creating them itself. This promotes loose coupling because the dependent object doesn't need to know how its dependencies are constructed.
- Service Locator: An alternative pattern where an object explicitly asks a "locator" for its dependencies. While it can reduce direct coupling, it introduces a "global dependency" on the locator itself, which can make testing harder and hide dependencies. DI is generally preferred over Service Locator.
- Container/Injector: A component responsible for instantiating, configuring, and managing the lifecycle of objects, and injecting their dependencies. While explicit DI containers are less common in Rust due to its strong type system and ownership model, the concept of a "container" often manifests as shared application state or custom structs.
Implementing Dependency Injection in Axum and Actix Web
In Rust web frameworks, the primary methods for dependency injection often revolve around sharing state across handlers or leveraging extractor patterns. We'll explore these strategies with practical code examples.
Axum: State and Extractors
Axum, with its tower::Service
foundation, provides elegant ways to manage shared state, which naturally lends itself to dependency injection.
Strategy 1: Shared Application State
The most common approach in Axum is to wrap all your application's shared resources into a single AppState
struct and then make it accessible to handlers via an Extension
.
Let's imagine we have a UserService
that interacts with a database and a Mailer
service for sending emails.
// src/main.rs use axum::* use std::* hyp::* use uuid::* // --- Dependencies --- // A mock database client #[derive(Debug, Clone)] struct DbClient { // In a real app, this would be a connection pool like sqlx::PgPool } impl DbClient { async fn new() -> Self { println!("Initializing DbClient..."); tokio::time::sleep(std::time::Duration::from_millis(50)).await; // Simulate async init DbClient {} } async fn fetch_user(&self, id: Uuid) -> Option<User> { println!("Fetching user {} from DB...", id); tokio::time::sleep(std::time::Duration::from_millis(100)).await; if id == Uuid::parse_str("c9f0b1a0-3e3e-4d4d-8a8a-0a0a0a0a0a0a").unwrap() { Some(User { id, name: "Alice".to_string(), email: "alice@example.com".to_string(), }) } else { None } } async fn create_user(&self, user: User) { println!("Creating user in DB: {:?}", user); tokio::time::sleep(std::time::Duration::from_millis(50)).await; } } // A mock mailer service #[derive(Debug, Clone)] struct Mailer { // In a real app, this could be a client for SendGrid, Mailchimp, etc. } impl Mailer { async fn new() -> Self { println!("Initializing Mailer..."); Mailer {} } async fn send_welcome_email(&self, email: String, name: String) { println!("Sending welcome email to {} ({})", name, email); tokio::time::sleep(std::time::Duration::from_millis(70)).await; // Logic to send email } } // --- Application Services (Business Logic) --- #[derive(Debug, Clone)] struct UserService { db_client: DbClient, } impl UserService { fn new(db_client: DbClient) -> Self { UserService { db_client } } async fn get_user_by_id(&self, id: Uuid) -> Option<User> { self.db_client.fetch_user(id).await } async fn register_user(&self, new_user: NewUser) -> User { let user = User { id: Uuid::new_v4(), name: new_user.name, email: new_user.email, }; self.db_client.create_user(user.clone()).await; user } } // --- DTOs and Models --- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct User { id: Uuid, name: String, email: String, } #[derive(Debug, Clone, serde::Deserialize)] struct NewUser { name: String, email: String, } // --- Application State --- // This struct holds our shared services/dependencies #[derive(Clone)] struct AppState { user_service: UserService, mailer: Mailer, } // A more robust way using Arc for complex services that might be shared across tasks // and potentially mutated (though here, they are mostly `Clone`). // If a service itself has internal mutable state that needs to be shared and modified // across *different* requests or handlers, wrap it in Arc<Mutex<T>> or Arc<RwLock<T>>. // For our `UserService` and `Mailer` here, their internal `DbClient` and `Mailer` // are themselves `Clone`, so a simple `AppState` clone is enough. // For demonstration, let's say DbClient itself is complex and expensive to clone, // and UserService holds an Arc to it. #[derive(Clone)] struct AppStateArc { user_service: Arc<UserService>, // If UserService could be expensive to clone or modified mailer: Arc<Mailer>, } // --- Handlers --- async fn get_user_handler( // Using State extractor (requires `AppState` to be clone) State(app_state): State<AppState>, axum::extract::Path(user_id): axum::extract::Path<Uuid>, ) -> Result<Json<User>, axum::http::StatusCode> { println!("Handling get_user_handler for ID: {}", user_id); match app_state.user_service.get_user_by_id(user_id).await { Some(user) => Ok(Json(user)), None => Err(axum::http::StatusCode::NOT_FOUND), } } async fn register_user_handler( // Using State extractor (requires `AppState` to be clone) State(app_state): State<AppState>, Json(new_user): Json<NewUser>, ) -> Json<User> { println!("Handling register_user_handler for user: {}", new_user.name); let registered_user = app_state.user_service.register_user(new_user).await; let _ = app_state .mailer .send_welcome_email(registered_user.email.clone(), registered_user.name.clone()) .await; Json(registered_user) } async fn get_user_handler_arc( // Using State extractor with Arc<AppStateArc> // This is useful if the AppState itself contains complex items you don't want to clone often State(app_state): State<Arc<AppStateArc>>, axum::extract::Path(user_id): axum::extract::Path<Uuid>, ) -> Result<Json<User>, axum::http::StatusCode> { println!("Handling get_user_handler_arc for ID: {}", user_id); match app_state.user_service.get_user_by_id(user_id).await { Some(user) => Ok(Json(user)), None => Err(axum::http::StatusCode::NOT_FOUND), } } #[tokio::main] async fn main() { // Initialize dependencies let db_client = DbClient::new().await; let mailer = Mailer::new().await; // Initialize application services let user_service = UserService::new(db_client.clone()); // db_client is Clone // Create application state let app_state = AppState { user_service: user_service.clone(), // user_service is Clone mailer: mailer.clone(), // mailer is Clone }; // For Arc example: let app_state_arc = Arc::new(AppStateArc { user_service: Arc::new(user_service), mailer: Arc::new(mailer), }); // Build Axum application let app = Router::new() .route("/users/:id", get(get_user_handler)) .route("/users", post(register_user_handler)) // Route for the Arc example .route("/arc/users/:id", get(get_user_handler_arc)) .with_state(app_state) // Attach AppState to the router .with_state(app_state_arc); // You can attach multiple states if needed let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); println!("Axum server listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); }
In this example:
- We define
DbClient
andMailer
as our low-level dependencies. UserService
is a higher-level service that depends onDbClient
.AppState
is a struct that collectsUserService
andMailer
. Crucially,AppState
(and its contained services) must implementClone
because Axum clones the state for each incoming request. If a service itself holds non-Clone
types or needs thread-safe mutation, it should be wrapped inArc<Mutex<T>>
orArc<RwLock<T>>
.- In
main
, we instantiate all dependencies and services. - We then attach
AppState
to the Axum router using.with_state(app_state)
. - Handlers access these dependencies using the
State<AppState>
extractor. Axum automatically provides the correctAppState
instance.
This pattern is very idiomatic for Axum and provides clear lifetime management and explicit dependency declaration.
Strategy 2: Custom Extractor for Specific Dependencies
While State
is great for whole application state, sometimes you might want to inject only a specific dependency or a combination of dependencies, potentially with some extra logic. You can create custom extractors for this.
// Continuing from previous Axum example, define a Custom UserService Extractor use axum::extract::* // ... (DbClient, Mailer, User, NewUser, UserService, AppState, AppStateArc definitions as above) ... // Our Custom UserService Extractor struct InjectedUserService(UserService); #[async_trait] impl<S> FromRequestParts<S> for InjectedUserService where S: Send + Sync, AppState: FromRequestParts<S>, // Ensure AppState can be extracted { type Rejection = <AppState as FromRequestParts<S>>::Rejection; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { let app_state = AppState::from_request_parts(parts, state).await?; Ok(InjectedUserService(app_state.user_service.clone())) } } // A handler using the custom extractor async fn get_user_with_custom_extractor( InjectedUserService(user_service): InjectedUserService, // Extract our custom service axum::extract::Path(user_id): axum::extract::Path<Uuid>, ) -> Result<Json<User>, axum::http::StatusCode> { println!( "Handling get_user_with_custom_extractor for ID: {}", user_id ); match user_service.get_user_by_id(user_id).await { Some(user) => Ok(Json(user)), None => Err(axum::http::StatusCode::NOT_FOUND), } } // Add this route to main() // .route("/custom_extracted_users/:id", get(get_user_with_custom_extractor))
This custom extractor fetches the AppState
and then extracts the UserService
from it. This can be beneficial if:
- You want to abstract the source of a dependency (e.g., it might come from state, or a request header, etc.).
- You want to perform validation or authorization logic before the handler receives the dependency.
- You want to inject a specific view or composition of services, rather than the entire
AppState
.
Actix Web: Application Data and Extractor Traits
Actix Web also offers robust mechanisms for dependency injection, primarily through application data (web::Data
) and custom extractors.
Strategy 1: Shared Application Data (web::Data
)
Similar to Axum's State
, Actix Web uses web::Data
to share application-level state and services across requests. The data is wrapped in Arc
internally, ensuring thread-safe access.
Let's adapt our previous example for Actix Web.
// src/main.rs use actix_web::* use std::* hyp::* use uuid::* // --- Dependencies --- // (DbClient, Mailer, User, NewUser definitions as above - just copy them) // Make sure DbClient and Mailer are Clone if you're not wrapping them in Arc inside the Service, // or if the service itself is Arc. For Actix Web's Data, the internal type must be Send + Sync + 'static. #[derive(Debug, Clone)] struct DbClient {} impl DbClient { async fn new() -> Self { println!("Initializing DbClient..."); tokio::time::sleep(std::time::Duration::from_millis(50)).await; DbClient {} } async fn fetch_user(&self, id: Uuid) -> Option<User> { println!("Fetching user {} from DB...", id); tokio::time::sleep(std::time::Duration::from_millis(100)).await; if id == Uuid::parse_str("c9f0b1a0-3e3e-4d4d-8a8a-0a0a0a0a0a0a").unwrap() { Some(User { id, name: "Bob".to_string(), email: "bob@example.com".to_string(), }) } else { None } } async fn create_user(&self, user: User) { println!("Creating user in DB: {:?}", user); tokio::time::sleep(std::time::Duration::from_millis(50)).await; } } #[derive(Debug, Clone)] struct Mailer {} impl Mailer { async fn new() -> Self { println!("Initializing Mailer..."); Mailer {} } async fn send_welcome_email(&self, email: String, name: String) { println!("Sending welcome email to {} ({})", name, email); tokio::time::sleep(std::time::Duration::from_millis(70)).await; } } // --- Application Services (Business Logic) --- #[derive(Debug)] // removed Clone here for UserService to show Arc usage in AppData struct UserService { db_client: DbClient, } impl UserService { fn new(db_client: DbClient) -> Self { UserService { db_client } } async fn get_user_by_id(&self, id: Uuid) -> Option<User> { self.db_client.fetch_user(id).await } async fn register_user(&self, new_user: NewUser) -> User { let user = User { id: Uuid::new_v4(), name: new_user.name, email: new_user.email, }; self.db_client.create_user(user.clone()).await; user } } // --- DTOs and Models --- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct User { id: Uuid, name: String, email: String, } #[derive(Debug, Clone, serde::Deserialize)] struct NewUser { name: String, email: String, } // --- Handlers --- #[get("/users/{user_id}")] async fn get_user_handler_actix( user_id: web::Path<Uuid>, // Inject UserService via web::Data, which holds an Arc internally user_service: web::Data<UserService>, ) -> impl Responder { println!("Handling get_user_handler_actix for ID: {}", user_id); match user_service.get_user_by_id(user_id.into_inner()).await { Some(user) => HttpResponse::Ok().json(user), None => HttpResponse::NotFound().body("User Not Found"), } } #[post("/users")] async fn register_user_handler_actix( new_user_data: web::Json<NewUser>, // Inject both UserService and Mailer user_service: web::Data<UserService>, mailer: web::Data<Mailer>, ) -> impl Responder { println!( "Handling register_user_handler_actix for user: {}", new_user_data.name ); let registered_user = user_service .register_user(new_user_data.into_inner()) .await; let _ = mailer .send_welcome_email(registered_user.email.clone(), registered_user.name.clone()) .await; HttpResponse::Created().json(registered_user) } #[actix_web::main] async fn main() -> std::io::Result<()> { // Initialize dependencies let db_client = DbClient::new().await; let mailer = Mailer::new().await; // Initialize application services // UserService itself is not Clone, so we wrap it in Arc for web::Data let user_service = Arc::new(UserService::new(db_client)); HttpServer::new(move || { App::new() // Attach dependencies as application data // web::Data wraps the type in Arc<T> .app_data(web::Data::new(user_service.clone())) // UserService must be Send + Sync + 'static .app_data(web::Data::new(mailer.clone())) // Mailer must be Send + Sync + 'static .service(get_user_handler_actix) .service(register_user_handler_actix) }) .bind(("127.0.0.1", 8080))? // Corrected port to 8080 .run() .await }
In the Actix Web example:
- We define our
DbClient
,Mailer
, andUserService
similar to Axum. - In
main
, we instantiateUserService
andMailer
. Sinceweb::Data
inherently wraps its content in anArc
,UserService
andMailer
themselves don't strictly need to beClone
if they are wrapped inArc
once. However, they must beSend + Sync + 'static
. - We attach these services to the
App
instance using.app_data(web::Data::new(...))
. We use.clone()
onuser_service
becauseHttpServer::new
expects aFnMut
closure, andapp_data
consumes the value. cloning theArc
is cheap. - Handlers receive these dependencies by simply declaring them as arguments of type
web::Data<T>
, whereT
is the type of the dependency. Actix Web's extractor system automatically resolves and injects the correct data.
This web::Data
approach is the most common and robust way to manage dependencies in Actix Web.
Strategy 2: Custom Extractor Implementation
Actix Web also allows for custom extractors using the FromRequest
trait, providing granular control over dependency injection, similar to Axum's custom extractors.
// Continuing from previous Actix Web example, define a Custom UserService Extractor use actix_web::FromRequest; // Import trait use futures::future::{ready, Ready}; use actix_web::dev::Payload; use actix_web::Error; // ... (DbClient, Mailer, User, NewUser, UserService definitions as above) ... // Our Custom UserService Extractor pub struct MyInjectedUserService(pub Arc<UserService>); // Hold Arc to the service impl FromRequest for MyInjectedUserService { type Error = Error; type Future = Ready<Result<Self, Self::Error>>; fn from_request(req: &actix_web::HttpRequest, _: &mut Payload) -> Self::Future { // We can access app data here if let Some(user_service_arc) = req.app_data::<web::Data<UserService>>() { ready(Ok(MyInjectedUserService(user_service_arc.into_inner()))) } else { // This case should ideally not happen if app_data is correctly configured ready(Err(actix_web::error::ErrorInternalServerError( "UserService not found in application data", ))) } } } // A handler using the custom extractor #[get("/custom_extracted_users/{user_id}")] async fn get_user_with_custom_extractor_actix( user_id: web::Path<Uuid>, InjectedUserService(user_service): MyInjectedUserService, // Use our custom extractor ) -> impl Responder { println!( "Handling get_user_with_custom_extractor_actix for ID: {}", user_id ); match user_service.get_user_by_id(user_id.into_inner()).await { Some(user) => HttpResponse::Ok().json(user), None => HttpResponse::NotFound().body("User Not Found"), } } // Add this route to main()'s App config: // .service(get_user_with_custom_extractor_actix) ## Benefits of Dependency Injection * **Testability:** By injecting dependencies, you can easily swap real implementations with mock objects during testing, making unit and integration tests much simpler and more reliable. * **Modularity and Loose Coupling:** Components are less reliant on the internal implementation details of their dependencies. They only "know" about the interfaces (traits) of what they need, promoting better separation of concerns. * **Maintainability:** Changes to a dependency's implementation don't require changes in every component that uses it, as long as the interface remains the same. This reduces the surface area for bugs. * **Flexibility and Reusability:** Services can be configured differently for various environments (development, staging, production) or reused in different contexts without modification. * **Clarity:** It's immediately clear what dependencies a component needs by looking at its constructor or handler arguments. ## Conclusion Dependency injection is a crucial design pattern for building scalable, testable, and maintainable applications in Rust, especially within web frameworks like Axum and Actix Web. By leveraging shared application state (Axum's `State`, Actix Web's `web::Data`) and custom extractors, developers can elegantly manage external resources and service dependencies, leading to cleaner codebases and a more explicit control flow. Mastering these dependency injection strategies will significantly improve your ability to craft robust and adaptable Rust web services.