How does axum::extract::State share application state across route handlers?

axum::extract::State provides a type-safe extraction mechanism that passes clone-on-read access to application state stored in the router, enabling handlers to share configuration, database connections, caches, and other services without global variables or manual parameter passing. The state is stored once in the router and automatically injected into handlers that request it via the State extractor.

Basic State Extraction

use axum::{
    extract::State,
    routing::get,
    Router,
};
use std::sync::Arc;
 
// Define your application state:
#[derive(Clone)]
struct AppState {
    db_pool: DbPool,
    config: Config,
}
 
#[derive(Clone)]
struct DbPool;
struct Config {
    max_connections: u32,
}
 
// Use State extractor in handlers:
async fn get_user(State(state): State<AppState>) -> String {
    format!("Connected with max_connections: {}", state.config.max_connections)
}
 
async fn create_user(State(state): State<AppState>) -> String {
    // state.db_pool available here
    "User created".to_string()
}
 
// Create router with state:
fn create_app() -> Router {
    let state = AppState {
        db_pool: DbPool,
        config: Config { max_connections: 100 },
    };
    
    Router::new()
        .route("/users", get(get_user).post(create_user))
        .with_state(state)
}

State<T> extracts a clone of T from the router for each request.

State Type Requirements

use axum::extract::State;
 
// State MUST implement Clone:
#[derive(Clone)]
struct ValidState {
    data: String,
}
 
// This would NOT compile without Clone:
// struct InvalidState {
//     data: String,
// }
 
// Clone is required because State extracts by cloning:
async fn handler(State(state): State<ValidState>) {
    // state is a clone of the router's state
}
 
// For expensive-to-clone state, use Arc:
#[derive(Clone)]
struct ExpensiveState {
    db: Arc<Database>,  // Arc is cheap to clone
    cache: Arc<Cache>,
}
 
struct Database;
struct Cache;

All state types must implement Clone since State provides a clone to each handler.

Sharing Arc

use axum::extract::State;
use std::sync::Arc;
 
// Common pattern: Wrap state in Arc at the router level:
#[derive(Clone)]
struct AppState {
    db: Arc<Database>,
}
 
// Option 1: State is Arc<AppState>
async fn handler1(State(state): State<Arc<AppState>>) -> String {
    format!("State refcount: {}", Arc::strong_count(&state))
}
 
// Option 2: State is AppState (which contains Arc fields)
async fn handler2(State(state): State<AppState>) -> String {
    // state.db is already Arc
    "Handler 2".to_string()
}
 
fn create_app() -> Router {
    let state = Arc::new(AppState {
        db: Arc::new(Database),
    });
    
    Router::new()
        .route("/handler1", get(handler1))
        .route("/handler2", get(handler2))
        .with_state(state)
}

Both patterns work; choose based on whether handlers need the outer Arc.

Multiple State Types

use axum::extract::State;
 
// WRONG: Cannot have multiple with_state calls:
// Router::new()
//     .with_state(state1)
//     .with_state(state2)  // Error!
 
// RIGHT: Combine into single state type:
#[derive(Clone)]
struct AppState {
    db: Database,
    cache: Cache,
    config: Config,
}
 
// Extract and destructure:
async fn db_handler(State(state): State<AppState>) -> String {
    // Use state.db
    "Using database".to_string()
}
 
async fn config_handler(State(state): State<AppState>) -> String {
    // Use state.config
    format!("Config: {:?}", state.config)
}
 
// Or extract sub-state:
struct Database;
struct Cache;
struct Config;

Axum routers support exactly one state type; compose multiple resources into one struct.

State in Middleware

use axum::{
    extract::State,
    middleware::{self, Next},
    response::Response,
    Router,
};
use std::sync::Arc;
 
#[derive(Clone)]
struct AppState {
    request_count: Arc<std::sync::atomic::AtomicU64>,
}
 
async fn counter_middleware(
    State(state): State<AppState>,
    request: axum::extract::Request,
    next: Next,
) -> Response {
    state.request_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
    next.run(request).await
}
 
async fn get_count(State(state): State<AppState>) -> String {
    format!("Total requests: {}", state.request_count.load(std::sync::atomic::Ordering::Relaxed))
}
 
fn create_app() -> Router {
    let state = AppState {
        request_count: Arc::new(std::sync::atomic::AtomicU64::new(0)),
    };
    
    Router::new()
        .route("/count", get(get_count))
        .layer(middleware::from_fn_with_state(state.clone(), counter_middleware))
        .with_state(state)
}

Use from_fn_with_state to pass state to middleware.

Nested Routers with State

use axum::{
    extract::State,
    routing::get,
    Router,
};
 
#[derive(Clone)]
struct AppState {
    db: Database,
    api_keys: Vec<String>,
}
 
struct Database;
 
// Parent router with state:
fn create_app() -> Router {
    let state = AppState {
        db: Database,
        api_keys: vec!["key1".to_string()],
    };
    
    let api_routes = Router::new()
        .route("/users", get(list_users))
        .route("/keys", get(list_keys));
    
    Router::new()
        .nest("/api", api_routes)
        .with_state(state)
}
 
async fn list_users(State(state): State<AppState>) -> String {
    "Users list".to_string()
}
 
async fn list_keys(State(state): State<AppState>) -> String {
    format!("Keys: {:?}", state.api_keys)
}

Nested routers inherit the parent's state type.

State and Extension

use axum::{
    extract::{State, Extension},
    Router,
};
 
// State: Application-wide singleton
#[derive(Clone)]
struct AppState {
    db: Database,
}
 
// Extension: Request-specific data
struct RequestId(u64);
 
// Both can be extracted:
async fn handler(
    State(state): State<AppState>,
    Extension(request_id): Extension<RequestId>,
) -> String {
    format!("Request {} using {:?}", request_id.0, state.db)
}
 
struct Database;
 
fn create_app() -> Router {
    let state = AppState { db: Database };
    
    Router::new()
        .route("/handler", get(handler))
        .layer(axum::middleware::from_fn(|request, next| async {
            let request_id = RequestId(123);
            next.run(request).await
        }))
        .with_state(state)
}

Use State for app-wide data, Extension for request-scoped data.

Mutable State Pattern

use axum::extract::State;
use std::sync::Arc;
use tokio::sync::RwLock;
 
// For mutable state, use RwLock or Mutex:
#[derive(Clone)]
struct AppState {
    // RwLock allows concurrent readers:
    data: Arc<RwLock<Vec<String>>>,
    
    // Mutex for exclusive access:
    counter: Arc<tokio::sync::Mutex<u64>>,
}
 
async fn read_data(State(state): State<AppState>) -> Vec<String> {
    state.data.read().await.clone()
}
 
async fn write_data(
    State(state): State<AppState>,
    axum::Json(item): axum::Json<String>,
) {
    state.data.write().await.push(item);
}
 
async fn increment_counter(State(state): State<AppState>) -> u64 {
    let mut guard = state.counter.lock().await;
    *guard += 1;
    *guard
}

Wrap mutable data in RwLock or Mutex for thread-safe mutation.

Database Pool Pattern

use axum::extract::State;
use sqlx::PgPool;
 
#[derive(Clone)]
struct AppState {
    pool: PgPool,
}
 
async fn get_users(State(state): State<AppState>) -> Result<String, String> {
    let users: Vec<(i32, String)> = sqlx::query_as("SELECT id, name FROM users")
        .fetch_all(&state.pool)
        .await
        .map_err(|e| e.to_string())?;
    
    Ok(format!("Found {} users", users.len()))
}
 
async fn create_user(
    State(state): State<AppState>,
    axum::Json(name): axum::Json<String>,
) -> Result<String, String> {
    sqlx::query("INSERT INTO users (name) VALUES ($1)")
        .bind(&name)
        .execute(&state.pool)
        .await
        .map_err(|e| e.to_string())?;
    
    Ok("User created".to_string())
}

The common pattern stores database pools in state for connection reuse.

Configuration Pattern

use axum::extract::State;
use std::sync::Arc;
 
#[derive(Clone)]
struct AppConfig {
    database_url: String,
    api_key: String,
    max_connections: u32,
    debug_mode: bool,
}
 
#[derive(Clone)]
struct AppState {
    config: Arc<AppConfig>,
    db: Database,
}
 
struct Database;
 
async fn config_handler(State(state): State<AppState>) -> String {
    if state.config.debug_mode {
        format!("Debug mode: DB URL = {}", state.config.database_url)
    } else {
        "Production mode".to_string()
    }
}
 
fn create_app(config: AppConfig) -> Router {
    let state = AppState {
        config: Arc::new(config),
        db: Database,
    };
    
    Router::new()
        .route("/config", get(config_handler))
        .with_state(state)
}

Configuration values are typically stored as Arc fields for cheap cloning.

Type-Safe Substate Extraction

use axum::extract::State;
use std::sync::Arc;
 
// Pattern: Extract specific substate via FromRef:
#[derive(Clone)]
struct AppState {
    db: Arc<Database>,
    cache: Arc<Cache>,
    config: Arc<Config>,
}
 
struct Database;
struct Cache;
struct Config;
 
// Implement FromRef for substate extraction:
impl axum::extract::FromRef<AppState> for Arc<Database> {
    fn from_ref(state: &AppState) -> Self {
        state.db.clone()
    }
}
 
