DevToolBoxGRATIS
Blogg

Rust Programming Guide: Ownership, Borrowing, Traits, Async Tokio, and Web with Axum

16 min readby DevToolBox

TL;DR

Rust gives you C/C++ performance with memory safety guaranteed at compile time — no garbage collector, no null pointer exceptions, no data races. Learn ownership (each value has one owner), borrowing (&T for read, &mut T for write), and lifetimes (how long references are valid). Use Result<T,E> for recoverable errors and Option<T> for nullable values. For async web services use Axum or Actix-web on top of Tokio runtime. Cargo is the best-in-class package manager: cargo new, cargo build, cargo test, cargo doc.

Key Takeaways

  • Rust eliminates memory safety issues at compile time through the ownership system — no garbage collector, performance equivalent to C/C++.
  • Every value has exactly one owner; borrowing rules: either multiple &T or exactly one &mut T at a time — never both simultaneously.
  • Use Result<T,E> for recoverable errors and Option<T> for nullable values; the ? operator propagates errors automatically — avoid unwrap() in production.
  • Traits are Rust's polymorphism mechanism; generic bounds (T: Trait) give zero-cost static dispatch while dyn Trait enables runtime dynamic dispatch.
  • Tokio is the dominant async runtime; Axum and Actix-web are the top choices for building high-performance web APIs.
  • Cargo is a best-in-class package manager and build tool: cargo new, cargo build, cargo test, cargo clippy, and cargo doc cover the entire development workflow.

1. What Is Rust and Why Do Developers Love It?

Rust is a systems programming language started by Mozilla in 2010 and officially released in 2015. Its goal is to deliver C/C++-level performance while guaranteeing memory safety at compile time through a unique ownership system — no garbage collector required. Rust has topped Stack Overflow's "Most Loved Language" survey for nine consecutive years (2016–2024) and has been adopted by the Linux kernel, Windows, Android, Firefox, Cloudflare, AWS, Meta, Google, and many other major projects.

The core problem Rust solves: roughly 70% of security vulnerabilities in C/C++ codebases stem from memory safety issues — dangling pointers, buffer overflows, data races. Rust eliminates these at compile time rather than relying on runtime garbage collection or manual memory management.

// Your first Rust program
fn main() {
    // Variables are immutable by default
    let name = "Rust";
    let mut counter = 0;   // mut makes it mutable

    println!("Hello from {}!", name);

    // Loops, conditions, and expressions
    for i in 1..=5 {
        counter += i;
    }
    println!("Sum 1..5 = {}", counter);  // 15

    // Everything is an expression
    let result = if counter > 10 {
        "greater than 10"
    } else {
        "10 or less"
    };
    println!("Result: {}", result);
}
Rust vs C++ vs Go — Language Comparison
=========================================

Feature              Rust              C++              Go
-------              ----              ---              --
Memory Safety        Compile-time      Manual           GC (runtime)
Garbage Collector    No                No               Yes
Null Safety          Option<T>         Raw pointers     nil (runtime)
Concurrency Safety   Compile-time      Manual           Goroutines+GC
Performance          C-equivalent      C-equivalent     ~20-50% slower
Compile Speed        Slower            Medium           Very fast
Runtime              Zero-size         Zero-size        Small runtime
Error Handling       Result<T,E>       Exceptions/codes errors (multiple return)
Generics             Yes (monomorphic) Yes (templates)  Yes (1.18+)
Async/Await          Yes (Tokio)       Yes (C++20)      Goroutines
Package Manager      Cargo             vcpkg/Conan/...  Go modules
Learning Curve       Steep             Very steep       Gentle
Best For             Systems, WebAsm   Systems, games   Cloud, CLIs, APIs

Choose Rust when:
  - Memory safety is critical (security software, OS kernels)
  - Zero-cost abstractions with no GC pauses (game engines, real-time)
  - WebAssembly targets
  - Embedded systems or resource-constrained environments

Choose C++ when:
  - Existing large C++ codebase
  - Graphics/game engines (Unreal, etc.)
  - Maximum ecosystem maturity

Choose Go when:
  - Cloud-native microservices and CLIs
  - Team prefers simplicity over raw performance
  - Fast build times are a priority

2. Installing Rust and Creating Your First Project

Rust is installed through rustup, the toolchain manager, which handles multiple Rust versions (stable, beta, nightly) and cross-compilation targets. After installation, the rustc compiler, Cargo package manager, and standard library docs are all ready to use.

Installation and Toolchain Management

# Install rustup (macOS/Linux)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Windows: download rustup-init.exe from https://rustup.rs

# Verify installation
rustc --version    # rustc 1.76.0 (07dca489a 2024-02-04)
cargo --version    # cargo 1.76.0

# Update Rust
rustup update

# Install nightly for experimental features
rustup install nightly
rustup default nightly

# Add a compilation target (cross-compile)
rustup target add wasm32-unknown-unknown    # WebAssembly
rustup target add x86_64-pc-windows-gnu    # Windows from Linux

# Install useful tools
cargo install cargo-watch   # auto-rebuild on change
cargo install cargo-expand  # expand macros
cargo install cargo-audit   # security vulnerability audit
cargo install cargo-flamegraph  # profiling
rustup component add clippy     # linter
rustup component add rustfmt    # formatter

