Shared State
The problem: sharing data across handlers
Section titled “The problem: sharing data across handlers”Your handlers need access to shared resources — a database pool, config, a cache client. In Express you attach things to app.locals or pass them via closure. In NestJS, Dependency Injection wires them automatically. In Axum, you use the State extractor.
Express app.locals vs Axum State
Section titled “Express app.locals vs Axum State”// Express — attach to app.localsimport express from 'express';import { Pool } from 'pg';
const app = express();app.locals.db = new Pool({ connectionString: process.env.DATABASE_URL });
app.get('/users', async (req, res) => { const db: Pool = req.app.locals.db; // untyped, runtime cast const { rows } = await db.query('SELECT * FROM users'); res.json(rows);});use axum::{extract::State, routing::get, Json, Router};use sqlx::PgPool;use std::sync::Arc;
#[derive(Clone)]struct AppState { db: PgPool,}
async fn list_users( State(state): State<Arc<AppState>>,) -> Json<Vec<User>> { let users = sqlx::query_as!(User, "SELECT * FROM users") .fetch_all(&state.db) .await .unwrap(); Json(users)}
#[tokio::main]async fn main() { let db = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()) .await .unwrap();
let state = Arc::new(AppState { db });
let app = Router::new() .route("/users", get(list_users)) .with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap();}NestJS providers vs Axum State
Section titled “NestJS providers vs Axum State”In NestJS, services are singletons created by the IoC container and injected into controllers via the constructor. In Axum, you create the state once in main, wrap it in Arc, and attach it to the Router. Every handler that needs it declares State(state): State<Arc<AppState>>.
// NestJS@Injectable()export class UsersService { constructor(private readonly db: DatabaseService) {}
async findAll() { return this.db.query('SELECT * FROM users'); }}
@Controller('users')export class UsersController { constructor(private readonly usersService: UsersService) {}
@Get() findAll() { return this.usersService.findAll(); }}// Axum — no framework magic, just Arc + Stateuse axum::{extract::State, Json};use std::sync::Arc;
#[derive(Clone)]struct AppState { db: PgPool, config: AppConfig,}
async fn list_users( State(state): State<Arc<AppState>>,) -> Json<Vec<User>> { // state.db and state.config are available here todo!()}Why Arc?
Section titled “Why Arc?”Arc<T> is an atomically reference-counted smart pointer. Axum handlers run concurrently on multiple Tokio tasks. Without Arc, passing AppState to multiple handlers would require copying it. With Arc, all handlers hold a reference to the same allocation — no copies, no locks for read-only data.
// AppState must implement Clone so Axum can clone the Arc per handler#[derive(Clone)]struct AppState { db: PgPool, // PgPool is already Clone + Arc internally config: AppConfig, // derive Clone on AppConfig}If your state contains mutable data (e.g., an in-memory cache), wrap only the mutable part:
use std::sync::{Arc, RwLock};
#[derive(Clone)]struct AppState { db: PgPool, cache: Arc<RwLock<HashMap<String, String>>>,}Run locally —
Staterequires a running Axum router with.with_state(state). Usecargo runand call your endpoint withcurl.