impl axum::extract::FromRef<AppState> for Arc<Cache> {
    fn from_ref(state: &AppState) -> Self {
        state.cache.clone()
    }
}
 
// Handler requests specific substate:
async fn db_handler(state: State<Arc<Database>>) -> String {
    "Using database directly".to_string()
}
 
// Or still use full state:
async fn full_handler(State(state): State<AppState>) -> String {
    "Using full state".to_string()
}

FromRef enables extracting specific fields without handlers knowing full state type.

FromRef Implementation

use axum::extract::{State, FromRef};
 
// FromRef trait for substate extraction:
// pub trait FromRef<T> {
//     fn from_ref(state: &T) -> Self;
// }
 
#[derive(Clone)]
struct AppState {
    db: Database,
    config: Config,
}
 
struct Database;
struct Config { port: u16 }
 
// Manual implementation:
impl FromRef<AppState> for Database {
    fn from_ref(state: &AppState) -> Self {
        state.db.clone()
    }
}
 
// Derive macro (axum::extract::FromRef derive):
// #[derive(Clone, FromRef)]
// struct AppState {
//     db: Database,
//     config: Config,
// }
// This automatically creates FromRef for each field
 
async fn handler(db: State<Database>) -> String {
    // Got Database directly without seeing AppState
    "Database handler".to_string()
}

Derive FromRef to automatically create substate extractors for each field.

State Lifecycle

use axum::extract::State;
use std::sync::Arc;
 
#[derive(Clone)]
struct AppState {
    id: u64,
}
 
// State is created once when building the router:
fn create_router() -> Router {
    let state = AppState { id: 42 };
    
    Router::new()
        .route("/", get(handler))
        .with_state(state)  // State moved into router
}
 
// State is cloned for each request:
async fn handler(State(state): State<AppState>) -> String {
    format!("State ID: {}", state.id)
}
 
// Alternative: Create state on startup:
async fn run_server() {
    let state = Arc::new(AppState { id: 42 });
    
    let app = Router::new()
        .route("/", get(handler))
        .with_state(state);
    
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

State lives for the entire server lifetime; handlers receive clones per request.

State with Layers

use axum::{
    extract::State,
    Router,
    middleware::{self, Next},
    routing::get,
};
use tower::ServiceBuilder;
use std::sync::Arc;
 
#[derive(Clone)]
struct AppState {
    db: Database,
}
 
struct Database;
 
async fn auth_layer(
    State(state): State<AppState>,
    request: axum::extract::Request,
    next: Next,
) -> Result<axum::response::Response, axum::response::StatusCode> {
    // Access state in layer
    let _db = &state.db;
    
    Ok(next.run(request).await)
}
 
async fn handler(State(state): State<AppState>) -> String {
    "Handler".to_string()
}
 
fn create_app() -> Router {
    let state = AppState { db: Database };
    
    Router::new()
        .route("/", get(handler))
        .layer(middleware::from_fn_with_state(state.clone(), auth_layer))
        .with_state(state)
}

Use from_fn_with_state to pass state to middleware layers.

State Cloning Strategy

use axum::extract::State;
use std::sync::Arc;
 
// Cheap to clone (recommendation):
#[derive(Clone)]
struct CheapState {
    db: Arc<PgPool>,      // Arc clone is cheap
    config: Arc<Config>,  // Arc clone is cheap
}
 
// Expensive to clone (avoid):
#[derive(Clone)]
struct ExpensiveState {
    data: Vec<String>,     // Vec clone is expensive
    map: HashMap<u64, String>,  // HashMap clone is expensive
}
 
// If you need expensive data:
#[derive(Clone)]
struct OptimizedState {
    data: Arc<Vec<String>>,  // Now cheap to clone
    map: Arc<HashMap<u64, String>>,  // Now cheap to clone
}
 
struct PgPool;
struct Config;

Design state types for cheap cloning by wrapping expensive fields in Arc.

Testing with State

use axum::{
    extract::State,
    routing::get,
    Router,
};
use tower::ServiceExt;  // For oneshot
 
#[derive(Clone, Debug)]
struct AppState {
    message: String,
}
 
async fn handler(State(state): State<AppState>) -> String {
    state.message
}
 
#[tokio::test]
async fn test_handler() {
    let state = AppState {
        message: "test message".to_string(),
    };
    
    let app = Router::new()
        .route("/test", get(handler))
        .with_state(state);
    
    let response = app
        .oneshot(axum::http::Request::builder()
            .uri("/test")
            .body(axum::body::Body::empty())
            .unwrap())
        .await
        .unwrap();
    
    let body = axum::body::to_bytes(response.into_body(), 1000).await.unwrap();
    assert_eq!(&body[..], b"test message");
}

Testing is straightforward: create state, build router, send request.

Real-World Example: Full Application State

use axum::{
    extract::State,
    routing::{get, post},
    Router,
    Json,
};
use std::sync::Arc;
use tokio::sync::RwLock;
 
// Complete application state:
#[derive(Clone)]
struct AppState {
    config: Arc<AppConfig>,
    db: Arc<Database>,
    cache: Arc<Cache>,
    metrics: Arc<Metrics>,
}
 
struct AppConfig {
    port: u16,
    max_connections: u32,
}
 
struct Database {
    pool: sqlx::PgPool,
}
 
struct Cache {
    data: RwLock<std::collections::HashMap<String, String>>,
}
 
struct Metrics {
    requests: std::sync::atomic::AtomicU64,
}
 
// Handlers use only what they need:
async fn health_check() -> &'static str {
    "OK"
}
 
