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.

Arc is how you share safely. Shared server state (config, pools, caches) is wrapped in an Arc<T> (atomically reference-counted pointer). Cloning an Arc doesn'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:

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 async and you .await it — 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.