axum-core-state
Overview
Axum delivers shared application state through one type parameter on Router
and one extractor. Router<S> means a router that is still missing a state
value of type S. Router::with_state(value) supplies that value and the
router type becomes Router<()>. Only Router<()> can be served. State
wiring is therefore checked entirely at compile time, never at runtime.
State APIs are identical across Axum 0.7 and 0.8: State, with_state,
FromRef, and the Router<S> model did not change at the 0.7 to 0.8
boundary. Every pattern in this skill applies to both versions.
Quick Reference
| Item | Signature | Purpose |
|---|---|---|
State<S> |
pub struct State<S>(pub S); |
FromRequestParts extractor that yields the state |
Router::with_state |
pub fn with_state<S2>(self, state: S) -> Router<S2> |
Supplies state, turns Router<S> into Router<()> |
Router::new |
pub fn new() -> Self |
Creates an empty router |
FromRef<T> |
fn from_ref(input: &T) -> Self; |
Reference-to-value conversion for substates |
#[derive(FromRef)] |
macro applied to the parent state struct | Generates one FromRef<Parent> impl per field |
Core rules:
- ALWAYS make application state
#[derive(Clone)]. Axum clones the state into every request. - ALWAYS wrap heavy or large state data in
Arcso the per-request clone is a refcount bump, not a deep copy. - ALWAYS use
State<S>for data known at startup (pools, config, caches). - NEVER use
Extension<T>for application-wide resources. A missingExtensioncompiles and fails as a runtime 500 on every request. - NEVER hold a
std::sync::MutexGuardacross an.awaitpoint. The handler future becomes!Sendand stops satisfyingHandler.
Decision Trees
State vs Extension
Is the value known when the server starts (pool, config, cache)?
YES: use State<S> with Router::with_state(value).
Wiring is checked at COMPILE time.
NO, the value is produced per request by middleware
(auth claims, authenticated user, request id):
use Extension<T> with .layer(Extension(value)).
Wiring is checked at RUNTIME.
NEVER reach for Extension<AppState> for app-wide resources. A forgotten or
mis-mounted Extension layer compiles fine and returns 500 on every request.
A forgotten with_state is a compile error caught before deployment.
Which mutex for mutable shared state
Does the critical section contain an .await point?
YES: use tokio::sync::Mutex.
Its guard is Send, lock() is async and yields.
NO (purely synchronous and short): std::sync::Mutex is fine and faster,
but ALWAYS scope the guard so it drops before any later .await.
The same split applies to RwLock: std::sync::RwLock vs
tokio::sync::RwLock, with the identical rule.
derive(FromRef) vs manual impl FromRef
Is each substate a plain field stored on the parent struct?
YES: use #[derive(FromRef)] on the parent (the default choice).
NO (the substate is computed, not stored): hand-write impl FromRef.
Patterns
Pattern: basic application state
State must implement Clone. A sqlx pool is itself a cheap Clone; wrap
heavy immutable data in Arc.
// axum 0.7 / 0.8 - identical
use axum::{Router, routing::get, extract::State};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
pool: sqlx::PgPool, // Pool is a cheap refcounted Clone, no Arc needed
config: Arc<AppConfig>, // heavy and immutable, Arc keeps the clone cheap
}
async fn handler(State(state): State<AppState>) -> String {
format!("connections: {}", state.pool.size())
}
let app: Router = Router::new()
.route("/", get(handler))
.with_state(AppState { pool, config: Arc::new(cfg) });
// Router<AppState> turns into Router<()>
Pattern: the Router typing model
Read Router<S> as a debt: the router still owes a value of type S.
// axum 0.7 / 0.8 - verbatim from the Router docs
let router: Router<AppState> = Router::new()
.route("/", get(|_: State<AppState>| async {}));
let router: Router<()> = router.with_state(AppState {});
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, router).await.unwrap();
A handler that takes State<AppState> makes its enclosing router
Router<AppState>. with_state pays the debt and produces Router<()>. Only
Router<()> has into_make_service and can be passed to axum::serve.
Pattern: FromRef substate composition
FromRef lets each handler depend only on the field it needs.
#[derive(FromRef)] generates the per-field impls automatically.
// axum 0.7 / 0.8 - identical
use axum::{Router, routing::get, extract::{State, FromRef}};
#[derive(Clone)]
struct ApiState { /* ... */ }
#[derive(Clone)]
struct DbState { pool: sqlx::PgPool }
#[derive(Clone, FromRef)] // generates FromRef<AppState> per field
struct AppState {
api: ApiState,
db: DbState,
}
async fn api_handler(State(api): State<ApiState>) { /* ... */ }
async fn db_handler(State(db): State<DbState>) { /* ... */ }
let app: Router = Router::new()
.route("/api", get(api_handler))
.route("/db", get(db_handler))
.with_state(AppState { api, db });
Every field type extracted as its own State<Field> MUST be Clone, because
from_ref returns it by value. #[derive(FromRef)] comes from axum-macros
and is re-exported as axum::extract::FromRef.
Pattern: mutable shared state
State is immutable-shared by default (it is cloned per request). For mutable
shared state use interior mutability, and choose the mutex by whether the lock
is held across an .await.
// axum 0.7 / 0.8 - CORRECT: tokio::sync::Mutex, guard is Send, lock() is async
use tokio::sync::Mutex;
use std::sync::Arc;
#[derive(Clone)]
struct AppState { data: Arc<Mutex<String>> }
async fn append(State(state): State<AppState>) {
let mut guard = state.data.lock().await; // async lock, Send guard
some_async_db_call().await; // fine: tokio guard is Send
guard.push_str("x");
}
See references/examples.md for the scoped std::sync::Mutex variant and the
failing !Send case.
Pattern: reading the missing-state compile error
Because state lives in the Router<S> type parameter, two mistakes produce a
compile-time error that mentions Handler and State together.
| Symptom in the error | Root cause | Fix |
|---|---|---|
expected Router<()>, found Router<AppState> and/or Handler<_, ()> is not satisfied |
.with_state(...) was never called |
Add .with_state(value) before axum::serve |
type mismatch naming AppState against another state type |
.with_state(WrongType) was called |
Pass the state type the handlers extract |
Whenever a confusing trait-bound error references Handler together with
State, the fix is almost always to add the missing .with_state(...) or to
correct the type passed to it. This is a deliberate design strength: the bug
is caught at compile time, never as a runtime failure.
Reference Links
references/methods.md: complete verified signatures forState,with_state,Router::new,FromRef, theFromRefblanket impl, and#[derive(FromRef)].references/examples.md: full version-annotated working code, including the manualimpl FromRef, both missing-state non-compiling cases, and the three mutable-state variants.references/anti-patterns.md: real mistakes with root-cause analysis, including theExtensionruntime-500 trap and thestd::sync::Mutexheld across.awaitfailure.
Related skills: axum-syntax-extractors (the full extractor set),
axum-impl-database (sharing a sqlx pool through state),
axum-errors-handler-trait (diagnosing the Handler is not satisfied error).