Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
52: Practice Exercise: Building a Small REST API
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
SerializeandDeserialize. If you forget these, you'll get cryptic errors about types not implementingIntoResponse. - Avoid heavy locks: If your API gets a lot of traffic, a
Mutexcan become a bottleneck. That's when you'd look intoRwLock, which allows multiple readers but only one writer. - Graceful extraction: Use the
Stateextractor 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
Notestruct with anid(u64),title(String), andcontent(String). - Implement a shared
AppStateusingArc<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 a404 Not Foundif 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.
There are no comments for now.