Cargo: The Package Manager and Build System

# Create a new binary project
cargo new my-project
cd my-project

# Create a new library project
cargo new my-lib --lib

# Project structure
my-project/
  Cargo.toml     # project manifest (like package.json)
  Cargo.lock     # exact dependency versions (commit to VCS for bins)
  src/
    main.rs      # entry point for binary
  tests/         # integration tests
  benches/       # benchmarks
  examples/      # example programs

# Build commands
cargo build               # debug build (fast compile, slow runtime)
cargo build --release     # optimized build (slow compile, fast runtime)
cargo run                 # build + run
cargo run --release       # optimized run
cargo test                # run all tests
cargo test my_function    # run tests matching a name
cargo doc --open          # build and open documentation
cargo clippy              # lint with Clippy
cargo fmt                 # format code
cargo check               # fast type-check without producing binary
cargo clean               # remove build artifacts

# Cargo.toml example
[package]
name = "my-project"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
reqwest = { version = "0.11", features = ["json"] }

[dev-dependencies]
mockall = "0.11"

[profile.release]
opt-level = 3
lto = true         # link-time optimization

3. Ownership, Borrowing, and Lifetimes

The ownership system is Rust's most unique feature and the key to understanding the language. It enables Rust to guarantee memory safety without a garbage collector, and is the primary challenge for beginners — the borrow checker rejects unsafe code at compile time.

Ownership Rules and Move Semantics

fn main() {
    // Rule 1: each value has one owner
    let s1 = String::from("hello");   // s1 owns the String

    // Rule 2: when owner goes out of scope, value is dropped
    {
        let s2 = String::from("world");
        println!("{}", s2);   // s2 valid here
    }  // s2 dropped here, memory freed automatically

    // Move semantics: ownership transfer
    let s3 = s1;             // s1 is MOVED to s3
    // println!("{}", s1);  // ERROR: s1 is no longer valid!
    println!("{}", s3);      // OK

    // Clone: explicit deep copy
    let s4 = s3.clone();
    println!("{} and {}", s3, s4);  // both valid

    // Copy types (stack-allocated primitives): implicit copy
    let x = 5;
    let y = x;               // x is COPIED, not moved
    println!("{} and {}", x, y);  // both valid
    // Types that implement Copy: i32, f64, bool, char, tuples of Copy types
}

// Ownership and functions
fn takes_ownership(s: String) {    // s moves into this function
    println!("{}", s);
}  // s is dropped here

fn gives_ownership() -> String {   // returns ownership to caller
    String::from("mine")
}

Borrowing and References

fn main() {
    let s = String::from("hello");

    // Immutable borrow: &T
    let len = calculate_length(&s);  // pass reference, not ownership
    println!("Length of s is: {}", len);  // s still valid here

    // Mutable borrow: &mut T
    let mut s2 = String::from("hello");
    change(&mut s2);
    println!("{}", s2);  // "hello, world"

    // Borrow rules demonstration
    let mut s3 = String::from("test");
    let r1 = &s3;      // immutable borrow 1 - OK
    let r2 = &s3;      // immutable borrow 2 - OK (multiple &T allowed)
    println!("{} {}", r1, r2);   // last use of r1, r2
    // r1 and r2 are no longer used after this point (NLL: Non-Lexical Lifetimes)
    let r3 = &mut s3;  // mutable borrow - OK (r1, r2 no longer active)
    r3.push_str(" done");
    // println!("{} {}", r1, r3); // ERROR: can't mix &T and &mut T
}

fn calculate_length(s: &String) -> usize {
    s.len()  // can read, cannot modify
}

fn change(s: &mut String) {
    s.push_str(", world");  // can modify through &mut
}

Lifetime Annotations

// Lifetime annotation: explicit when compiler can't infer
// 'a is a lifetime parameter ('a is conventional, any letter works)

// Without annotation this would fail — compiler doesn't know
// if the return value lives as long as x or y
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
    // 'a means: return value lives at least as long as the
    // shorter of x and y's lifetimes
}

// Struct with a reference field — requires lifetime annotation
struct Excerpt<'a> {
    text: &'a str,   // text must live as long as this struct
}

impl<'a> Excerpt<'a> {
    fn announce(&self, msg: &str) -> &str {
        println!("Attention: {}", msg);
        self.text  // lifetime elision: return has same lifetime as &self
    }
}

fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("xyz");
        result = longest(s1.as_str(), s2.as_str());
        println!("Longest: {}", result);  // OK: result used within s2's scope
    }
    // println!("{}", result);  // ERROR: s2 dropped, result might dangle
}

4. Structs, Enums, and Pattern Matching

Rust's enums are far more powerful than enums in most languages — each variant can carry different types and amounts of data, making them algebraic data types (ADTs). Combined with exhaustive pattern matching, they enable type-safe and highly expressive code.

Structs and Methods

// Named struct
#[derive(Debug, Clone)]  // auto-implement Debug and Clone traits
struct User {
    username: String,
    email: String,
    active: bool,
    login_count: u32,
}

// Tuple struct (named tuple)
struct Point(f64, f64, f64);
struct Color(u8, u8, u8);

// Unit struct (no fields, useful for traits)
struct AlwaysEqual;

