How does axum::extract::Extension differ from State for application state sharing?

axum::extract::Extension and State provide two distinct mechanisms for sharing application state in Axum, with Extension designed for optional, additive data that can be added by middleware, while State provides required application state that must be explicitly passed to route handlers. The key difference is that Extension uses a type-based registry where handlers request optional extensions by type, and extensions can be added by layers without modifying handler signatures, whereas State uses generics on the router to propagate a single state type that handlers must declare in their signatures. Extension values are cloned on each request, making them suitable for cheaply-clonable configuration, while State values can be referenced without cloning, making them appropriate for shared resources like database pools.

Basic State Usage

use axum::{
    Router,
    extract::State,
    routing::get,
};
use std::sync::Arc;
 
struct AppState {
    db_pool: String, // In reality: sqlx::PgPool
    api_key: String,
}
 
async fn get_user(State(state): State<Arc<AppState>>) -> String {
    format!("User from pool: {}", state.db_pool)
}
 
#[tokio::main]
async fn main() {
    let state = Arc::new(AppState {
        db_pool: "postgres://localhost".to_string(),
        api_key: "secret".to_string(),
    });
    
    let app = Router::new()
        .route("/user", get(get_user))
        .with_state(state);
    
    // Server would be created here
}

State requires declaring the state type in the handler signature and is passed through with_state.

Basic Extension Usage

use axum::{
    Router,
    extract::Extension,
    routing::get,
};
use std::sync::Arc;
 
struct Config {
    app_name: String,
    max_connections: u32,
}
 
async fn get_status(Extension(config): Extension<Arc<Config>>) -> String {
    format!("App: {}, Max conn: {}", config.app_name, config.max_connections)
}
 
#[tokio::main]
async fn main() {
    let config = Arc::new(Config {
        app_name: "MyApp".to_string(),
        max_connections: 100,
    });
    
    let app = Router::new()
        .route("/status", get(get_status))
        .layer(Extension(config));
    
    // Extension is added via layer
}

Extension is added through middleware layers and extracted by type.

Required vs Optional Semantics

use axum::{
    Router,
    extract::{State, Extension},
    routing::get,
};
use std::sync::Arc;
 
struct RequiredState {
    database: String,
}
 
struct OptionalConfig {
    feature_enabled: bool,
}
 
// State handler - MUST receive state, compile error if missing
async fn with_state(State(state): State<Arc<RequiredState>>) -> String {
    format!("Database: {}", state.database)
}
 
// Extension handler - optional, returns Option<Extension<T>>
async fn with_extension(Extension(config): Extension<Arc<OptionalConfig>>) -> String {
    format!("Feature enabled: {}", config.feature_enabled)
}
 
// Extension that might not be present
async fn maybe_extension(Extension(config): Extension<Option<Arc<OptionalConfig>>>) -> String {
    match config {
        Some(c) => format!("Feature enabled: {}", c.feature_enabled),
        None => "No config provided".to_string(),
    }
}
 
#[tokio::main]
async fn main() {
    let state = Arc::new(RequiredState {
        database: "postgres".to_string(),
    });
    
    let config = Arc::new(OptionalConfig {
        feature_enabled: true,
    });
    
    let app = Router::new()
        .route("/state", get(with_state))
        .route("/extension", get(with_extension))
        .with_state(state)
        .layer(Extension(config));
}

State is required at compile time; Extension provides optional values that may not be present.

Cloning Behavior

use axum::{
    Router,
    extract::{State, Extension},
    routing::get,
};
use std::sync::Arc;
 
#[derive(Clone)]
struct SharedData {
    count: Arc<std::sync::atomic::AtomicU32>,
}
 
// State: no cloning by default
async fn state_handler(State(data): State<Arc<SharedData>>) -> String {
    // Arc is passed by reference, no clone
    format!("Count: {}", data.count.load(std::sync::atomic::Ordering::SeqCst))
}
 
// Extension: always clones
async fn extension_handler(Extension(data): Extension<SharedData>) -> String {
    // SharedData is CLONED for each request
    format!("Count: {}", data.count.load(std::sync::atomic::Ordering::SeqCst))
}
 
#[tokio::main]
async fn main() {
    let shared = Arc::new(SharedData {
        count: Arc::new(std::sync::atomic::AtomicU32::new(0)),
    });
    
    // State: Arc<SharedData>
    let state_router = Router::new()
        .route("/state", get(state_handler))
        .with_state(shared.clone());
    
    // Extension: SharedData (cloned)
    let extension_router = Router::new()
        .route("/extension", get(extension_handler))
        .layer(Extension(SharedData {
            count: Arc::new(std::sync::atomic::AtomicU32::new(0)),
        }));
}

