Loading pageâŚ
Rust walkthroughs
Loading pageâŚ
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.