// Implementing methods
impl User {
    // Associated function (no self): constructor
    fn new(username: String, email: String) -> Self {
        User {
            username,
            email,
            active: true,
            login_count: 0,
        }
    }

    // Method with immutable borrow
    fn is_active(&self) -> bool {
        self.active
    }

    // Method with mutable borrow
    fn deactivate(&mut self) {
        self.active = false;
    }

    // Method that consumes self
    fn into_email(self) -> String {
        self.email
    }
}

fn main() {
    let mut user = User::new(
        String::from("alice"),
        String::from("alice@example.com"),
    );
    println!("{:?}", user);   // Debug output
    user.deactivate();

    // Struct update syntax
    let user2 = User {
        email: String::from("bob@example.com"),
        ..user   // remaining fields from user (moved)
    };
}

Enums and Pattern Matching

// Enum with data in variants (ADT)
#[derive(Debug)]
enum Shape {
    Circle(f64),              // radius
    Rectangle(f64, f64),      // width, height
    Triangle { base: f64, height: f64 },  // named fields
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(r) => std::f64::consts::PI * r * r,
            Shape::Rectangle(w, h) => w * h,
            Shape::Triangle { base, height } => 0.5 * base * height,
        }
    }
}

// Message enum — common Rust pattern
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

// Pattern matching — must be exhaustive
fn process_message(msg: Message) {
    match msg {
        Message::Quit => println!("Quitting"),
        Message::Move { x, y } => println!("Move to ({}, {})", x, y),
        Message::Write(text) => println!("Write: {}", text),
        Message::ChangeColor(r, g, b) => println!("Color: rgb({},{},{})", r, g, b),
    }
}

// if let — match a single pattern
fn check_coin(coin: Option<u32>) {
    if let Some(value) = coin {
        println!("Got {} cents", value);
    } else {
        println!("No coin");
    }
}

// while let — loop until pattern fails
fn drain_stack(stack: &mut Vec<i32>) {
    while let Some(top) = stack.pop() {
        println!("{}", top);
    }
}

5. Error Handling with Result and Option

Rust has no exceptions. Error handling is done through two types: Option<T> for values that might not exist, and Result<T, E> for operations that might fail. This explicit error handling forces developers to address error paths at the point of writing, which is a major reason Rust programs are highly reliable.

use std::fs;
use std::num::ParseIntError;

// Option<T> — value might be absent
fn find_first_even(numbers: &[i32]) -> Option<i32> {
    numbers.iter().find(|&&n| n % 2 == 0).copied()
}

// Result<T, E> — operation might fail
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
    s.trim().parse::<i32>()
}

// ? operator — early return on error
fn read_and_parse(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?;   // returns Err if file missing
    let number = content.trim().parse::<i32>()?;  // returns Err if not a number
    Ok(number * 2)
}

// Handling Results with combinators
fn demonstrate_combinators() {
    // map: transform Ok value
    let doubled = parse_number("21").map(|n| n * 2);
    println!("{:?}", doubled);  // Ok(42)

    // and_then: chain fallible operations
    let result = parse_number("10")
        .and_then(|n| if n > 0 { Ok(n) } else { Err("0".parse::<i32>().unwrap_err()) });

    // unwrap_or: provide default value
    let val = parse_number("abc").unwrap_or(0);
    println!("{}", val);  // 0

    // unwrap_or_else: compute default lazily
    let val2 = parse_number("abc").unwrap_or_else(|e| {
        eprintln!("Parse error: {}", e);
        -1
    });

    // Option combinators
    let name: Option<&str> = Some("  alice  ");
    let trimmed = name.map(|s| s.trim());
    let upper = name.map(|s| s.trim()).map(|s| s.to_uppercase());

    // ok_or: convert Option to Result
    let result: Result<&str, &str> = name.ok_or("no name provided");
}

Custom Error Types and anyhow

use std::fmt;

// Custom error type — best practice for library code
#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    Custom(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {}", e),
            AppError::Parse(e) => write!(f, "Parse error: {}", e),
            AppError::Custom(msg) => write!(f, "Error: {}", msg),
        }
    }
}

// impl From<> enables automatic ? conversion
impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}

fn process() -> Result<(), AppError> {
    let content = std::fs::read_to_string("data.txt")?;  // auto-converts io::Error
    let num: i32 = content.trim().parse()?;  // auto-converts ParseIntError
    if num < 0 {
        return Err(AppError::Custom("number must be positive".into()));
    }
    Ok(())
}

// --- OR use anyhow for application code ---
// Cargo.toml: anyhow = "1"
use anyhow::{Context, Result, bail, anyhow};

fn process_with_anyhow(path: &str) -> Result<i32> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("Failed to read file: {}", path))?;
    let num: i32 = content.trim().parse()
        .context("File does not contain a valid integer")?;
    if num < 0 {
        bail!("number {} must be non-negative", num);
    }
    Ok(num)
}

6. Traits and Generics

Traits define shared behavior, similar to interfaces in other languages but with default implementations and operator overloading. Generics combined with trait bounds enable zero-cost abstractions: the compiler generates specialized code for each concrete type (monomorphization), with no runtime overhead.

// Define a trait
trait Summary {
    fn summarize_author(&self) -> String;  // required method

