Just added: Algorithms you can run and practice
Updated Jul 10, 2026 Edit on GitHub

Managed State & Fairings

So far every handler in the books API has been an island. Phase 3 taught request guards (per-route inputs that can reject), Phase 4 taught responders (the shape of what you send back). A real service needs two more things: a place to keep shared stuff every handler touches - the book store, a database pool, a config value - and a way to run logic for every request, like logging or attaching a header. Rocket has one tool for each.

A shared dependency is managed state: register it once on the builder with .manage(value) and pull it into any handler through the &State<T> guard. A cross-cutting lifecycle behavior is a fairing: attach it once with .attach(...) and it runs on every request and/or response. State is "give me the thing." Fairings are "do this thing, always."

📝 This is the 🔴 advanced part of the guide because both features lean on Rust concepts you now need at the same time: shared references and interior mutability for state, and the async trait dance for fairings. Take it slowly; the payoff is that the books API stops being a toy.

Managed state: one source of truth

Until now our handlers had nowhere to keep the books between requests. Define a struct that holds the data, hand it to Rocket with .manage(...), and any handler can ask for it by adding a &State<AppState> parameter.

use rocket::serde::json::Json;
use rocket::State;
use std::collections::HashMap;
use std::sync::Mutex;

#[derive(Clone)]
struct Book {
    id: u32,
    title: String,
}

struct AppState {
    books: Mutex<HashMap<u32, Book>>,
}

#[get("/books")]
fn list(state: &State<AppState>) -> Json<Vec<String>> {
    let books = state.books.lock().unwrap();
    let titles = books.values().map(|b| b.title.clone()).collect();
    Json(titles)
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(AppState { books: Mutex::new(HashMap::new()) })
        .mount("/", routes![list])
}

What just happened: .manage(AppState { ... }) stored one AppState value inside Rocket's state registry, keyed by its type. The list handler asked for it by writing state: &State<AppState> - a request guard, exactly like Phase 3's, except this one never fails as long as you registered the type. &State<AppState> derefs to &AppState, so state.books reaches the field directly. Every request to /books sees the same AppState, so anything one handler writes, the next one reads.

The shared-reference catch

⚠️ Look closely at that signature: &State<AppState> is a shared (&) reference. Rocket may run handlers concurrently, so it can only ever hand you a read-only borrow of your state - there is no &mut State. That's why the books field is a Mutex<HashMap<...>> and not a bare HashMap. To change the data you need interior mutability: you take the lock, mutate through the guard, and the lock makes concurrent access safe.

#[post("/books", data = "<book>")]
fn add(book: Json<Book>, state: &State<AppState>) -> Json<Book> {
    let mut books = state.books.lock().unwrap();
    let stored = book.into_inner();
    books.insert(stored.id, stored.clone());
    Json(stored)
}

What just happened: even though state is a shared reference, state.books.lock() returns a MutexGuard we can treat as &mut HashMap, so insert mutates the real, shared map. The lock is held only for the body of this function and released when books drops. If two requests POST at once, one waits for the other's lock - no torn writes. For read-heavy workloads reach for RwLock instead of Mutex; the principle is the same.

💡 A Mutex<HashMap> is fine for a demo, not how you'd store real data. For a real database you manage a connection pool instead - typically rocket_db_pools, which sets up the pool as managed state and gives handlers a connection through the same &State-style guard. Same pattern; only the type inside changes.

You can manage more than one type

State is keyed by type, so you can .manage(...) several different values and ask for whichever ones a handler needs:

struct Config {
    site_name: String,
}

#[get("/about")]
fn about(config: &State<Config>) -> String {
    format!("Welcome to {}", config.site_name)
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(AppState { books: Mutex::new(HashMap::new()) })
        .manage(Config { site_name: "The Stacks".into() })
        .mount("/", routes![list, add, about])
}

What just happened: we registered two distinct types - AppState and Config. The about handler only needs the config, so it asks for just &State<Config>; list and add ask for &State<AppState>. Rocket matches each request by the type inside State<...>. ⚠️ The flip side of type-keying: if a handler asks for a type you never .managed, the guard fails and Rocket rejects the request (a 500 in dev), with a log line naming the missing type. The fix is always "you forgot to .manage it."

Fairings: middleware for the whole lifecycle

Managed state answers "what do my handlers share?" Fairings answer "what should happen on every request or response, regardless of which handler runs?" Logging each request, stamping a header onto every response, setting up a resource at startup, wiring CORS - none of that belongs in any single handler. Fairings are Rocket's middleware: callbacks that hook into the request/response lifecycle and run globally.

The full-power way implements the Fairing trait: an info() method (a name plus which lifecycle stages you want), then async callbacks for those stages - commonly on_request and on_response, with on_ignite/on_liftoff for startup work.

use rocket::{Request, Response, Data};
use rocket::fairing::{Fairing, Info, Kind};

struct RequestLogger;

#[rocket::async_trait]
impl Fairing for RequestLogger {
    fn info(&self) -> Info {
        Info { name: "Request logger", kind: Kind::Request | Kind::Response }
    }

