Skip to Content
Course content

52: Practice Exercise: Building a Small REST API

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

You've spent the last few lessons learning about async Rust and how traits work. Now we're putting it all together. When I first started building APIs in Rust, I thought it would be as simple as creating a HashMap and passing it to my handlers. I was wrong. The compiler doesn't just "let things slide" when it comes to memory safety across threads, and that's exactly where most people hit a wall.

// This is the kind of code that looks right but won't compile
struct AppState {
    todos: HashMap<u64, String>,
}

async fn add_todo(
    State(state): State<AppState>, 
    Json(payload): Json<Todo>
) -> impl IntoResponse {
    state.todos.insert(payload.id, payload.text); // ERROR: cannot borrow `state.todos` as mutable
    StatusCode::CREATED
}

The "Cannot Borrow as Mutable" Wall

If you try to run the code above, Rust will give you a fit. The problem is that AppState is being shared across multiple threads (since axum or actix-web handle requests concurrently). By default, the State extractor gives you a shared reference. You can't just mutate a HashMap inside a shared reference because two different requests might try to write to that map at the exact same microsecond, which would cause a data race.

I remember staring at this error for an hour, wondering why I couldn't just make the state mut. But in a multi-threaded server, mut isn't enough. We need interior mutability and thread-safe pointer sharing.

Wrapping State for Thread-Safe Access

To fix this, we need to wrap our data in an Arc (Atomic Reference Counter) and a Mutex (Mutual Exclusion). The Arc allows multiple handlers to "own" the state, and the Mutex ensures that only one thread can actually touch the HashMap at a time.

use std::sync::{Arc, Mutex};
use tokio::sync::Mutex as AsyncMutex; // Use this for async-heavy apps

struct AppState {
    // We wrap the data we want to change in a Mutex
    db: Arc<Mutex<HashMap<u64, String>>>,
}

async fn add_todo(
    State(state): State<AppState>, 
    Json(payload): Json<Todo>
) -> impl IntoResponse {
    // We lock the mutex to get mutable access
    let mut db = state.db.lock().unwrap(); 
    db.insert(payload.id, payload.text);
    StatusCode::CREATED
}

By calling .lock().unwrap(), we're telling Rust: "Wait until no one else is using this map, then give me exclusive access." The unwrap() is there because if a thread panics while holding the lock, the mutex becomes "poisoned." In a production app, you might handle that more gracefully, but for a REST API, crashing the request is usually acceptable if your state is corrupted.

Structuring the API Flow

When you're building this out, keep your data structures separate from your API logic. I always recommend creating a models.rs for your structs and a handlers.rs for your logic. It keeps the main.rs from becoming a 500-line monster.

  • Serde is your best friend: Make sure your request and response structs derive Serialize and Deserialize. If you forget these, you'll get cryptic errors about types not implementing IntoResponse.
  • Avoid heavy locks: If your API gets a lot of traffic, a Mutex can become a bottleneck. That's when you'd look into RwLock, which allows multiple readers but only one writer.
  • Graceful extraction: Use the State extractor rather than trying to pass clones of your state into every single function manually. It's cleaner and more idiomatic.



📋 Practical Task

Project: Building a Collaborative Note-Taking Backend

Your task is to build a small REST API for a note-taking application. You will need to use axum and tokio.

Requirements:

  • Create a Note struct with an id (u64), title (String), and content (String).
  • Implement a shared AppState using Arc<Mutex<HashMap<u64, Note>>>.
  • Implement the following endpoints:
    • POST /notes: Adds a new note to the store.
    • GET /notes: Returns a list of all current notes.
    • GET /notes/:id: Returns a specific note by its ID. Return a 404 Not Found if the ID doesn't exist.
    • DELETE /notes/:id: Removes a note from the store.
  • Ensure all request and response bodies are handled as JSON.

Goal: Successfully handle concurrent requests (try sending multiple requests via curl or Postman) without the server panicking or the data becoming inconsistent.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.