    // Default implementation
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

#[derive(Debug)]
struct Article { title: String, author: String, content: String }

impl Summary for Article {
    fn summarize_author(&self) -> String { self.author.clone() }

    // Override default implementation
    fn summarize(&self) -> String {
        format!("{}, by {} — {}", self.title, self.author, &self.content[..50])
    }
}

// Generics with trait bounds — static dispatch (zero cost)
fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

// Multiple bounds with + syntax
fn notify_debug<T: Summary + std::fmt::Debug>(item: &T) {
    println!("{:?}", item);
    println!("{}", item.summarize());
}

// Where clause for complex bounds
fn complex_function<T, U>(t: &T, u: &U) -> String
where
    T: std::fmt::Display + Clone,
    U: Summary + std::fmt::Debug,
{
    format!("{} {}", t, u.summarize())
}

// impl Trait syntax (sugar for generics in simple cases)
fn notify_impl(item: &impl Summary) {
    println!("{}", item.summarize());
}

// Return impl Trait — return type is opaque
fn make_summarizable() -> impl Summary {
    Article {
        title: String::from("Test"),
        author: String::from("Alice"),
        content: String::from("Content here"),
    }
}

Trait Objects — Dynamic Dispatch

// dyn Trait — runtime polymorphism via vtable (like virtual in C++)
struct Tweet { username: String, content: String }
impl Summary for Tweet {
    fn summarize_author(&self) -> String { format!("@{}", self.username) }
}

// Vec of different types sharing a trait
fn print_all(items: &[Box<dyn Summary>]) {
    for item in items {
        println!("{}", item.summarize());
    }
}

fn main() {
    let article = Box::new(Article {
        title: "Rust Rules".into(),
        author: "Alice".into(),
        content: "Lorem ipsum dolor sit amet consectetur adipiscing elit...".into(),
    });
    let tweet = Box::new(Tweet {
        username: "bob".into(),
        content: "Rust is amazing!".into(),
    });
    let items: Vec<Box<dyn Summary>> = vec![article, tweet];
    print_all(&items);
}

// Important standard library traits to implement:
// Clone      — explicit deep copy: #[derive(Clone)]
// Copy       — implicit bitwise copy (stack types): #[derive(Copy, Clone)]
// Debug      — {:?} formatting: #[derive(Debug)]
// Display    — {} formatting: impl fmt::Display
// Iterator   — .iter(), for loops: impl Iterator
// From/Into  — type conversions: impl From<T>
// PartialEq  — == operator: #[derive(PartialEq)]
// Hash       — use as HashMap key: #[derive(Hash, Eq, PartialEq)]

7. Closures and Iterators

Rust's iterators are lazy: calling adaptors like .map() and .filter() does not execute immediately — only when a consuming method (.collect(), .sum(), .for_each()) is called. This design makes chained operations as efficient as hand-written loops — the compiler can fully optimize away the iterator abstraction.

// Closures — anonymous functions that capture their environment
fn main() {
    // Basic closure syntax
    let add = |x, y| x + y;          // type inferred
    let square = |x: i32| -> i32 { x * x };
    println!("{}", add(3, 4));    // 7

    // Capture by reference (Fn)
    let threshold = 10;
    let is_big = |n: &i32| n > &threshold;  // captures threshold by ref

    // Capture by value (move)
    let text = String::from("hello");
    let contains_hello = move |s: &str| s.contains(&text);  // moves text
    // text is moved into closure, cannot use it here

    // Three closure traits:
    // Fn     — borrows immutably, can be called multiple times
    // FnMut  — borrows mutably, can be called multiple times
    // FnOnce — consumes captured values, called only once

    let mut count = 0;
    let mut increment = || { count += 1; count };  // FnMut
    println!("{}", increment());  // 1
    println!("{}", increment());  // 2
}
// Iterator methods — zero-cost abstractions
fn iterator_examples() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // map: transform each element
    let doubled: Vec<i32> = numbers.iter().map(|&n| n * 2).collect();

    // filter: keep elements matching predicate
    let evens: Vec<&i32> = numbers.iter().filter(|&&n| n % 2 == 0).collect();

    // filter_map: filter + map in one step
    let parsed: Vec<i32> = vec!["1", "two", "3", "four"]
        .iter()
        .filter_map(|s| s.parse().ok())
        .collect();

    // fold/reduce: accumulate a value
    let sum = numbers.iter().fold(0, |acc, &n| acc + n);  // 55
    let sum2: i32 = numbers.iter().sum();  // shorthand for sum

    // chain: concatenate iterators
    let a = vec![1, 2, 3];
    let b = vec![4, 5, 6];
    let combined: Vec<i32> = a.iter().chain(b.iter()).copied().collect();

    // zip: pair two iterators
    let pairs: Vec<(i32, i32)> = a.iter().zip(b.iter()).map(|(&x, &y)| (x, y)).collect();

    // enumerate: add index
    for (i, val) in numbers.iter().enumerate() {
        println!("Index {}: {}", i, val);
    }

    // flat_map: map then flatten
    let words = vec!["hello world", "foo bar"];
    let all_words: Vec<&str> = words.iter().flat_map(|s| s.split_whitespace()).collect();

    // take/skip: limit or offset
    let first_three: Vec<i32> = numbers.iter().take(3).copied().collect();
    let skip_first: Vec<i32> = numbers.iter().skip(7).copied().collect();

