Skip to Content
Course content

169: State Sharing in Axum Applications

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're running a professional kitchen. You have five different chefs (your request handlers) all working on different orders simultaneously. Now, these chefs all need access to the same master recipe book and a shared spice rack. It wouldn't make sense for every single chef to carry their own identical copy of the recipe book or their own personal set of 50 spices; it's a waste of space and a nightmare to keep them all updated. Instead, you put the book and the spices in one central location where everyone can reach them.

In Axum, your "shared spice rack" is the State. But there's a catch: because Axum is asynchronous and multi-threaded, it's like having chefs who might try to grab the same spice jar at the exact same millisecond. Rust's compiler will stop you right there unless you provide a way to handle that concurrent access safely.

Defining the Shared Resource

To get started, you need a struct that holds everything your handlers need. I usually recommend wrapping this state in an Arc (Atomic Reference Counted pointer). Since Axum clones the state for every request, Arc allows all those clones to point to the same memory location rather than duplicating the data.

use std::sync::{Arc, Mutex};
use axum::{extract::State, routing::get, Router};

struct AppState {
    // We use Mutex because we want to change this value during runtime
    request_count: Mutex,
    app_name: String,
}

type SharedState = Arc;

Notice that app_name is just a String, but request_count is wrapped in a Mutex. If the data is read-only, you don't need the mutex. But the moment you want to increment a counter or update a cache, you need that lock to prevent data races.

Wiring the State into the Router

Once your state is defined, you have to tell your router about it. You don't pass the state into the handlers manually; instead, you use the with_state method on your Router. This effectively "injects" the spice rack into the kitchen.

#[tokio::main]
async fn main() {
    let shared_state = Arc::new(AppState {
        request_count: Mutex::new(0),
        app_name: "Mentorship API".to_string(),
    });

    let app = Router::new()
        .route("/", get(handler))
        .with_state(shared_state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Extracting State in Your Handlers

Now for the part where the "chefs" actually use the tools. Axum uses an extractor called State. You simply add it as an argument to your handler function, and Axum handles the heavy lifting of pulling the correct reference out of the router's state.

async fn handler(State(state): State) -> String {
    // Lock the mutex to get access to the counter
    let mut count = state.request_count.lock().unwrap();
    *count += 1;

    format!("Welcome to {}! You are visitor number {}.", state.app_name, count)
}

I've seen a lot of developers struggle here by forgetting that State<T> requires T to be Clone. That's exactly why we used Arc. Cloning an Arc doesn't clone the data inside; it just bumps the reference count, making it an extremely cheap operation that satisfies Axum's requirements.

A Note on Granularity

One piece of advice from the trenches: don't put everything in one giant Mutex. If you have ten different variables in one Mutex<AppState>, only one handler can access any of those variables at a time. This creates a bottleneck. Instead, put the Mutex around the specific fields that need to change, just like I did with request_count above. This allows one handler to read the app_name while another is updating the counter.




📋 Practical Task

Exercise: Building a Shared API Key Validator

Your goal is to create a small Axum application that manages a list of "authorized" API keys in shared state. Instead of a hard-coded list, the keys should be stored in a HashSet inside the state, allowing the application to track which keys are currently valid.

Requirements:

  • Create an AppState struct containing a HashSet<String> wrapped in a RwLock (since you'll be reading keys more often than writing them).
  • Implement a GET route /validate?key=your_key that checks if the provided key exists in the shared state.
  • Implement a POST route /add_key that takes a string and adds a new key to the shared state.
  • Use Arc to share the state across these two handlers.
  • Ensure the /validate handler returns a 200 OK if the key is found and a 403 Forbidden if it is not.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.