Extension clones values for each request; State shares the same reference.

Multiple Values

use axum::{
    Router,
    extract::{State, Extension},
    routing::get,
};
use std::sync::Arc;
 
struct Database { url: String }
struct Cache { url: String }
struct Logger { level: String }
 
// Multiple Extensions: each extracted separately
async fn multi_extension(
    Extension(db): Extension<Arc<Database>>,
    Extension(cache): Extension<Arc<Cache>>,
    Extension(logger): Extension<Arc<Logger>>,
) -> String {
    format!("DB: {}, Cache: {}, Log: {}", db.url, cache.url, logger.level)
}
 
// Single State: combine into one struct
struct AppState {
    db: Database,
    cache: Cache,
    logger: Logger,
}
 
async fn multi_state(State(state): State<Arc<AppState>>) -> String {
    format!("DB: {}, Cache: {}, Log: {}", 
        state.db.url, state.cache.url, state.logger.level)
}
 
#[tokio::main]
async fn main() {
    let db = Arc::new(Database { url: "postgres".to_string() });
    let cache = Arc::new(Cache { url: "redis".to_string() });
    let logger = Arc::new(Logger { level: "info".to_string() });
    
    let ext_app = Router::new()
        .route("/ext", get(multi_extension))
        .layer(Extension(db))
        .layer(Extension(cache))
        .layer(Extension(logger));
    
    let state_app = Router::new()
        .route("/state", get(multi_state))
        .with_state(Arc::new(AppState {
            db: Database { url: "postgres".to_string() },
            cache: Cache { url: "redis".to_string() },
            logger: Logger { level: "info".to_string() },
        }));
}

Multiple Extension values can coexist by type; State is a single type.

Middleware Adding Extensions

use axum::{
    Router,
    extract::Extension,
    routing::get,
    middleware::{self, Next},
    response::Response,
    body::Body,
    http::Request,
};
use std::sync::Arc;
 
struct RequestId(String);
 
// Middleware adds extension
async fn add_request_id(
    request: Request<Body>,
    next: Next,
) -> Response {
    // Add request-specific data as extension
    let request_id = RequestId(uuid::Uuid::new_v4().to_string());
    
    let mut response = next.run(request).await;
    
    // Extensions can be added by middleware
    response.extensions_mut().insert(request_id);
    
    response
}
 
// Handler reads extension added by middleware
async fn get_request_id(Extension(request_id): Extension<RequestId>) -> String {
    format!("Request ID: {}", request_id.0)
}
 
#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/id", get(get_request_id))
        .layer(middleware::from_fn(add_request_id));
}

Middleware can add Extension values dynamically per request; State cannot be modified by middleware.

Type-Based Registry

use axum::{
    Router,
    extract::Extension,
    routing::get,
};
use std::sync::Arc;
 
// Different types stored separately
struct ApiKey(String);
struct RateLimit(u32);
struct FeatureFlags(std::collections::HashMap<String, bool>);
 
async fn handler(
    Extension(api_key): Extension<ApiKey>,
    Extension(rate_limit): Extension<RateLimit>,
    Extension(flags): Extension<Arc<FeatureFlags>>,
) -> String {
    format!("Key: {}, Limit: {}, Flags: {:?}", 
        api_key.0, rate_limit.0, flags.0)
}
 
#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/api", get(handler))
        .layer(Extension(ApiKey("secret".to_string())))
        .layer(Extension(RateLimit(100)))
        .layer(Extension(Arc::new(FeatureFlags(
            [("feature_a".to_string(), true)].into()
        ))));
    
    // Each type is stored separately in the extension registry
}

Extension uses type as key; only one value per type is stored.

State with Nested Routers

use axum::{
    Router,
    extract::State,
    routing::get,
};
use std::sync::Arc;
 
struct AppState {
    db: String,
}
 
async fn root_handler(State(state): State<Arc<AppState>>) -> String {
    format!("Root: {}", state.db)
}
 
async fn nested_handler(State(state): State<Arc<AppState>>) -> String {
    format!("Nested: {}", state.db)
}
 
#[tokio::main]
async fn main() {
    let state = Arc::new(AppState { db: "postgres".to_string() });
    
    // State must be propagated to nested routers
    let nested = Router::new()
        .route("/nested", get(nested_handler));
    
    let app = Router::new()
        .route("/", get(root_handler))
        .nest("/api", nested)
        .with_state(state);
    
    // State is shared across all routes
}

State is propagated automatically to nested routers when using with_state.

