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
48: Building Web Servers with Actix or Axum
Think of a web server like a busy restaurant. When a customer walks through the door, they don't just wander into the kitchen and start cooking their own meal. That would be chaos. Instead, they meet a host—the router. The host looks at what the customer wants ("I'd like a table for two" or "I'm just picking up a takeout order") and directs them to the right place. If it's takeout, they go to the counter; if it's a full meal, they go to the dining room. The people actually preparing the food—the chefs—are your handler functions. They take a specific request, do the work, and send back a finished plate (the response).
In Rust, we have two heavy hitters for this: Actix-web and Axum. Actix is a powerhouse and has been around longer, but Axum is currently the darling of the community because it's built on top of tokio and tower, making it feel more like a natural extension of the async ecosystem. I'll focus on Axum here because its use of "extractors" is a pattern you'll see everywhere.
Routing Requests to Your Logic
In a basic server, the router is where you define your API's "menu." You tell the server: "If someone hits this specific URL path with this specific HTTP method, run this function."
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
// We build our "menu" here
let app = Router::new()
.route("/health", get(health_check))
.route("/books/:id", get(get_book));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn health_check() -> &'static str {
"Server is humming along nicely!"
}
async fn get_book(axum::extract::Path(id): axum::extract::Path<u32>) -> String {
format!("Fetching details for book ID: {}", id)
}
Notice that get_book doesn't just take a generic request object. It uses a Path extractor. This is a brilliant bit of Rust magic: Axum looks at the function arguments and says, "Oh, you need a u32 from the URL path? I'll parse that for you before I even call the function." If the user sends /books/abc instead of a number, Axum handles the error automatically and returns a 400 Bad Request. You don't have to write the boilerplate parsing code yourself.
Managing the Pantry with Shared State
A chef can't cook if the pantry is locked. In a web server, your "pantry" is your database connection or a shared cache. Since Rust is obsessed with memory safety, you can't just have a global variable that every request touches—that would be a data race nightmare.
The solution is to use Arc (Atomic Reference Counting) and often a Mutex or RwLock if the data needs to change. We wrap this in an AppState struct and "inject" it into the router.
use axum::{extract::State, routing::get, Router};
use std::sync::{Arc, RwLock};
use std::collections::HashMap;
struct AppState {
inventory: RwLock<HashMap<u32, String>>,
}
async fn get_book(
State(state): State<Arc<AppState>>,
axum::extract::Path(id): axum::extract::Path<u32>
) -> String {
let inventory = state.inventory.read().unwrap();
match inventory.get(&id) {
Some(book) => format!("Found: {}", book),
None => "Book not found in catalog".to_string(),
}
}
#[tokio::main]
async fn main() {
let shared_state = Arc::new(AppState {
inventory: RwLock::new(HashMap::from([
(1, "The Rust Programming Language".to_string()),
(2, "Programming Rust".to_string()),
])),
});
let app = Router::new()
.route("/books/:id", get(get_book))
.with_state(shared_state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
I prefer this pattern because it's explicit. You can see exactly what dependencies your handler needs just by looking at the function signature. If a handler needs the database, it asks for State. If it needs a user ID, it asks for Path. It makes testing these functions in isolation much easier because you can just pass in a mock state.
Choosing Between Actix and Axum
You might be wondering why you'd ever pick Actix over Axum. Actix-web is incredibly fast—often topping the Techempower benchmarks. It has its own actor system under the hood, which is powerful but adds a layer of conceptual complexity. Axum, on the other hand, is built on the tower ecosystem, meaning any middleware written for tower (like timeout, tracing, or rate-limiting) works out of the box. For 95% of projects, Axum's ergonomics and integration with the broader Tokio ecosystem make it the smoother choice.
📋 Practical Task
Build a Real-time Hardware Inventory Tracker
Your task is to build a small web server using Axum that tracks the stock levels of computer components. Your server needs to handle the following requirements:
- Shared State: Create an
AppStatestruct that holds aHashMap<String, u32>(where the key is the component name, like "GPU", and the value is the quantity in stock). This must be wrapped in anArcand aRwLock. - The GET Endpoint: Create a route
/stock/:item. If the item exists in the map, return the quantity (e.g., "GPU: 15 units"). If it doesn't exist, return a 404 Not Found response. - The POST Endpoint: Create a route
/restock. This endpoint should accept a JSON body (you'll need to useaxum::Json) containing the item name and the amount to add. It should update the shared state and return a success message.
Hint: You will need to add serde = { version = "1.0", features = ["derive"] } to your Cargo.toml to handle the JSON payload for the restock endpoint.
There are no comments for now.