Step 2 — Four Core Language Ideas
You do not need all of Rust to build a backend. Four ideas explain roughly 90% of the code you'll read and write. Learn these and Axum, SQLx, and Tokio stop feeling like magic.
1. Ownership & borrowing — "who is responsible for this value?"
Every value in Rust has exactly one owner. You either move it (hand ownership away) or borrow it (& for read-only access, &mut for exclusive write access).
let title = String::from("Write the docs");
// Borrow: the function reads `title` but doesn't take it.
print_len(&title);
println!("still mine: {title}"); // ✅ works — we only lent it out
// Move: the function takes ownership; `title` is gone afterwards.
consume(title);
// println!("{title}"); // ❌ compile error: value moved
Why a backend cares: a database connection pool is created once and shared by every request handler. You don't want each handler to own (and try to free) the pool — they borrow it:
async fn list_tasks(pool: &PgPool) -> Vec<Task> { /* ... */ }
// ^ borrow — "lend me the pool, I'll give it back"
When the borrow checker complains, it's almost always saying: "you used this after you gave it away." The fix is usually to borrow (&) instead of move, or to .clone() when the value is cheap to copy.
Arcis how you share safely. Shared server state (config, pools, caches) is wrapped in anArc<T>(atomically reference-counted pointer). Cloning anArcdoesn't copy the data — it bumps a counter — so every handler can cheaply hold its own handle to the same state.
2. Option and Result — no null, and errors are values
Rust has no null. Two enums replace it:
Option<T>= a value that might be absent:Some(x)orNone.Result<T, E>= an operation that might fail:Ok(x)orErr(e).
The compiler forces you to handle both arms — you cannot accidentally ignore an error or dereference "nothing."
fn find_task(id: i64, tasks: &[Task]) -> Option<&Task> {
tasks.iter().find(|t| t.id == id) // Some(task) or None
}
match find_task(42, &tasks) {
Some(task) => println!("found {}", task.title),
None => println!("no such task"),
}
The ? operator — early return on error
? is the workhorse of Rust backends. It means: "if this is Err (or None), return it from the current function right now; otherwise unwrap the success value and keep going."
async fn get_task(id: i64, pool: &PgPool) -> Result<Task, sqlx::Error> {
let task = sqlx::query_as::<_, Task>("SELECT * FROM tasks WHERE id = $1")
.bind(id)
.fetch_one(pool)
.await?; // ← if the query errors, this function returns that error
Ok(task)
}
Read ? as "unwrap-or-bubble-up." It turns nested error checks into a clean linear pipeline. It only works when the current function returns a compatible Result — which is why backend handlers almost always return Result.
3. Traits — Rust's version of interfaces
A trait is a set of behaviors a type can implement (like an interface in other languages). Axum is built almost entirely out of two traits:
IntoResponse — "anything that can become an HTTP response." A status code, a Json<T>, a tuple of both, or your own error type can all implement it, so handlers can return any of them uniformly.
Extractors (FromRequest / FromRequestParts) — "anything that can be pulled out of a request." This is why an Axum handler can just declare what it needs as parameters:
async fn create_task(
State(pool): State<PgPool>, // pulled from shared state
Json(body): Json<NewTask>, // parsed from the JSON request body
) -> impl IntoResponse { /* ... */ }
Each parameter type knows how to build itself from the request. That's dependency injection at compile time, with zero reflection. The #[derive(...)] you'll see on structs is the compiler generating a trait implementation for you (Serde's Serialize/Deserialize work this way).
4. async / await — one server, thousands of connections
A Rust backend does not spawn a thread per request. Instead, functions marked async return a future (a paused computation), and .await says: "suspend here until this I/O is ready, and let the runtime do other work meanwhile."
// Anything touching the network or disk is async and must be awaited.
let task = get_task(42, &pool).await?; // while this waits on Postgres,
// the same thread serves other requests
Tokio is the runtime that schedules all these futures across a small thread pool. The rule of thumb:
If it touches the network or disk, it's
asyncand you.awaitit — DB queries, HTTP calls, file reads. CPU-only work stays synchronous.
While one request awaits the database, that OS thread progresses hundreds of others. That's how a small container serves high concurrency.
You now have the whole toolkit
Ownership (who owns data), Result/Option + ? (errors as values), traits (the interface mechanism behind Axum), and async/await (concurrency). Every remaining step is these four ideas applied to a real problem.
Next: Step 3 — Project & Workspace Setup, where we scaffold the Cargo project our Tasks API will live in.