Extension with Nested Routers

use axum::{
    Router,
    extract::Extension,
    routing::get,
};
use std::sync::Arc;
 
struct Config {
    value: String,
}
 
async fn parent_handler(Extension(config): Extension<Arc<Config>>) -> String {
    format!("Parent: {}", config.value)
}
 
async fn child_handler(Extension(config): Extension<Arc<Config>>) -> String {
    format!("Child: {}", config.value)
}
 
#[tokio::main]
async fn main() {
    let config = Arc::new(Config { value: "shared".to_string() });
    
    // Extensions apply to nested routers if added at parent level
    let child = Router::new()
        .route("/child", get(child_handler));
    
    let app = Router::new()
        .route("/parent", get(parent_handler))
        .nest("/api", child)
        .layer(Extension(config));
    
    // Extension layer applies to all routes
}

Extension layers apply to all routes in the router tree when added at the parent level.

Generic State Handlers

use axum::{
    Router,
    extract::State,
    routing::get,
};
use std::sync::Arc;
 
// Handler can be generic over state
async fn generic_handler<S>(State(state): State<Arc<S>>) -> String
where
    S: std::fmt::Debug,
{
    format!("State: {:?}", state)
}
 
#[derive(Debug)]
struct StateA { name: String }
#[derive(Debug)]
struct StateB { name: String }
 
#[tokio::main]
async fn main() {
    let state_a = Arc::new(StateA { name: "A".to_string() });
    let state_b = Arc::new(StateB { name: "B".to_string() });
    
    // Generic handler works with different states
    let app_a = Router::new()
        .route("/", get(generic_handler::<StateA>))
        .with_state(state_a);
    
    let app_b = Router::new()
        .route("/", get(generic_handler::<StateB>))
        .with_state(state_b);
}

State handlers can be generic; Extension handlers must specify concrete types.

Compile-Time Guarantees

use axum::{
    Router,
    extract::State,
    routing::get,
};
use std::sync::Arc;
 
struct MyState {
    database: String,
}
 
// This handler requires MyState
async fn needs_state(State(state): State<Arc<MyState>>) -> String {
    state.database.clone()
}
 
#[tokio::main]
async fn main() {
    let state = Arc::new(MyState {
        database: "postgres".to_string(),
    });
    
    // This compiles: state is provided
    let working_app = Router::new()
        .route("/", get(needs_state))
        .with_state(state);
    
    // This would NOT compile:
    // let broken_app = Router::new()
    //     .route("/", get(needs_state));
    // Error: missing `.with_state()`
    
    // State provides compile-time guarantee that handlers receive required state
}

State enforces state presence at compile time; missing state causes compilation errors.

Extension Absence Handling

use axum::{
    Router,
    extract::Extension,
    routing::get,
};
use std::sync::Arc;
 
struct OptionalConfig {
    enabled: bool,
}
 
// Extension returns Option when type might not be present
async fn maybe_has_extension(
    Extension(config): Extension<Option<Arc<OptionalConfig>>>,
) -> String {
    match config {
        Some(c) => format!("Config present, enabled: {}", c.enabled),
        None => "Config not provided".to_string(),
    }
}
 
// Without Option, missing extension causes runtime error
async fn requires_extension(
    Extension(config): Extension<Arc<OptionalConfig>>,
) -> String {
    format!("Enabled: {}", config.enabled)
}
 
#[tokio::main]
async fn main() {
    // Without extension added
    let no_ext_app = Router::new()
        .route("/maybe", get(maybe_has_extension))
        .route("/requires", get(requires_extension));
    // "/maybe" returns "Config not provided"
    // "/requires" returns 500 error
    
    // With extension added
    let with_ext_app = Router::new()
        .route("/maybe", get(maybe_has_extension))
        .route("/requires", get(requires_extension))
        .layer(Extension(Arc::new(OptionalConfig { enabled: true })));
    // Both routes work
}

Extension can return Option<T> to handle missing values gracefully.

Performance Considerations

use axum::{
    Router,
    extract::{State, Extension},
    routing::get,
};
use std::sync::Arc;
 
// Heavy state: use State to avoid cloning
struct DatabasePool {
    connections: Vec<String>, // In reality: actual connection pool
}
 
// Light configuration: Extension is fine
struct AppConfig {
    max_items: u32,
    timeout_ms: u64,
}
 
async fn state_pool(State(pool): State<Arc<DatabasePool>>) -> usize {
    // No clone, just Arc reference
    pool.connections.len()
}
 
