Shared State
Pass databases, caches, and config to handlers using App::with_state and Arc-wrapped types.
App::with_state
App::with_state(S) creates an AppWithState<S> that stores your state behind an Arc<S> and shares it across all handlers and threads. The bound is S: Send + Sync + 'static.
use rust_web_server::app::App;
use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
use rust_web_server::range::Range;
use rust_web_server::mime_type::MimeType;
use rust_web_server::core::New;
struct Config {
version: &'static str,
}
let app = App::with_state(Config { version: "1.0.0" })
.get("/version", |_req, _params, _conn, cfg| {
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
r.content_range_list = vec![
Range::get_content_range(
cfg.version.as_bytes().to_vec(),
MimeType::TEXT_PLAIN.to_string(),
)
];
r
});Handler signature
fn handler(
request: &Request,
params: &PathParams,
connection: &ConnectionInfo,
state: &S,
) -> ResponseHandlers receive an immutable &S reference. The state type S itself is never cloned per-request — only the Arc<S> wrapping it is cloned once at registration time.
Outside a handler — for example in a test — app.state() returns the same &S.
routes! macro with state
The routes! macro works with any builder that has the .get() / .post() etc. methods, including AppWithState:
use rust_web_server::app::App;
use rust_web_server::routes;
use rust_web_server::request::Request;
use rust_web_server::router::PathParams;
use rust_web_server::server::ConnectionInfo;
use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
use rust_web_server::core::New;
struct AppState {
db_url: String,
}
fn get_items(_req: &Request, _p: &PathParams, _c: &ConnectionInfo, s: &AppState) -> Response {
let _ = &s.db_url; // use db_url to open a connection
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
r
}
let app = routes! {
App::with_state(AppState { db_url: "postgres://localhost/mydb".to_string() }),
GET "/items" => get_items,
};Mutable shared data
Because handlers receive &S (not &mut S), you need interior mutability for data that changes after startup. Two common patterns:
Arc<Mutex<T>> — exclusive write access
use std::sync::{Arc, Mutex};
use rust_web_server::app::App;
struct Counter {
hits: Arc<Mutex<u64>>,
}
let app = App::with_state(Counter { hits: Arc::new(Mutex::new(0)) })
.get("/count", |_req, _params, _conn, state| {
let mut hits = state.hits.lock().unwrap();
*hits += 1;
// build response with *hits ...
todo!()
});Arc<RwLock<T>> — concurrent reads, exclusive writes
Prefer RwLock when reads vastly outnumber writes (e.g., a shared cache or config snapshot):
use std::sync::{Arc, RwLock};
use std::collections::HashMap;
use rust_web_server::app::App;
struct Cache {
store: Arc<RwLock<HashMap<String, String>>>,
}
let app = App::with_state(Cache { store: Arc::new(RwLock::new(HashMap::new())) })
.get("/cache/:key", |_req, params, _conn, state| {
let key = params.get("key").unwrap_or("");
let guard = state.store.read().unwrap();
let value = guard.get(key).cloned().unwrap_or_default();
// build response with value ...
todo!()
});Accessing state from within state
If you store Arc-wrapped sub-components inside S, you can clone the inner Arc and spawn background tasks without coupling the lifetime to the request:
use std::sync::{Arc, Mutex};
struct JobQueue(Arc<Mutex<Vec<String>>>);
// Inside a handler:
// let queue = Arc::clone(&state.0);
// std::thread::spawn(move || { queue.lock().unwrap().push(job); });AsyncAppWithState — async handlers
When compiled with the http2 feature, App::with_async_state(S) returns an AsyncAppWithState<S> whose handlers are async fn:
use rust_web_server::async_state::AsyncAppWithState;
use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
use rust_web_server::core::New;
struct Db { url: String }
let app = AsyncAppWithState::new(Db { url: "postgres://localhost/mydb".to_string() })
.get("/users/:id", |_req, params, _conn, state| async move {
let id = params.get("id").unwrap_or("unknown");
// await a real async DB call here
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
r
});AsyncAppWithState requires --features http2 (or the default http3 feature). It is not available in the http1-only build because that build has no tokio runtime.
The async bridge automatically detects whether it is running inside an existing tokio runtime (HTTP/2 / HTTP/3 path) or in the synchronous thread-pool (HTTP/1.1 path) and adapts accordingly.
Unmatched requests
Routes are tried in registration order. Any request that does not match a registered route falls through to the built-in App controller chain (static files, /healthz, /readyz, /metrics, 404 Not Found).