Loading pageâŚ
Rust walkthroughs
Loading pageâŚ
axum::Extension state differ from using axum::State for sharing application data?axum::Extension and axum::State are both mechanisms for sharing application data with handlers, but they differ fundamentally in type safety, performance, and intended use cases. State is the primary, type-safe mechanism where the state type is encoded directly in the router and extracted via State<S> extractorsâthe router owns the state and handlers receive references with the exact type. Extension uses a type-erased Extensions map internally, storing values as Box<dyn Any> and requiring downcasting at runtime. State should be your default choice for application state; Extension exists primarily for middleware-added data and legacy compatibility. The key trade-off is that State provides compile-time type guarantees and direct references, while Extension offers flexibility at the cost of runtime checks and reference counting.
use axum::{
Router,
extract::State,
routing::get,
response::IntoResponse,
};
use std::sync::Arc;
// Define your application state
#[derive(Clone)]
struct AppState {
db_pool: DbPool,
config: Config,
counter: Arc<atomic::AtomicU64>,
}
struct DbPool;
struct Config;
// Handler receives typed state via State extractor
async fn get_user(State(state): State<AppState>) -> impl IntoResponse {
// state is AppState, typed and ready to use
let count = state.counter.load(std::sync::atomic::Ordering::Relaxed);
format!("Count: {}", count)
}
async fn health_check(State(state): State<AppState>) -> impl IntoResponse {
// Same state type, same direct access
"OK"
}
#[tokio::main]
async fn main() {
let state = AppState {
db_pool: DbPool,
config: Config,
counter: Arc::new(atomic::AtomicU64::new(0)),
};
// State is passed to the router with .with_state()
let app = Router::new()
.route("/user", get(get_user))
.route("/health", get(health_check))
.with_state(state);
// Run the app
// axum::Server::bind(...).serve(app.into_make_service()).await
}State provides typed access to application state with compile-time guarantees.
use axum::{
Router,
Extension,
routing::get,
response::IntoResponse,
};
use std::sync::Arc;
#[derive(Clone)]
struct SharedData {
value: Arc<String>,
}
// Handler receives Extension via Extension extractor
async fn get_data(Extension(data): Extension<SharedData>) -> impl IntoResponse {
// data is SharedData, extracted from type-erased map
data.value.clone()
}
async fn get_data_ref(Extension(data): Extension<Arc<SharedData>>) -> impl IntoResponse {
// Can also use Arc directly
data.value.clone()
}
#[tokio::main]
async fn main() {
let shared = SharedData {
value: Arc::new("Hello".to_string()),
};
// Extension is added via .layer()
let app = Router::new()
.route("/data", get(get_data))
.layer(Extension(shared));
// Note: Extension uses tower::ServiceBuilder internally
}Extension wraps data in a type-erased map and requires runtime extraction.
use axum::{
Router,
extract::State,
Extension,
routing::get,
response::IntoResponse,
};
// === STATE: Compile-time type safety ===
#[derive(Clone)]
struct MyState {
value: String,
}
async fn state_handler(State(state): State<MyState>) -> impl IntoResponse {
// Compiler knows state is MyState
state.value.clone()
}
// This would NOT compile - wrong state type:
// async fn wrong_state(State(state): State<String>) -> impl IntoResponse {
// state
// }
// === EXTENSION: Runtime type checking ===
#[derive(Clone)]
struct MyExtension {
value: String,
}
async fn extension_handler(Extension(ext): Extension<MyExtension>) -> impl IntoResponse {
ext.value.clone()
}
// This WOULD compile but PANIC at runtime - wrong type:
// async fn wrong_extension(Extension(ext): Extension<String>) -> impl IntoResponse {
// ext
// }
// Panics with: "Extension extraction failed: no extension of type String found"State enforces types at compile time; Extension panics at runtime on type mismatch.
use axum::Extension;
use std::any::Any;
use std::sync::Arc;
// Simplified view of Extension internals
mod extension_internals {
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::sync::Arc;
// The actual Extensions type is a type-erased map
pub struct Extensions {
map: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
}
impl Extensions {
pub fn insert<T: Clone + Send + Sync + 'static>(&mut self, value: T) {
self.map.insert(TypeId::of::<T>(), Box::new(value));
}
pub fn get<T: Clone + Send + Sync + 'static>(&self) -> Option<&T> {
self.map
.get(&TypeId::of::<T>())
.and_then(|v| v.downcast_ref::<T>())
}
}
}
// Extension<T> extracts T from this map at runtime
// This requires:
// 1. TypeId lookup in HashMap
// 2. downcast_ref to convert Any to concrete type
// 3. Clone to return owned valueExtension uses type erasure with Box<dyn Any> and runtime downcasting.
use axum::extract::State;
// Simplified view of State mechanism
mod state_internals {
// The router stores the state directly with its type
// Handlers receive &S or S depending on Clone
// State extractor implementation (simplified)
pub struct State<S>(pub S);
// The extractor receives the state from the request extensions
// but the TYPE is known at compile time from the router
//
// Router::with_state::<S>(state) stores S in request
// State<S> extractor retrieves it with the known type
//
// Key: The state type S is part of the Router's type:
// Router<(), S> where S is the state type
}State maintains full type information throughout the request pipeline.
use axum::{extract::State, Extension, response::IntoResponse};
use std::sync::Arc;
use std::time::Instant;
#[derive(Clone)]
struct Data {
value: Arc<String>,
}
// State: Direct reference, no allocation
async fn state_handler(State(data): State<Data>) -> impl IntoResponse {
// Single pointer dereference
data.value.as_str()
}
// Extension: Type-erased lookup + downcast + clone
async fn extension_handler(Extension(data): Extension<Data>) -> impl IntoResponse {
// 1. HashMap lookup by TypeId
// 2. downcast_ref from Any
// 3. Clone (required by Extension)
data.value.as_str()
}
// Performance difference:
// - State: O(1), no allocation, no runtime checks
// - Extension: O(1) HashMap lookup + downcast + clone
//
// The difference is small per-request but measurable under load:
// - State has lower latency
// - State has better cache locality
// - Extension allocates on each extraction (clone)State is more efficient: no type erasure, no downcasting, no mandatory cloning.
use axum::{
Router,
extract::State,
Extension,
routing::get,
response::IntoResponse,
};
use std::sync::Arc;
#[derive(Clone)]
struct Database {
url: String,
}
#[derive(Clone)]
struct Cache {
addr: String,
}
#[derive(Clone)]
struct Config {
debug: bool,
}
// === Using State with combined struct ===
#[derive(Clone)]
struct AppState {
db: Database,
cache: Cache,
config: Config,
}
async fn state_db(State(state): State<AppState>) -> impl IntoResponse {
state.db.url.clone()
}
// === Using Extension for multiple types ===
async fn extension_db(Extension(db): Extension<Database>) -> impl IntoResponse {
db.url.clone()
}
async fn extension_cache(Extension(cache): Extension<Cache>) -> impl IntoResponse {
cache.addr.clone()
}
async fn extension_config(Extension(config): Extension<Config>) -> impl IntoResponse {
format!("Debug: {}", config.debug)
}
#[tokio::main]
async fn main() {
// State approach: single struct
let state = AppState {
db: Database { url: "postgres://...".to_string() },
cache: Cache { addr: "localhost:6379".to_string() },
config: Config { debug: true },
};
let state_app = Router::new()
.route("/db", get(state_db))
.with_state(state);
// Extension approach: multiple extensions
let ext_app = Router::new()
.route("/db", get(extension_db))
.route("/cache", get(extension_cache))
.route("/config", get(extension_config))
.layer(Extension(Database { url: "postgres://...".to_string() }))
.layer(Extension(Cache { addr: "localhost:6379".to_string() }))
.layer(Extension(Config { debug: true }));
}State uses a single struct; Extension allows multiple independent types.
use axum::{
Router,
Extension,
routing::get,
response::IntoResponse,
middleware::{self, Next},
http::Request,
};
use std::sync::Arc;
// Extension is ideal for middleware-added data
#[derive(Clone)]
struct RequestId(String);
async fn add_request_id<B>(
Extension(request_id): Extension<RequestId>,
mut request: Request<B>,
next: Next<B>,
) -> impl IntoResponse {
// Add request ID to request extensions for handlers
let id = uuid::Uuid::new_v4().to_string();
request.extensions_mut().insert(RequestId(id.clone()));
next.run(request).await
}
async fn handler(Extension(request_id): Extension<RequestId>) -> impl IntoResponse {
format!("Request ID: {}", request_id.0)
}
// This is where Extension shines: cross-cutting concerns
// added by middleware that handlers didn't anticipateExtension is the natural choice for middleware-injected data.
use axum::{
Router,
extract::State,
routing::get,
response::IntoResponse,
};
#[derive(Clone)]
struct AppState {
value: String,
}
async fn root_handler(State(state): State<AppState>) -> impl IntoResponse {
state.value.clone()
}
async fn api_handler(State(state): State<AppState>) -> impl IntoResponse {
state.value.clone()
}
#[tokio::main]
async fn main() {
let state = AppState {
value: "shared".to_string(),
};
// State flows to sub-routers automatically
let api_routes = Router::new()
.route("/users", get(api_handler));
let app = Router::new()
.route("/", get(root_handler))
.nest("/api", api_routes)
.with_state(state);
// Both root and api handlers receive the same AppState
}State naturally propagates to nested routers with the same type.
use axum::{
Router,
Extension,
routing::get,
response::IntoResponse,
};
#[derive(Clone)]
struct SharedData {
value: String,
}
async fn handler(Extension(data): Extension<SharedData>) -> impl IntoResponse {
data.value.clone()
}
#[tokio::main]
async fn main() {
let shared = SharedData {
value: "shared".to_string(),
};
// Extensions need to be added at the right layer level
let api_routes = Router::new()
.route("/users", get(handler));
let app = Router::new()
.route("/", get(handler))
.nest("/api", api_routes)
.layer(Extension(shared));
// Extension added here applies to ALL routes
// Layer ordering matters!
}Extension layers must be positioned carefully for proper scope.
use axum::{
Router,
extract::State,
routing::{get, post},
response::IntoResponse,
Json,
};
use std::sync::Arc;
use tokio::sync::RwLock;
// Use State for core application state
#[derive(Clone)]
struct AppState {
// Database connections
db: DbPool,
// Configuration
config: Arc<Config>,
// Shared mutable state
cache: Arc<RwLock<Cache>>,
// Business logic services
user_service: UserService,
}
struct DbPool;
struct Config;
struct Cache;
struct UserService;
// Handlers receive the full application state
async fn get_user(
State(state): State<AppState>,
) -> impl IntoResponse {
// All application context available
let db = &state.db;
let config = &state.config;
// ...
"user data"
}
async fn create_user(
State(state): State<AppState>,
Json(payload): Json<CreateUser>,
) -> impl IntoResponse {
// Same state, same type safety
"created"
}
struct CreateUser;
#[tokio::main]
async fn main() {
let state = AppState {
db: DbPool,
config: Arc::new(Config),
cache: Arc::new(RwLock::new(Cache)),
user_service: UserService,
};
let app = Router::new()
.route("/users", get(get_user))
.route("/users", post(create_user))
.with_state(state);
}Use State for primary application state that handlers depend on.
use axum::{
Router,
Extension,
routing::get,
response::IntoResponse,
middleware::{self, Next},
http::Request,
};
use std::sync::Arc;
// Use Extension for middleware-added data
// Request-scoped data added by middleware
#[derive(Clone)]
struct RequestContext {
request_id: String,
user_id: Option<String>,
start_time: std::time::Instant,
}
// Legacy integration where type isn't known at router build time
#[derive(Clone)]
struct LegacyData {
value: String,
}
async fn middleware_example<B>(
Extension(mut ctx): Extension<RequestContext>,
req: Request<B>,
next: Next<B>,
) -> impl IntoResponse {
// Modify context per-request
next.run(req).await
}
async fn handler(
Extension(ctx): Extension<RequestContext>,
Extension(legacy): Extension<LegacyData>,
) -> impl IntoResponse {
format!("Request: {}, Legacy: {}", ctx.request_id, legacy.value)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/handler", get(handler))
.layer(Extension(RequestContext {
request_id: String::new(),
user_id: None,
start_time: std::time::Instant::now(),
}))
.layer(Extension(LegacyData {
value: "legacy".to_string(),
}));
}Use Extension for middleware-injected data and cross-cutting concerns.
use axum::{
Router,
extract::State,
Extension,
routing::get,
response::IntoResponse,
};
use std::sync::Arc;
#[derive(Clone)]
struct MyData {
value: Arc<String>,
}
// State: Can receive reference or owned
async fn state_ref(State(data): State<MyData>) -> impl IntoResponse {
// data is MyData (cloned, but Arc clone is cheap)
data.value.as_str()
}
// Can also receive Arc directly for more control
#[derive(Clone)]
struct MyData2 {
value: Arc<String>,
}
async fn state_arc(State(data): State<Arc<MyData2>>) -> impl IntoResponse {
// data is Arc<MyData2>, single reference
data.value.as_str()
}
// Extension: Always requires Clone
async fn extension_handler(Extension(data): Extension<MyData>) -> impl IntoResponse {
// Extension extraction ALWAYS clones
// MyData must be Clone
data.value.as_str()
}
// If clone is expensive, wrap in Arc
#[derive(Clone)]
struct ExpensiveData {
big_data: Arc<Vec<u8>>,
}
async fn expensive_extension(
Extension(data): Extension<Arc<ExpensiveData>>
) -> impl IntoResponse {
// Arc clone is cheap
format!("{} bytes", data.big_data.len())
}Both require Clone, but State gives more control over the reference type.
use axum::{
Router,
extract::State,
Extension,
routing::get,
response::IntoResponse,
middleware::{self, Next},
http::Request,
};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
db: Database,
config: Config,
}
struct Database;
struct Config;
// Middleware adds per-request context
#[derive(Clone)]
struct RequestContext {
request_id: String,
}
async fn auth_middleware<B>(
State(state): State<AppState>,
Extension(ctx): Extension<RequestContext>,
req: Request<B>,
next: Next<B>,
) -> impl IntoResponse {
// Can use both State and Extension in middleware
next.run(req).await
}
// Handler uses both State and Extension
async fn handler(
State(state): State<AppState>,
Extension(ctx): Extension<RequestContext>,
) -> impl IntoResponse {
format!("Request {} using {:?}", ctx.request_id, state.db)
}
#[tokio::main]
async fn main() {
let state = AppState {
db: Database,
config: Config,
};
let app = Router::new()
.route("/api", get(handler))
.layer(Extension(RequestContext {
request_id: "default".to_string(),
}))
.with_state(state);
}State and Extension can be used together for different purposes.
use axum::{
Router,
extract::State,
Extension,
routing::get,
response::IntoResponse,
};
// BEFORE: Using Extension for everything
#[derive(Clone)]
struct Config {
db_url: String,
}
async fn old_handler(Extension(config): Extension<Config>) -> impl IntoResponse {
config.db_url.clone()
}
// AFTER: Migrate to State
#[derive(Clone)]
struct AppState {
config: Config,
}
async fn new_handler(State(state): State<AppState>) -> impl IntoResponse {
state.config.db_url.clone()
}
// Gradual migration: support both during transition
async fn transition_handler(
Extension(config): Extension<Config>,
State(state): State<AppState>,
) -> impl IntoResponse {
// Prefer State, fall back to Extension during migration
state.config.db_url.clone()
}
#[tokio::main]
async fn main() {
let state = AppState {
config: Config { db_url: "postgres://...".to_string() },
};
let app = Router::new()
.route("/new", get(new_handler))
.route("/transition", get(transition_handler))
.layer(Extension(state.config.clone())) // Still support Extension
.with_state(state);
}Gradual migration allows moving from Extension to State incrementally.
| Aspect | State | Extension |
|--------|---------|-------------|
| Type safety | Compile-time | Runtime (panics on mismatch) |
| Performance | Direct reference | HashMap lookup + downcast + clone |
| Ownership | Router owns, handlers reference | Shared via Clone |
| Use case | Application state | Middleware-added data |
| Multiple values | Single struct | Multiple Extensions |
| Router type | Router<(), S> | Any router |
| Propagation | Automatic to nested routers | Requires layer ordering |
| Error handling | Compile error | Runtime panic |
State and Extension serve different purposes in Axum:
Use State for:
Use Extension for:
Key insight: State is the idiomatic choice for application state because it encodes the state type in the router's type signature, enabling compile-time verification that handlers receive the correct type. Extension exists as a more flexible mechanism suitable for middleware-added data where the handler doesn't choose what it receives. The flexibility of Extension comes at a cost: runtime type checking, mandatory cloning, and the possibility of panics. For primary application state, prefer State; reserve Extension for cross-cutting middleware concerns.