    async fn on_request(&self, req: &mut Request<'_>, _data: &mut Data<'_>) {
        println!("--> {} {}", req.method(), req.uri());
    }

    async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) {
        println!("<-- {} for {}", res.status(), req.uri());
    }
}

What just happened: info() declares the fairing's name (shown in startup logs) and its Kind - here Request | Kind::Response, telling Rocket to call both callbacks. on_request fires before the matched handler runs and can read or tweak the incoming request; on_response fires after, with mutable access to the outgoing Response. #[rocket::async_trait] is what lets a trait have async fn methods. This single fairing now logs every request and response across the entire app - no handler had to opt in.

Ad-hoc fairings for the quick ones

Writing a whole struct + trait impl is overkill for one tiny hook. Rocket gives you ad-hoc fairings via AdHoc: pass a name and a closure, get a fairing back.

use rocket::fairing::AdHoc;

#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(AppState { books: Mutex::new(HashMap::new()) })
        .mount("/", routes![list, add])
        .attach(RequestLogger)
        .attach(AdHoc::on_response("Server header", |_req, res| Box::pin(async move {
            res.set_raw_header("X-Server", "Rocket");
        })))
}

What just happened: .attach(...) registers a fairing - both RequestLogger and the ad-hoc one - and Rocket runs them in attach order. AdHoc::on_response takes a name and a closure; because the closure is async, its body is wrapped in Box::pin(async move { ... }). This one stamps an X-Server: Rocket header onto every response, in about four lines. CORS works the same way and is the textbook fairing use case - attach a configured rocket_cors::Cors fairing once and every response gains the right headers, no per-handler code.

Guards or fairings? The deciding question

These two tools overlap enough to confuse, so here's the clean split.

💡 Request guards are per-route, type-driven, and can reject. They run only for the handler they're added to, and if one fails (bad token, missing state, malformed input) the request stops there. Use them for inputs and access control. Fairings are global lifecycle hooks that run for every request/response and don't gate individual routes. Use them for cross-cutting concerns - logging, response headers, CORS, startup initialization.

The litmus test: if the logic should be able to block one specific route, it's a guard (Phase 3). If it should happen everywhere and just observe or decorate, it's a fairing. Authentication that rejects unauthorized callers → guard. A header on every response → fairing. Reaching for a fairing to do per-route work means re-checking which route you're on inside a global hook - the signal you wanted a guard all along.

With shared state and lifecycle hooks in hand, the books API has a real spine. Next: full CRUD, and failing gracefully with error catchers.

Recap

  • Managed state is shared data registered once with .manage(value) and pulled into handlers via the &State<T> request guard. Every request sees the same value.
  • &State<T> is a shared reference, so changing the data needs interior mutability - a Mutex (or RwLock) field. Take the lock, mutate, release.
  • State is keyed by type: you can manage several distinct types, but asking for a type you never managed fails the request.
  • For real databases you manage a connection pool (e.g. rocket_db_pools) rather than a Mutex<HashMap> - same pattern, production-grade type.
  • Fairings are Rocket's middleware: implement the Fairing trait (info() + on_request/on_response, plus on_ignite/on_liftoff) or use ad-hoc fairings, and attach them with .attach(...). They run globally.
  • Guards = per-route, can reject; fairings = global hooks (logging, headers, CORS, init). Pick by whether the logic should block a single route.

Quick check

[
  {
    "q": "Why does writable managed state usually wrap its data in a Mutex?",
    "choices": ["Rocket requires every managed value to be a Mutex", "The &State<T> guard gives only a shared reference, so mutation needs interior mutability", "Mutex makes handlers run faster", "Without it the state would not be shared between requests"],
    "answer": 1,
    "explain": "Handlers receive &State<T>, a shared reference, and Rocket may run them concurrently. To mutate shared data safely you need interior mutability such as a Mutex or RwLock."
  },
  {
    "q": "A handler asks for &State<Config>, but you never called .manage(Config). What happens?",
    "choices": ["Rocket auto-creates a default Config", "It compiles but silently passes None", "The State guard fails and Rocket rejects the request", "The server refuses to start"],
    "answer": 2,
    "explain": "Managed state is keyed by type. Extracting a type you never managed makes the guard fail, so Rocket rejects that request (a 500 in dev) and logs the missing type."
  },
  {
    "q": "You want to add an X-Server header to every response your app sends. Which tool fits?",
    "choices": ["A request guard on each handler", "An ad-hoc fairing attached with .attach", "A new managed state value", "A custom responder per route"],
    "answer": 1,
    "explain": "A header on every response is a cross-cutting, global concern with no per-route gating - exactly what a fairing is for. AdHoc::on_response attached with .attach does it in a few lines."
  }
]

Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.

Check your understanding 3 questions

1. Why does writable managed state usually wrap its data in a Mutex?

2. A handler asks for &State<Config>, but you never called .manage(Config). What happens?

3. You want to add an X-Server header to every response your app sends. Which tool fits?