Cookbook / Middleware and state

Shared state

Register database pools, clients and counters once and read them from any handler.

Register a value with app.state(..) and read it in any handler with req.state::<T>(). Values are shared across all requests and threads, so use interior mutability (Mutex, atomics) for anything that changes.

src/main.rs
use axeon::{Json, Server, ServerError};
use axeon::json;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;

struct Counters {
    visits: AtomicU64,
}

type Notes = Mutex<HashMap<u64, String>>;

fn main() {
    let mut app = Server::new();
    app.state(Counters { visits: AtomicU64::new(0) });
    app.state::<Notes>(Mutex::new(HashMap::new()));

    app.get("/visit", |req| async move {
        let counters = req.state::<Counters>().ok_or_else(|| ServerError::InternalError("missing state".into()))?;
        let n = counters.visits.fetch_add(1, Ordering::Relaxed) + 1;
        Ok::<_, ServerError>(Json(json!({ "visits": n })))
    });

    app.put("/notes/:id", |req| async move {
        let id: u64 = req.param("id")?;
        let text = req.text()?.to_string();
        let notes = req.state::<Notes>().unwrap();
        notes.lock().unwrap().insert(id, text);
        Ok::<_, ServerError>(()) // 204 No Content
    });

    app.get("/notes/:id", |req| async move {
        let id: u64 = req.param("id")?;
        let notes = req.state::<Notes>().unwrap();
        let note = notes.lock().unwrap().get(&id).cloned();
        Ok::<_, ServerError>(note) // None becomes 404
    });

    app.listen("127.0.0.1:3000").expect("server failed to start");
}

Notes#

  • State is looked up by type. Wrap two values of the same type in newtypes (struct ReadPool(PgPool)) to tell them apart.
  • Never hold a std::sync::Mutex guard across an .await. Copy what you need out, drop the guard, then await. For locks held across awaits use tokio::sync::Mutex.
  • Per-request data, such as the signed-in user, belongs in extensions, not state.