async fn get_config(State(state): State<Arc<AppConfig>>) -> Json<AppConfig> {
    Json(AppConfig {
        port: state.port,
        max_connections: state.max_connections,
    })
}
 
async fn get_user(
    State(state): State<Arc<Database>>,
    path: axum::extract::Path<u64>,
) -> Result<Json<User>, axum::http::StatusCode> {
    // Use database
    Err(axum::http::StatusCode::NOT_FOUND)
}
 
async fn cache_stats(State(state): State<Arc<Cache>>) -> Json<CacheStats> {
    let data = state.data.read().await;
    Json(CacheStats {
        entries: data.len(),
    })
}
 
#[derive(serde::Serialize)]
struct User { id: u64, name: String }
 
#[derive(serde::Serialize)]
struct CacheStats { entries: usize }
 
fn create_app(state: AppState) -> Router {
    Router::new()
        .route("/health", get(health_check))
        .route("/config", get(get_config))
        .route("/user/:id", get(get_user))
        .route("/cache/stats", get(cache_stats))
        .with_state(state)
}

Production applications compose multiple services into a single state type.

Error Handling with State

use axum::{
    extract::State,
    response::{Result, IntoResponse},
    Json,
    http::StatusCode,
};
use std::sync::Arc;
 
#[derive(Clone)]
struct AppState {
    db: Arc<Database>,
}
 
struct Database;
 
enum AppError {
    DatabaseError(String),
    NotFound,
}
 
impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        match self {
            AppError::DatabaseError(msg) => {
                (StatusCode::INTERNAL_SERVER_ERROR, msg).into_response()
            }
            AppError::NotFound => {
                (StatusCode::NOT_FOUND, "Not found").into_response()
            }
        }
    }
}
 
async fn get_user(
    State(state): State<AppState>,
    axum::extract::Path(id): axum::extract::Path<u64>,
) -> Result<Json<User>, AppError> {
    // State is available for error handling
    let user = find_user(&state.db, id)
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::NotFound)?;
    
    Ok(Json(user))
}
 
async fn find_user(db: &Database, id: u64) -> Result<Option<User>, std::io::Error> {
    Ok(None)
}
 
#[derive(serde::Serialize)]
struct User { id: u64, name: String }

State is available in error handling code for logging, metrics, or cleanup.

Key Points

fn key_points() {
    // 1. State<T> extracts a clone of T from the router
    // 2. State type must implement Clone
    // 3. Wrap expensive-to-clone fields in Arc
    // 4. with_state() moves state into the router
    // 5. Exactly one state type per router
    // 6. Nested routers inherit parent state
    // 7. Use FromRef to extract substate
    // 8. State is created once, cloned per request
    // 9. Use RwLock/Mutex for mutable state
    // 10. middleware::from_fn_with_state passes state to middleware
    // 11. Arc<State> pattern for shared reference counting
    // 12. Testing is straightforward with oneshot
    // 13. State is available in middleware layers
    // 14. Combine multiple resources into one state struct
    // 15. State lifetime is the entire server runtime
}

Key insight: axum::extract::State solves the problem of sharing application-wide resources across handlers without globals or manual threading. The pattern is simple at its core: you define a Clone type containing your shared resources, pass it to Router::with_state, and handlers extract State<YourType> to receive a clone. The important design decision is the cloning strategy—since State clones for every request, expensive fields should be wrapped in Arc so cloning is just a reference count increment. For mutable state, wrap fields in RwLock (multiple concurrent readers) or Mutex (exclusive access). The FromRef trait enables a clean separation where handlers can request specific sub-resources (State<Arc<Database>>) without depending on the full AppState type, making handlers more reusable and testable. This architecture—type-safe extraction combined with Arc-wrapped shared state—is idiomatic Axum for production applications, providing both compile-time guarantees about available resources and efficient runtime sharing across concurrent requests.