async fn extension_config(Extension(config): Extension<Arc<AppConfig>>) -> u32 {
    // Arc is cloned, but Arc::clone is cheap
    config.max_items
}
 
// Clone-heavy types: wrap in Arc for Extension
struct LargeData {
    data: Vec<u8>, // Could be large
}
 
async fn large_extension(Extension(data): Extension<Arc<LargeData>>) -> usize {
    // Arc clone is cheap even for large data
    data.data.len()
}
 
#[tokio::main]
async fn main() {
    let pool = Arc::new(DatabasePool {
        connections: vec
!["conn1".to_string(), "conn2".to_string()],
    });
    
    let config = Arc::new(AppConfig {
        max_items: 100,
        timeout_ms: 5000,
    });
    
    let app = Router::new()
        .route("/pool", get(state_pool))
        .route("/config", get(extension_config))
        .with_state(pool)
        .layer(Extension(config));
}

State avoids cloning overhead; Extension clones but Arc mitigates this.

Layer Ordering Effects

use axum::{
    Router,
    extract::Extension,
    routing::get,
};
use std::sync::Arc;
 
struct Config {
    name: String,
}
 
async fn handler(Extension(config): Extension<Arc<Config>>) -> String {
    config.name.clone()
}
 
#[tokio::main]
async fn main() {
    let config_v1 = Arc::new(Config { name: "v1".to_string() });
    let config_v2 = Arc::new(Config { name: "v2".to_string() });
    
    // Extensions are applied in order (innermost first)
    // Outer extensions shadow inner ones of same type
    
    let app = Router::new()
        .route("/", get(handler))
        .layer(Extension(config_v2))  // Outer: v2 shadows v1
        .layer(Extension(config_v1)); // Inner: v1 is shadowed
    
    // Handler sees config_v2 (last Extension layer)
}

Multiple Extension layers with the same type shadow earlier values.

Practical Patterns

use axum::{
    Router,
    extract::{State, Extension},
    routing::get,
    middleware::{self, Next},
    response::Response,
    body::Body,
    http::Request,
};
use std::sync::Arc;
 
// State: application-wide shared resources
struct AppState {
    db_pool: String, // Database pool
    redis: String,   // Cache client
}
 
// Extension: request-scoped data
struct User {
    id: u64,
    name: String,
}
 
struct RequestId(String);
 
// Middleware adds request-scoped extension
async fn auth_middleware(
    Extension(app_state): Extension<Arc<AppState>>,
    request: Request<Body>,
    next: Next,
) -> Response {
    // Use app_state for validation
    // Add user as extension for handlers
    let user = User { id: 1, name: "Alice".to_string() };
    
    let mut response = next.run(request).await;
    response.extensions_mut().insert(user);
    response
}
 
async fn protected_route(
    State(state): State<Arc<AppState>>,
    Extension(user): Extension<User>,
) -> String {
    format!("User {} accessing DB: {}", user.name, state.db_pool)
}
 
#[tokio::main]
async fn main() {
    let state = Arc::new(AppState {
        db_pool: "postgres".to_string(),
        redis: "redis".to_string(),
    });
    
    let app = Router::new()
        .route("/protected", get(protected_route))
        .layer(middleware::from_fn(auth_middleware))
        .layer(Extension(state.clone()))
        .with_state(state);
}

Pattern: State for long-lived shared resources; Extension for request-scoped data.

Synthesis

Key differences:

Aspect State Extension
Presence Required Optional
Registration with_state() .layer(Extension(...))
Extraction State<T> Extension<T>
Cloning No (reference) Yes (cloned)
Count One per router Multiple by type
Middleware modification No Yes
Compile-time check Yes No

When to use each:

Use Case Recommended
Database pools State
Application config State or Extension
Request IDs Extension
Authenticated users Extension
Feature flags Extension
Multiple services State (combined)

Performance implications:

Mechanism Overhead
State<Arc<T>> Arc reference (cheap)
Extension<T> where T: Clone Full clone per request
Extension<Arc<T>> Arc clone (cheap)

Key insight: axum::extract::Extension and State serve complementary roles in application state sharing. State is designed for required, application-wide state that must be present for handlers to function—database pools, cache clients, and configuration—providing compile-time guarantees through the type system. Extension is designed for optional, additive data that can be supplied by middleware layers—request IDs, authenticated users, and feature flags—enabling a layered architecture where outer layers enrich requests for inner handlers. The cloning behavior difference is significant: Extension clones values for each request, making Arc wrappers essential for expensive types, while State passes references directly. For most applications, use State for core application state that must be present, and Extension for request-scoped data added by middleware or optional configuration that handlers may or may not use.