    // any/all: boolean queries
    let has_even = numbers.iter().any(|&n| n % 2 == 0);  // true
    let all_positive = numbers.iter().all(|&n| n > 0);   // true

    // max, min, count
    let max = numbers.iter().max();       // Some(10)
    let count = numbers.iter().count();   // 10
}

8. Async Rust: Tokio Runtime and async/await

Rust's async/await is language-level syntax sugar — the compiler transforms async functions into state machines (implementing the Future trait). Unlike JavaScript's async/await, Rust Futures are lazy: creating a Future does not execute anything; it must be polled by a runtime (like Tokio) to make progress. Tokio is the dominant async runtime with a multi-threaded work-stealing scheduler.

# Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use tokio::time::{sleep, Duration};
use tokio::sync::{mpsc, Mutex};
use std::sync::Arc;

// #[tokio::main] initializes the Tokio runtime
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Basic async/await
    let result = fetch_data("https://api.example.com/data").await?;
    println!("{:?}", result);

    // Concurrent tasks with tokio::spawn
    let handle1 = tokio::spawn(async {
        sleep(Duration::from_millis(100)).await;
        println!("Task 1 done");
        42
    });
    let handle2 = tokio::spawn(async {
        sleep(Duration::from_millis(50)).await;
        println!("Task 2 done");
        24
    });

    let (r1, r2) = tokio::join!(handle1, handle2);  // await both
    println!("Results: {} {}", r1?, r2?);

    // tokio::select! — race multiple futures
    tokio::select! {
        _ = sleep(Duration::from_millis(1000)) => println!("Timed out"),
        result = fetch_data("https://api.example.com") => {
            println!("Got result: {:?}", result);
        }
    }

    // Shared state with Arc<Mutex<T>>
    let counter = Arc::new(Mutex::new(0u32));
    let mut handles = vec![];
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        handles.push(tokio::spawn(async move {
            let mut lock = counter.lock().await;
            *lock += 1;
        }));
    }
    for h in handles { h.await?; }
    println!("Final count: {}", counter.lock().await);

    // Message passing with mpsc channels
    let (tx, mut rx) = mpsc::channel::<String>(32);
    tokio::spawn(async move {
        tx.send("hello".to_string()).await.unwrap();
        tx.send("world".to_string()).await.unwrap();
    });
    while let Some(msg) = rx.recv().await {
        println!("Received: {}", msg);
    }
    Ok(())
}

async fn fetch_data(url: &str) -> anyhow::Result<serde_json::Value> {
    let response = reqwest::get(url).await?;
    let json = response.json::<serde_json::Value>().await?;
    Ok(json)
}

9. Common Rust Data Structures: Vec, HashMap, and BTreeMap

The Rust standard library provides a carefully designed set of data structures. Vec<T> is the most commonly used dynamic array; HashMap<K, V> provides O(1) average lookup using SipHash (protection against hash flooding attacks); BTreeMap<K, V> maintains sorted key order with O(log n) lookup, suitable for scenarios requiring ordered iteration.

use std::collections::{HashMap, BTreeMap, HashSet, BTreeSet, VecDeque, BinaryHeap};

// Vec<T> — dynamic array (most common collection)
fn vec_examples() {
    let mut v: Vec<i32> = Vec::new();
    let v2 = vec![1, 2, 3, 4, 5];  // macro shorthand

    v.push(1);              // O(1) amortized
    v.pop();                // O(1)
    v.insert(0, 99);       // O(n)
    v.remove(0);           // O(n)
    v.retain(|&x| x > 0);  // keep elements matching predicate
    v.sort();               // in-place sort O(n log n)
    v.sort_by(|a, b| b.cmp(a));  // reverse sort
    v.dedup();             // remove consecutive duplicates (sort first)

    // Slices — view into a Vec
    let slice: &[i32] = &v2[1..3];  // [2, 3]
    let first = v2.first();   // Option<&i32>
    let last = v2.last();     // Option<&i32>
    let len = v2.len();
    let is_empty = v2.is_empty();

    // Pre-allocate capacity for performance
    let mut v3: Vec<i32> = Vec::with_capacity(1000);
    for i in 0..1000 { v3.push(i); }  // no reallocation
}
// HashMap<K, V> — O(1) average lookup
fn hashmap_examples() {
    let mut scores: HashMap<String, i32> = HashMap::new();

    // Insert
    scores.insert("Alice".to_string(), 100);
    scores.insert("Bob".to_string(), 85);

    // entry API — insert if not present
    scores.entry("Alice".to_string()).or_insert(0);
    scores.entry("Carol".to_string()).or_insert(0);  // inserts 0

    // entry with modification
    let count = scores.entry("Alice".to_string()).or_insert(0);
    *count += 10;  // now 110

    // Lookup
    let alice = scores.get("Alice");       // Option<&i32>
    let bob = scores["Bob"];               // panics if missing!
    let dave = scores.get("Dave").copied().unwrap_or(0);

    // Iterate
    for (name, score) in &scores {
        println!("{}: {}", name, score);
    }

    // Collect from iterator
    let map: HashMap<&str, i32> = vec![("a", 1), ("b", 2)]
        .into_iter()
        .collect();

    // Remove
    scores.remove("Bob");
    let contains = scores.contains_key("Alice");  // true
}

