What is the difference between axum::Extension and State for sharing application data across handlers?
axum::Extension stores shared data in a type-erased map using TypeId keys with Arc wrapping, while State passes typed data directly through the request context with zero overheadâmaking State more performant and type-safe but Extension more flexible for optional data and multiple data types. Both mechanisms allow sharing application state across handlers, but Extension uses middleware layers to insert data into a type map that handlers extract, whereas State is a direct typed parameter passed to handlers with compile-time guarantees. The choice depends on whether you need flexibility (multiple optional state types) or performance (single required state type).
Understanding State Sharing in Axum
use axum::{
Router,
Extension,
extract::State,
routing::get,
};
use std::sync::Arc;
// Both Extension and State allow sharing data with handlers
// Extension approach
async fn handler_with_extension(
Extension(db): Extension<Arc<Database>>,
) -> String {
format!("Database: {:?}", db)
}
// State approach
async fn handler_with_state(
State(db): State<Arc<Database>>,
) -> String {
format!("Database: {:?}", db)
}
#[derive(Debug)]
struct Database {
url: String,
}Both mechanisms inject shared data into handlers, but with different implementation strategies.
Extension: Type-Erased Storage
use axum::{Router, Extension, routing::get};
use std::sync::Arc;
async fn get_user(Extension(db): Extension<Arc<Database>>) -> String {
db.get_user()
}
async fn get_post(Extension(db): Extension<Arc<Database>>) -> String {
db.get_post()
}
#[tokio::main]
async fn main() {
let db = Arc::new(Database::new("postgres://..."));
// Extension adds data to a type map
let app = Router::new()
.route("/user", get(get_user))
.route("/post", get(get_post))
.layer(Extension(db)); // Added via middleware layer
// Extension uses type erasure internally
// Data is stored as: HashMap<TypeId, Arc<dyn Any>>
}
struct Database {
url: String,
}
impl Database {
fn new(url: &str) -> Self {
Database { url: url.to_string() }
}
fn get_user(&self) -> String {
"user data".to_string()
}
fn get_post(&self) -> String {
"post data".to_string()
}
}Extension stores data in a type-erased map, extracting by type at runtime.
State: Direct Typed State
use axum::{Router, extract::State, routing::get};
use std::sync::Arc;
async fn get_user(State(db): State<Arc<Database>>) -> String {
db.get_user()
}
async fn get_post(State(db): State<Arc<Database>>) -> String {
db.get_post()
}
#[tokio::main]
async fn main() {
let db = Arc::new(Database::new("postgres://..."));
// State is passed directly via with_state
let app = Router::new()
.route("/user", get(get_user))
.route("/post", get(get_post))
.with_state(db); // Typed state passed directly
}
// State has no type erasure - direct reference to typed dataState passes typed data directly to handlers without type erasure.
Performance Differences
use axum::{Extension, extract::State};
use std::sync::Arc;
// Extension overhead:
// 1. HashMap lookup by TypeId
// 2. Arc<dyn Any> downcast
// 3. Reference counting overhead
async fn extension_handler(Extension(db): Extension<Arc<Database>>) {
// Runtime lookup: request.extensions().get::<Arc<Database>>()
// Type cast: Arc<dyn Any> -> Arc<Database>
// Multiple pointer indirections
}
// State overhead:
// 1. Direct reference to state
// 2. No runtime lookup
// 3. No type erasure
async fn state_handler(State(db): State<Arc<Database>>) {
// Direct access: request.state::<Arc<Database>>()
// Or stored directly in router context
// Single pointer dereference
}
// State is essentially zero-cost abstraction
// Extension has measurable overhead per requestState avoids runtime type lookups and downcasting; Extension pays these costs on every request.
Multiple State Types with Extension
use axum::{Router, Extension, routing::get};
use std::sync::Arc;
// Extension supports multiple data types
async fn handler(
Extension(db): Extension<Arc<Database>>,
Extension(cache): Extension<Arc<Cache>>,
Extension(config): Extension<Arc<Config>>,
) -> String {
format!("db: {}, cache: {}, config: {}",
db.url, cache.name, config.debug)
}
#[tokio::main]
async fn main() {
let db = Arc::new(Database::new("postgres://..."));
let cache = Arc::new(Cache::new("redis://..."));
let config = Arc::new(Config::default());
// Multiple extensions - each type is stored separately
let app = Router::new()
.route("/", get(handler))
.layer(Extension(db))
.layer(Extension(cache))
.layer(Extension(config));
}
// Extension can have any number of different types
// Each type is keyed by TypeId in the internal mapExtension naturally supports multiple state types; each type is stored independently.
Single State Type with State
use axum::{Router, extract::State, routing::get};
use std::sync::Arc;
// State supports only ONE type per application
// But that type can contain multiple fields
struct AppState {
db: Arc<Database>,
cache: Arc<Cache>,
config: Arc<Config>,
}
async fn handler(
State(state): State<Arc<AppState>>,
) -> String {
format!("db: {}, cache: {}, config: {}",
state.db.url, state.cache.name, state.config.debug)
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState {
db: Arc::new(Database::new("postgres://...")),
cache: Arc::new(Cache::new("redis://...")),
config: Arc::new(Config::default()),
});
// Only one state type
let app = Router::new()
.route("/", get(handler))
.with_state(state);
}
// State approach: bundle multiple resources in one structState requires bundling multiple resources into a single struct type.
Compile-Time vs Runtime Errors
use axum::{Extension, extract::State, routing::get, Router};
use std::sync::Arc;
// Extension: Runtime errors if missing
async fn extension_handler(Extension(db): Extension<Arc<Database>>) -> String {
db.url.clone()
}
// If Extension is not added, handler panics at runtime:
// "Extension has not been set"
// State: Compile-time guarantee
async fn state_handler(State(db): State<Arc<Database>>) -> String {
db.url.clone()
}
// If State is not provided, router won't compile:
// "Router::into_make_service requires that the router has state"
// Example of compile-time error:
// let app = Router::new()
// .route("/", get(state_handler))
// // .with_state(db) // MISSING - won't compile!
// ;
// Error: the trait bound `Router<(), ()>: IntoMakeService<...>` is not satisfied
fn main() {
// Extension errors are runtime panics
// State errors are compile-time
// Extension: handler will panic if Extension not added
// State: won't compile if with_state not called
}State provides compile-time guarantees; Extension fails at runtime with panics.
Extracting State in Handlers
use axum::{Extension, extract::State, Json, routing::post};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
struct AppState {
db: Arc<Database>,
}
#[derive(Deserialize)]
struct CreateUser {
name: String,
}
#[derive(Serialize)]
struct User {
id: u64,
name: String,
}
// Extension extraction
async fn create_user_extension(
Extension(db): Extension<Arc<Database>>,
Json(payload): Json<CreateUser>,
) -> Json<User> {
let user = db.create_user(&payload.name);
Json(user)
}
// State extraction
async fn create_user_state(
State(db): State<Arc<Database>>,
Json(payload): Json<CreateUser>,
) -> Json<User> {
let user = db.create_user(&payload.name);
Json(user)
}
// State with bundled resources
async fn create_user_bundled(
State(state): State<Arc<AppState>>,
Json(payload): Json<CreateUser>,
) -> Json<User> {
let user = state.db.create_user(&payload.name);
Json(user)
}Both use extractor syntax; State gives direct typed access.
Layer Ordering with Extension
use axum::{Router, Extension, routing::get};
use tower::ServiceBuilder;
use std::sync::Arc;
// Extension is added via layers, which have ordering implications
async fn handler(
Extension(db): Extension<Arc<Database>>,
Extension(config): Extension<Arc<Config>>,
) -> String {
format!("db: {}, config: {}", db.url, config.debug)
}
#[tokio::main]
async fn main() {
let db = Arc::new(Database::new("postgres://..."));
let config = Arc::new(Config::default());
// Layer order matters! Extensions must be added before handlers
let app = Router::new()
.route("/", get(handler))
.layer(Extension(db)) // Added first
.layer(Extension(config)); // Added second
// Layers wrap the service in reverse order:
// config -> db -> handler
// Alternative: use ServiceBuilder for clarity
let app = Router::new()
.route("/", get(handler))
.layer(
ServiceBuilder::new()
.layer(Extension(db))
.layer(Extension(config))
);
}Extension uses layer ordering; careful attention to middleware order is required.
Optional State with Extension
use axum::{Extension, Json, routing::get, Router};
use std::sync::Arc;
// Extension can be optional
async fn optional_extension_handler(
Extension(db): Extension<Option<Arc<Database>>>,
) -> String {
match db {
Some(db) => format!("Database: {}", db.url),
None => "No database configured".to_string(),
}
}
// Or using Option in the layer
async fn handler_with_optional(
// Handler expects Extension, but we can add Option<T>
) -> String {
"result".to_string()
}
#[tokio::main]
async fn main() {
// Extension can be omitted entirely
let app1 = Router::new()
.route("/", get(optional_extension_handler))
// No Extension added - but handler expects it!
// This would panic at runtime
;
// Better: add explicit Option
let app2 = Router::new()
.route("/", get(|Extension(opt): Extension<Option<Arc<Database>>>| {
match opt {
Some(db) => format!("db: {}", db.url),
None => "no db".to_string(),
}
}))
.layer(Extension(None::<Arc<Database>>));
}Extension supports optional state; State is always required.
Nesting and Routers
use axum::{Router, Extension, extract::State, routing::get};
use std::sync::Arc;
struct AppState {
db: Arc<Database>,
}
// Extension propagates through nested routers automatically
fn create_nested_router_extension() -> Router {
let db = Arc::new(Database::new("postgres://..."));
let nested = Router::new()
.route("/inner", get(|Extension(db): Extension<Arc<Database>>| {
format!("nested db: {}", db.url)
}));
Router::new()
.nest("/api", nested)
.layer(Extension(db))
}
// State requires explicit passing to nested routers
fn create_nested_router_state() -> Router {
let db = Arc::new(Database::new("postgres://..."));
let state = Arc::new(AppState { db: db.clone() });
let nested = Router::new()
.route("/inner", get(|State(db): State<Arc<Database>>| {
format!("nested db: {}", db.url)
}))
.with_state(db); // Must pass state to nested router
Router::new()
.nest("/api", nested)
.with_state(state)
}Extension automatically propagates through nesting; State requires explicit state passing.
Mutable State Patterns
use axum::{Extension, extract::State, routing::post, Json};
use std::sync::Arc;
use tokio::sync::RwLock;
// Both Extension and State typically use Arc for shared ownership
// For mutable state, wrap in RwLock or Mutex
struct AppState {
db: Arc<Database>,
counter: Arc<RwLock<u64>>,
}
// Extension with mutable state
async fn increment_extension(
Extension(counter): Extension<Arc<RwLock<u64>>>,
) -> String {
let mut count = counter.write().await;
*count += 1;
format!("Count: {}", *count)
}
// State with mutable state
async fn increment_state(
State(counter): State<Arc<RwLock<u64>>>,
) -> String {
let mut count = counter.write().await;
*count += 1;
format!("Count: {}", *count)
}
// Bundled mutable state
async fn increment_bundled(
State(state): State<Arc<AppState>>,
) -> String {
let mut count = state.counter.write().await;
*count += 1;
format!("Count: {}", *count)
}Both approaches use Arc<RwLock<T>> for mutable state; State bundles multiple resources.
Middleware Access to State
use axum::{Extension, extract::State, middleware, routing::get, Router, Request, Next, response::Response};
use std::sync::Arc;
struct AppState {
db: Arc<Database>,
}
// Extension can be accessed in middleware
async fn extension_middleware(
Extension(db): Extension<Arc<Database>>,
request: Request,
next: Next,
) -> Response {
// Can use db in middleware
println!("Database URL: {}", db.url);
next.run(request).await
}
// State is harder to access in middleware
async fn state_middleware(
// State is not directly accessible in middleware
// Must use request extensions
request: Request,
next: Next,
) -> Response {
// Cannot easily access State here
// Would need to use Extension for middleware access
next.run(request).await
}
#[tokio::main]
async fn main() {
let db = Arc::new(Database::new("postgres://..."));
// Extension in middleware
let app_extension = Router::new()
.route("/", get(|Extension(db): Extension<Arc<Database>>| {
format!("db: {}", db.url)
}))
.layer(middleware::from_fn(extension_middleware))
.layer(Extension(db));
}Extension is easier to access in middleware; State requires workarounds.
When to Use Each Approach
use axum::{Extension, extract::State, Router};
use std::sync::Arc;
// Use Extension when:
// 1. Multiple independent state types needed
// 2. Optional state (some routes don't need all state)
// 3. Adding state to existing router without changing types
// 4. Middleware needs to access state
// 5. Gradual migration from older axum versions
// Use State when:
// 1. Single, required application state
// 2. Performance is critical
// 3. Type safety is important
// 4. New project with clean state structure
// 5. Compile-time guarantees desired
fn choose_approach() {
// Extension: Flexible, runtime-checked
let app_extension = Router::new()
.layer(Extension(Arc::new(Database::new("..."))))
.layer(Extension(Arc::new(Cache::new("..."))))
.layer(Extension(Arc::new(Config::default())));
// State: Performant, compile-time-checked
struct AppState {
db: Arc<Database>,
cache: Arc<Cache>,
config: Arc<Config>,
}
let state = Arc::new(AppState {
db: Arc::new(Database::new("...")),
cache: Arc::new(Cache::new("...")),
config: Arc::new(Config::default()),
});
let app_state = Router::new()
.with_state(state);
}Choose based on flexibility needs vs performance/type-safety requirements.
Migration from Extension to State
use axum::{Extension, extract::State, Router, routing::get};
use std::sync::Arc;
// Gradual migration from Extension to State
// Step 1: Create a combined state struct
struct AppState {
db: Arc<Database>,
cache: Arc<Cache>,
config: Arc<Config>,
}
// Step 2: Update handlers to use State
async fn handler_v2(State(state): State<Arc<AppState>>) -> String {
format!("db: {}", state.db.url)
}
// Step 3: Keep Extension handlers during migration
async fn handler_v1(
Extension(db): Extension<Arc<Database>>,
Extension(cache): Extension<Arc<Cache>>,
) -> String {
format!("db: {}", db.url)
}
// Step 4: Use both during transition
#[tokio::main]
async fn main() {
let state = Arc::new(AppState {
db: Arc::new(Database::new("...")),
cache: Arc::new(Cache::new("...")),
config: Arc::new(Config::default()),
});
let app = Router::new()
.route("/v1", get(handler_v1))
.route("/v2", get(handler_v2))
.layer(Extension(state.db.clone())) // Still needed for v1
.layer(Extension(state.cache.clone()))
.with_state(state); // Added for v2
}Migration is possible by supporting both approaches during transition.
Synthesis
Architecture comparison:
// Extension: Type-erased map storage
// Internal: HashMap<TypeId, Arc<dyn Any>>
let app = Router::new()
.layer(Extension(db)) // Inserts into type map
.layer(Extension(cache)); // Another entry in type map
// Handler extracts via Extension<T>
async fn handler(Extension(db): Extension<Arc<Database>>) {
// Runtime: lookup by TypeId, downcast Arc<dyn Any> -> Arc<Database>
}
// State: Direct typed storage
// Internal: stored in router context
let app = Router::new()
.with_state(state); // Single typed state
// Handler extracts via State<T>
async fn handler(State(state): State<Arc<AppState>>) {
// Direct access - no lookup, no downcast
}Key differences:
// Extension:
// + Multiple state types
// + Optional state support
// + Easy middleware access
// + Good for existing codebases
// - Runtime type lookup
// - Arc overhead per type
// - Panic if missing
// - Type erasure
// State:
// + Zero-cost abstraction
// + Compile-time type safety
// + Single reference to state
// + Required by router
// - Single state type
// - Bundle pattern needed for multiple resources
// - Harder middleware access
// - Requires struct for multiple resourcesChoosing between them:
// Use Extension for:
// - Multiple independent state types
// - Optional or conditional state
// - Middleware that needs state
// - Quick prototypes
// - Migration from older axum
// Use State for:
// - New production applications
// - Single, well-defined state
// - Performance-critical handlers
// - Type safety requirements
// - Compile-time guaranteesKey insight: Extension and State serve similar purposesâsharing application data across handlersâbut differ fundamentally in implementation and trade-offs. Extension stores data in a type-erased map using TypeId keys, allowing multiple independent state types to coexist and supporting optional state, but requiring runtime lookups and Arc overhead. State passes a single typed state directly through the request context, providing zero-cost access and compile-time guarantees, but requiring all state to be bundled into a single struct type. For most production applications, State is preferred for its type safety and performance; Extension is valuable for optional state, middleware access, and gradual migrations. The choice ultimately depends on whether you prioritize flexibility (Extension) or performance and type safety (State).
