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.
use axum::{Router, routing::get, extract::State};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
pool: sqlx::PgPool,
config: Arc<AppConfig>,
}
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) });
Read Router<S> as a debt: the router still owes a value of type S.
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.
use axum::{Router, routing::get, extract::{State, FromRef}};
#[derive(Clone)]
struct ApiState { }
#[derive(Clone)]
struct DbState { pool: sqlx::PgPool }
#[derive(Clone, FromRef)]
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.
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;
some_async_db_call().await;
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 for State,
with_state, Router::new, FromRef, the FromRef blanket impl, and
#[derive(FromRef)].
references/examples.md: full version-annotated working code, including the
manual impl FromRef, both missing-state non-compiling cases, and the three
mutable-state variants.
references/anti-patterns.md: real mistakes with root-cause analysis,
including the Extension runtime-500 trap and the std::sync::Mutex held
across .await failure.
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).