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();
}

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.

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>`

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:

classDiagram class AppState { +db: PgPool +books: Arc~dyn BookRepo~ +settings: Arc~Settings~ +cookie_key: Key } class FromRef~AppState~ { <<trait>> +from_ref(&AppState) Self } class PgPool class Settings class Key AppState *-- PgPool AppState *-- Settings AppState *-- Key PgPool ..|> FromRef~AppState~ : derived Settings ..|> FromRef~AppState~ : via Arc Key ..|> FromRef~AppState~ : derived
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();
}
  • 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) or Tokens: FromRef<S> (the CurrentUser extractor on Custom Extractors); FromRef is 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

State<S>

Router::with_state; checked at compile time

application-wide dependencies — the default choice

Extension<T>

Router::layer(Extension(value)) or inserted into request extensions by middleware; a missing value is a runtime 500

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

get(move |...| ...) capturing an Arc

one-off values for a single handler

task-locals

tokio::task_local! scoped by middleware

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 (or RwLock) 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 not Send and does not compile (see Handlers).

  • Use tokio::sync::Mutex only 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, DashMap or 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 an mpsc channel) 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();
}

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.