// BTreeMap<K, V> — sorted, O(log n) lookup
fn btreemap_examples() {
    let mut map: BTreeMap<&str, i32> = BTreeMap::new();
    map.insert("zebra", 1);
    map.insert("apple", 2);
    map.insert("mango", 3);

    // Iterates in sorted key order: apple, mango, zebra
    for (k, v) in &map { println!("{}: {}", k, v); }

    // Range queries — unique to BTreeMap
    use std::ops::Bound::Included;
    for (k, v) in map.range("a"..="m") {
        println!("{}: {}", k, v);  // apple, mango
    }
}
// Other important collections
fn other_collections() {
    // HashSet<T> — unordered unique values, O(1) lookup
    let mut set: HashSet<i32> = HashSet::new();
    set.insert(1); set.insert(2); set.insert(1);  // dedup: {1, 2}
    let contains = set.contains(&1);  // true
    let union: HashSet<_> = set.union(&HashSet::from([2, 3])).collect();
    let intersection: HashSet<_> = set.intersection(&HashSet::from([1, 3])).collect();

    // BTreeSet<T> — sorted unique values
    let bset: BTreeSet<i32> = vec![3, 1, 4, 1, 5].into_iter().collect();
    // Iterates in sorted order: 1, 3, 4, 5

    // VecDeque<T> — double-ended queue, O(1) push/pop on both ends
    let mut deque: VecDeque<i32> = VecDeque::new();
    deque.push_back(1);
    deque.push_front(0);
    let front = deque.pop_front();  // Some(0)
    let back = deque.pop_back();    // Some(1)

    // BinaryHeap<T> — max-heap, O(log n) insert/pop
    let mut heap: BinaryHeap<i32> = BinaryHeap::new();
    heap.push(3); heap.push(1); heap.push(4); heap.push(2);
    let max = heap.pop();  // Some(4)
}

10. Rust for the Web: Axum and Actix-web

Rust's web frameworks have matured significantly. Axum (maintained by the Tokio team) and Actix-web are the two dominant choices. Both rank near the top in TechEmpower framework benchmarks, far outperforming most frameworks in other languages.

Axum: Modern Web Framework Built on Tower

# Cargo.toml
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower-http = { version = "0.5", features = ["cors", "trace", "compression-gzip"] }
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-rustls", "uuid"] }
uuid = { version = "1", features = ["v4", "serde"] }
use axum::{
    extract::{Path, Query, State, Json},
    http::StatusCode,
    response::{IntoResponse, Response},
    routing::{get, post, put, delete},
    Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;

#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
    email: String,
}

#[derive(Debug, Clone, Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}

#[derive(Debug, Clone, Deserialize)]
struct PaginationQuery {
    page: Option<u32>,
    per_page: Option<u32>,
}

// Shared application state
type AppState = Arc<RwLock<Vec<User>>>;

#[tokio::main]
async fn main() {
    let state: AppState = Arc::new(RwLock::new(vec![
        User { id: 1, name: "Alice".into(), email: "alice@example.com".into() },
    ]));

    let app = Router::new()
        .route("/users", get(list_users).post(create_user))
        .route("/users/:id", get(get_user).put(update_user).delete(delete_user))
        .route("/health", get(|| async { "OK" }))
        .with_state(state)
        .layer(
            tower_http::cors::CorsLayer::permissive()
        );

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("Listening on http://0.0.0.0:3000");
    axum::serve(listener, app).await.unwrap();
}

// GET /users?page=1&per_page=10
async fn list_users(
    State(state): State<AppState>,
    Query(params): Query<PaginationQuery>,
) -> Json<Vec<User>> {
    let users = state.read().await;
    let page = params.page.unwrap_or(1) as usize;
    let per_page = params.per_page.unwrap_or(10) as usize;
    let start = (page - 1) * per_page;
    let paginated: Vec<User> = users.iter().skip(start).take(per_page).cloned().collect();
    Json(paginated)
}

// GET /users/:id
async fn get_user(
    State(state): State<AppState>,
    Path(id): Path<u32>,
) -> Result<Json<User>, StatusCode> {
    let users = state.read().await;
    users.iter()
        .find(|u| u.id == id)
        .cloned()
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}

// POST /users
async fn create_user(
    State(state): State<AppState>,
    Json(payload): Json<CreateUser>,
) -> (StatusCode, Json<User>) {
    let mut users = state.write().await;
    let id = users.len() as u32 + 1;
    let user = User { id, name: payload.name, email: payload.email };
    users.push(user.clone());
    (StatusCode::CREATED, Json(user))
}

// PUT /users/:id
async fn update_user(
    State(state): State<AppState>,
    Path(id): Path<u32>,
    Json(payload): Json<CreateUser>,
) -> Result<Json<User>, StatusCode> {
    let mut users = state.write().await;
    if let Some(user) = users.iter_mut().find(|u| u.id == id) {
        user.name = payload.name;
        user.email = payload.email;
        Ok(Json(user.clone()))
    } else {
        Err(StatusCode::NOT_FOUND)
    }
}

