Skip to content

Shared State

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,
) -> Response

Handlers 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.

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> — 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> — 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
});

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).