09. Shared State
📋 Jump to Takeaways🎁 Your database pool, config, and HTTP client are created once in main. Every handler needs them. Axum's state system passes them to handlers without global variables or function argument chains.
AppState
Define a struct that holds everything handlers need:
// src/state.rs
use sqlx::PgPool;
use crate::config::Config;
#[derive(Clone)]
pub struct AppState {
pub pool: PgPool,
pub config: Config,
}#[derive(Clone)] is required — Axum clones the state for each handler call. PgPool is already cheap to clone (it's an Arc internally). For expensive resources, wrap them in Arc yourself.
Registering State
Pass state to the router with .with_state():
use axum::{Router, routing::{get, post}};
let state = AppState {
pool: create_pool(&config.database_url).await,
config,
};
let app = Router::new()
.route("/health", get(health))
.route("/links", post(create_link))
.route("/links/:code", get(get_link))
.with_state(state);Extracting State in Handlers
Use the State extractor:
use axum::{extract::{State, Path}, Json};
use crate::{state::AppState, error::AppError};
pub async fn get_link(
State(state): State<AppState>,
Path(code): Path<String>,
) -> Result<Json<Link>, AppError> {
let link = find_by_code(&state.pool, &code)
.await?
.ok_or(AppError::NotFound)?;
Ok(Json(link))
}
pub async fn create_link(
State(state): State<AppState>,
Json(body): Json<CreateLink>,
) -> Result<(StatusCode, Json<Link>), AppError> {
let link = create(&state.pool, &body.url).await?;
Ok((StatusCode::CREATED, Json(link)))
}State(state): State<AppState> destructures the extractor — state is your AppState. Combine it with any other extractors in the same function signature.
Sharing Expensive Resources
PgPool clones cheaply because it's already Arc-wrapped. For other resources you want to share without copying, wrap in Arc:
use std::sync::Arc;
#[derive(Clone)]
pub struct AppState {
pub pool: PgPool,
pub http_client: Arc<reqwest::Client>, // shared, not cloned on each request
}Nested Routers with State
When splitting routes across modules, pass state to each sub-router:
mod links;
mod health;
let app = Router::new()
.merge(health::router(state.clone()))
.merge(links::router(state.clone()));// src/health.rs
pub fn router(state: AppState) -> Router {
Router::new()
.route("/health", get(health))
.with_state(state)
}
// src/links.rs
pub fn router(state: AppState) -> Router {
Router::new()
.route("/links", get(list).post(create))
.route("/links/:code", get(get_one).delete(delete_one))
.with_state(state)
}Each module owns its routes and its state. main.rs just merges them. Pass state.clone() to every sub-router — the AppState::Clone impl makes this cheap (the pool's Arc just increments a reference count).
Key Takeaways
- Define
AppStateas aClone-able struct holding the database pool, config, and other shared resources .with_state(state)registers state with the routerState<AppState>is an extractor — use it likePathorJsonin handler signaturesPgPoolis cheap to clone; wrap other expensive resources inArc- Split routes into modules with their own
router(state)function for clean organization
🎁 You've used Path, Query, Json, and State to pull data into handlers. But Axum has a whole system for this — and you can write your own extractors for things like authentication tokens and validated inputs. Next up: the Extractor trait.