// DELETE /users/:id
async fn delete_user(
    State(state): State<AppState>,
    Path(id): Path<u32>,
) -> StatusCode {
    let mut users = state.write().await;
    let len_before = users.len();
    users.retain(|u| u.id != id);
    if users.len() < len_before { StatusCode::NO_CONTENT } else { StatusCode::NOT_FOUND }
}

Actix-web: High-Performance Actor-Model Web Framework

# Cargo.toml
[dependencies]
actix-web = "4"
actix-cors = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use actix_web::{
    web, App, HttpServer, HttpResponse, Responder,
    middleware::Logger,
};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone)]
struct Product {
    id: u32,
    name: String,
    price: f64,
}

#[derive(Deserialize)]
struct CreateProduct {
    name: String,
    price: f64,
}

async fn get_products() -> impl Responder {
    let products = vec![
        Product { id: 1, name: "Widget".into(), price: 9.99 },
    ];
    HttpResponse::Ok().json(products)
}

async fn create_product(body: web::Json<CreateProduct>) -> impl Responder {
    let product = Product {
        id: 1,
        name: body.name.clone(),
        price: body.price,
    };
    HttpResponse::Created().json(product)
}

async fn get_product_by_id(path: web::Path<u32>) -> impl Responder {
    let id = path.into_inner();
    HttpResponse::Ok().json(Product {
        id,
        name: format!("Product {}", id),
        price: 19.99,
    })
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap(Logger::default())
            .wrap(actix_cors::Cors::permissive())
            .service(
                web::scope("/api")
                    .route("/products", web::get().to(get_products))
                    .route("/products", web::post().to(create_product))
                    .route("/products/{id}", web::get().to(get_product_by_id))
            )
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

11. Testing in Rust: Unit Tests, Integration Tests, and Benchmarks

Rust has a built-in test framework requiring no additional dependencies. Unit tests live in a #[cfg(test)] module in the same file as the code under test and can access private functions. Integration tests live in the tests/ directory and can only access the public API. The cargo test command automatically discovers and runs all tests.

// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 { a + b }
pub fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 { Err("Division by zero".to_string()) }
    else { Ok(a / b) }
}

// Unit tests — same file, #[cfg(test)] module
#[cfg(test)]
mod tests {
    use super::*;   // import everything from parent module

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
        assert_eq!(add(-1, 1), 0);
        assert_ne!(add(1, 1), 3);
    }

    #[test]
    fn test_divide_success() {
        let result = divide(10.0, 2.0);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), 5.0);
    }

    #[test]
    fn test_divide_by_zero() {
        let result = divide(10.0, 0.0);
        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), "Division by zero");
    }

    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn test_panic() {
        let v = vec![1, 2, 3];
        let _ = v[99];   // intentional panic
    }

    #[test]
    #[ignore]   // run with: cargo test -- --ignored
    fn expensive_test() {
        // slow test skipped by default
    }
}

// Integration test: tests/integration_test.rs
// (separate file, can only use public API)
// use my_crate::{add, divide};
// #[test]
// fn test_public_api() {
//     assert_eq!(add(10, 20), 30);
// }

// Run specific tests
// cargo test               — run all tests
// cargo test test_add      — run tests matching "test_add"
// cargo test -- --nocapture  — show println! output
// cargo test -- --test-threads=1  — run single-threaded

Frequently Asked Questions

What is Rust's ownership system?

Rust's ownership system is the core mechanism for memory safety without a garbage collector. Three rules: 1) Each value has exactly one owner; 2) When the owner goes out of scope, the value is automatically dropped (freed); 3) At any given time a value can have either one mutable reference (&mut T) OR any number of immutable references (&T), but not both simultaneously. These rules are enforced at compile time, eliminating dangling pointers, double frees, and data races.

What is the difference between borrowing and references in Rust?

A reference (&T) is the syntactic form; borrowing is the semantic concept. An immutable borrow &T allows reading but not modification, and multiple immutable borrows can coexist. A mutable borrow &mut T allows reading and writing, but only one mutable borrow of the same data can exist in a given scope, and it cannot coexist with immutable borrows. The borrow checker validates at compile time that all references remain valid for their entire lifetime, preventing dangling references.

What are lifetimes and when do I need to annotate them?

Lifetimes describe the scope for which a reference remains valid, preventing dangling references. The Rust compiler uses lifetime elision rules to infer lifetimes in most cases automatically. Explicit annotation is required when: a function returns a reference (the return lifetime must be tied to a parameter lifetime), a struct holds reference fields, or implementing methods with references in impl blocks. Syntax: fn longest<'a>(x: &'a str, y: &'a str) -> &'a str — meaning the return value lives no longer than the shorter of x and y.

What is the difference between Result and Option in Rust?

Option<T> represents a value that may or may not exist — Some(T) or None — used for nullable values like hash map lookups or optional configuration. Result<T, E> represents an operation that may succeed (Ok(T)) or fail (Err(E)), used for error handling in file I/O, network requests, and parsing. The ? operator propagates errors or None automatically in functions returning Result or Option: it returns early on Err/None or unwraps the value to continue. Always prefer ? over unwrap() in production code to avoid panics.

How are Rust traits different from interfaces in other languages?

