Updated Aug 27, 2026

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 AppState as a Clone-able struct holding the database pool, config, and other shared resources
  • .with_state(state) registers state with the router
  • State<AppState> is an extractor — use it like Path or Json in handler signatures
  • PgPool is cheap to clone; wrap other expensive resources in Arc
  • 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.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy