State & Dependency Injection
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against axum 0.8.9 on Rust 1.99 (edition 2024) with tokio 1.53, checked on 2026-10-06. Handlers need shared resources — a database pool, an HTTP client, configuration, caches — and axum has no global container: you put them in a state value that the router hands to handlers. Done right, this is compile-time-checked dependency injection: a handler that asks for something the router does not provide does not compile.
Where it runs in the request
In the request lifecycle (tokio accepts
the connection, hyper parses the request, tower layers wrap the router, the router matches a route, middleware and
extractors run, the handler runs, the response travels back out), state is attached when the router is built and read
in the extractor step: the State extractor clones it from the router after routing and before the handler.
Middleware reaches the same state with from_fn_with_state, and the outer tower layers before the router do not see
it at all — they receive it, if they need it, as their own constructor arguments.
State, Router<S> and with_state
use std::sync::Arc;
use axum::{Router, extract::State, routing::get};
#[derive(Clone)]
struct AppState {
library_name: Arc<str>,
http: reqwest::Client, // already an Arc inside: cheap to clone
}
async fn home(State(state): State<AppState>) -> String {
format!("Welcome to {}\n", state.library_name)
}
fn api() -> Router<AppState> {
// still *needs* an AppState: it is not given one here
Router::new().route("/ping", get(|State(s): State<AppState>| async move { format!("{} api\n", s.library_name) }))
}
fn app(state: AppState) -> Router {
Router::new()
.route("/", get(home))
.nest("/api/v1", api())
.with_state(state) // Router<AppState> -> Router<()>
}
#[tokio::main]
async fn main() {
let state = AppState { library_name: Arc::from("Bookshelf"), http: reqwest::Client::new() };
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app(state)).await.unwrap();
}
Source: axum: Using the State extractor
The type parameter of Router<S> is the state the router is missing, not the state it has. Router::new() infers
S from the handlers you add; with_state(state) supplies it and turns the router into Router<()> (written
Router), which is the only kind axum::serve accepts. Sub-routers are typed Router<AppState> and receive the state
from the parent when they are nested or merged, so it is supplied exactly once.
Source: axum: Router::with_state
If you forget with_state, or give a router the wrong state type, the error is a type mismatch at the route call,
not at runtime:
error[E0308]: mismatched types
--> src/app.rs:6:30
|
6 | Router::new().route("/", get(home))
| ----- ^^^^^^^^^ expected `MethodRouter`, found `MethodRouter<AppState>`
|
= note: expected struct `MethodRouter<()>`
found struct `MethodRouter<AppState>`
Source: Rust error codes: E0308 and axum: Router::with_state
The function’s return type promised a Router (= Router<()>) but a handler needs an AppState. Either return
Router<AppState> and call with_state higher up, or call with_state before returning.
E0308 is Rust’s generic "mismatched types" code.
Why state must be cheap to clone
State<S> requires S: Clone, and axum clones the state for every request. That is fine because state should be a
bundle of handles, each of which is an Arc or contains one: sqlx::PgPool, reqwest::Client,
redis::aio::ConnectionManager, Arc<Settings>, Arc<dyn BookRepo>. Cloning them increments reference counts.
Never put a large owned value (a Vec of books, a String of templates) directly in state; wrap it in an Arc.
Substates with FromRef
Handlers and extractors that need only part of the state should not depend on the whole AppState. FromRef
describes how to obtain a sub-value, and State<Sub> then works in any router whose state implements
FromRef<AppState> for Sub. The derive implements it for every field:
use std::sync::Arc;
use axum::{
Router,
extract::{FromRef, State},
routing::get,
};
#[derive(Debug)]
struct Settings {
library_name: String,
max_loans: u8,
}
#[derive(Clone, Default)]
struct Metrics {
requests: Arc<std::sync::atomic::AtomicU64>,
}
#[derive(Clone, FromRef)]
struct AppState {
settings: Arc<Settings>,
metrics: Metrics,
}
// Needs only the settings: works with any state that provides an Arc<Settings>.
async fn loan_policy(State(settings): State<Arc<Settings>>) -> String {
format!("{} lends up to {} books\n", settings.library_name, settings.max_loans)
}
async fn hits(State(metrics): State<Metrics>) -> String {
let n = metrics.requests.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
format!("{n} hits\n")
}
#[tokio::main]
async fn main() {
let state = AppState {
settings: Arc::new(Settings { library_name: "Bookshelf".into(), max_loans: 5 }),
metrics: Metrics::default(),
};
let app = Router::new().route("/policy", get(loan_policy)).route("/hits", get(hits)).with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Source: axum: State — Substates and derive FromRef
-
Each field type must be
Clone, and no two fields may have the same type (the derive would generate conflicting impls). Wrap duplicates in newtypes (struct ReadPool(PgPool),struct WritePool(PgPool)). -
#[from_ref(skip)]on a field omits its impl. -
Library extractors use a bound like
Key: FromRef<S>(axum-extra’s private cookie jar) orTokens: FromRef<S>(theCurrentUserextractor on Custom Extractors);FromRefis what makes them work with your state without knowing its type.
State, Extension, closures or task-locals?
axum documents four ways to share data with handlers:
| Mechanism | How | Use for |
|---|---|---|
|
|
application-wide dependencies — the default choice |
|
|
per-request data produced by middleware (the authenticated user, a request ID); values that must reach code that cannot see the state type |
closure captures |
|
one-off values for a single handler |
task-locals |
|
data needed deep inside non-handler code without passing it explicitly; rarely worth the indirection |
Before axum 0.6 introduced State, Extension with AddExtensionLayer was the only option, which is why older code
uses it for everything; prefer State for dependencies because the compiler checks it. Passing data from middleware to
handlers through extensions is shown on Writing Middleware.
Mutable state: which mutex?
Shared mutable state needs synchronisation. The tokio documentation’s guidance, which also applies to axum:
-
Use
std::sync::Mutex(orRwLock) when the critical section is short and contains no.await. It is faster than tokio’s mutex and is the right default. Keep the guard in an inner block or a synchronous helper so it is dropped before any.await— otherwise the handler’s future is notSendand does not compile (see Handlers). -
Use
tokio::sync::Mutexonly when the lock must be held across an.await, such as serialising use of one connection or a rate-limited upstream API. -
Avoid locks entirely where possible: atomics for counters,
DashMapor a sharded map for hot maps, a database or Redis for anything that must survive restarts or be shared between instances, and an actor (a task owning the data, driven through anmpscchannel) for complex state machines.
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use axum::{
Json, Router,
extract::{Path, State},
http::StatusCode,
routing::{get, put},
};
#[derive(Clone, Default)]
struct AppState {
// short critical sections, no .await inside: std RwLock
ratings: Arc<RwLock<HashMap<String, Vec<u8>>>>,
}
async fn rate(State(state): State<AppState>, Path(isbn): Path<String>, Json(stars): Json<u8>) -> StatusCode {
if !(1..=5).contains(&stars) {
return StatusCode::UNPROCESSABLE_ENTITY;
}
state.ratings.write().expect("ratings lock poisoned").entry(isbn).or_default().push(stars);
StatusCode::NO_CONTENT
}
async fn average(State(state): State<AppState>, Path(isbn): Path<String>) -> Result<String, StatusCode> {
let avg = {
let ratings = state.ratings.read().expect("ratings lock poisoned");
let r = ratings.get(&isbn).ok_or(StatusCode::NOT_FOUND)?;
f64::from(r.iter().map(|&s| u32::from(s)).sum::<u32>()) / r.len() as f64
}; // read guard dropped here
tokio::task::yield_now().await; // safe: no guard alive
Ok(format!("{avg:.1}\n"))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/books/{isbn}/rating", put(rate).get(average))
.with_state(AppState::default());
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Source: Tokio tutorial: Shared state
In-process state is per instance: with two replicas behind a load balancer, each has its own ratings map. Real
Bookshelf data lives in Postgres and Redis (see Databases with sqlx and
Redis & Caching).
Trait objects in state for testability
Putting Arc<dyn BookRepo> in state instead of a concrete PgBookRepo lets tests swap in an in-memory implementation
without a database, exactly like constructor injection in Spring or ASP.NET Core — only without a container:
use std::sync::Arc;
use axum::{
Router,
extract::{Path, State},
http::StatusCode,
routing::get,
};
use futures::future::BoxFuture;
pub trait BookRepo: Send + Sync {
fn title(&self, isbn: String) -> BoxFuture<'_, Option<String>>;
}
struct FixedRepo;
impl BookRepo for FixedRepo {
fn title(&self, isbn: String) -> BoxFuture<'_, Option<String>> {
Box::pin(async move { (isbn == "9780441013593").then(|| "Dune".to_owned()) })
}
}
#[derive(Clone)]
struct AppState {
books: Arc<dyn BookRepo>,
}
async fn title(State(state): State<AppState>, Path(isbn): Path<String>) -> Result<String, StatusCode> {
state.books.title(isbn).await.ok_or(StatusCode::NOT_FOUND)
}
pub fn app(books: Arc<dyn BookRepo>) -> Router {
Router::new().route("/books/{isbn}/title", get(title)).with_state(AppState { books })
}
#[tokio::main]
async fn main() {
// main wires the real adapter; tests call app(Arc::new(FakeRepo)) instead
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app(Arc::new(FixedRepo))).await.unwrap();
}
The trade-offs between trait objects and generics, and where the ports live, are discussed on Project Structure; the tests themselves on Testing.
Where the books are outdated
Yoon (Beginning Axum, chapter 2) uses State correctly but recommends tokio::sync::Mutex for shared state;
Gangappa & Rangaraj (Mastering Distributed Observability in Rust, chapter 2) present it as the default as well.
tokio’s own documentation says the opposite: use std::sync::Mutex unless the guard must be held across an .await.
Crossley et al. (Rust for Scientific Computing, ch. 15) and pre-0.6 tutorials share dependencies with
AddExtensionLayer/Extension, which still works but moves a compile-time check to runtime.