Rust traits are similar to interfaces but more powerful. Key differences: 1) Traits can have default method implementations; 2) You can implement your own trait for an external type (orphan rule: either the trait or the type must be defined in the current crate); 3) Trait objects (dyn Trait) support runtime polymorphism while generic bounds (T: Trait) provide zero-cost static dispatch; 4) Traits can be used as function parameters (impl Trait syntax) or return types. Important standard library traits include Clone, Copy, Debug, Display, Iterator, From/Into, and Serialize/Deserialize (via serde).

What is Tokio and how do I write async code in Rust?

Tokio is Rust's most popular async runtime, providing async I/O, task scheduling, timers, and network primitives. Rust's async/await syntax generates state machines (Futures), and the Tokio runtime polls and drives these Futures. Key concepts: async fn returns impl Future; .await suspends the current task until the Future completes; tokio::spawn creates concurrent tasks (similar to goroutines); tokio::select! waits on multiple Futures and takes the first to complete. Use the #[tokio::main] macro to initialize the runtime and run the async main function.

What advantages does Rust have over C++?

Rust's main advantages over C++: 1) Memory safety: the compiler eliminates dangling pointers, buffer overflows, and data races through ownership and borrow checking, whereas C++ relies on programmer discipline; 2) Concurrency safety: Send and Sync traits make data races impossible at compile time; 3) Better tooling: Cargo provides a unified package manager, build system, and test runner; 4) Superior error messages: Rust's compiler pinpoints exact problems with actionable suggestions; 5) No undefined behavior in safe Rust. Performance is comparable — both support zero-cost abstractions — but C++ has a more mature ecosystem and larger legacy codebase.

How do I build a web API in Rust?

The mainstream choices are Axum or Actix-web. Axum (tokio-rs/axum) is built on the Tower middleware ecosystem and Tokio, using an Extractor pattern to parse requests (Path, Query, Json), with routing defined via Router::new().route() — ideal for teams familiar with the Tower ecosystem. Actix-web is a high-performance actor-model framework that consistently ranks near the top in benchmarks, with an API style closer to traditional web frameworks. Both use serde for JSON serialization, sqlx or SeaORM for database access, and tower-http for CORS/logging/compression middleware.

Rust Core Concepts Quick Reference

Rust Core Concepts — Quick Reference
=====================================

Concept                Syntax / Notes
-------                --------------
Immutable variable     let x = 5;
Mutable variable       let mut x = 5;
Type annotation        let x: i32 = 5;
Ownership transfer     let y = x;  // x is moved
Immutable borrow       let r = &x;
Mutable borrow         let r = &mut x;   (only one at a time)
Dereference            *r
String literal         &str (reference, stack)
Owned string           String (heap-allocated)
Option                 Some(val) | None
Result                 Ok(val) | Err(e)
Error propagation      ?  (replaces match Err(e) => return Err(e))
Struct                 struct Name { field: Type }
Tuple struct           struct Color(u8, u8, u8);
Enum variant           enum Msg { Quit, Move { x: i32 }, Write(String) }
Pattern match          match val { Variant(x) => expr, _ => default }
Closure                |args| body  or  |args| -> Type { body }
Generic function       fn foo<T: Trait>(x: T) -> T
Lifetime annotation    fn foo<'a>(x: &'a str) -> &'a str
Trait definition       trait Name { fn method(&self) -> Type; }
Trait implementation   impl TraitName for TypeName { ... }
Trait object           Box<dyn Trait>  /  &dyn Trait
Async function         async fn foo() -> T { ... }
Await                  let val = future.await;
Spawn task             tokio::spawn(async { ... })
Arc (shared ownership) Arc::new(data) + Arc::clone(&arc)
Mutex (thread safety)  Mutex::new(data) + data.lock().await
Vec                    Vec::new() / vec![1,2,3]
HashMap                HashMap::new() + map.entry(k).or_insert(v)
Iterator chain         iter.filter(|x| ...).map(|x| ...).collect()
Derive macros          #[derive(Debug, Clone, Serialize, Deserialize)]
Test                   #[test] fn test_name() { assert_eq!(...); }
Conditional compile    #[cfg(test)] mod tests { ... }
𝕏 Twitterin LinkedIn
Var dette nyttig?

Hold deg oppdatert

Få ukentlige dev-tips og nye verktøy.

Ingen spam. Avslutt når som helst.

Try These Related Tools

.*Regex Tester{ }JSON FormatterB→Base64 Encoder

Related Articles

Rust Basics Guide: Ownership, Borrowing, Lifetimes, and Systems Programming

Master Rust programming. Covers ownership system, borrowing, lifetimes, structs, enums, pattern matching, error handling, traits, iterators, concurrency, and Rust vs C++ vs Go comparison.

Go (Golang) Guide: Goroutines, Channels, Generics, and Building REST APIs

Master Go programming. Covers goroutines, channels, interfaces, generics, error handling, REST APIs with net/http, testing, modules, and Go vs Node.js vs Python comparison for backend development.

Advanced Go Guide: Goroutines, Channels, Context, Generics, pprof, and REST APIs

Master advanced Go programming. Covers goroutines/channels deep dive, context package, interface embedding, error wrapping, generics (Go 1.18+), sync primitives, Go memory model, pprof profiling, table-driven tests/benchmarks/fuzz testing, Gin/Chi REST APIs, GORM vs sqlx, and Docker.