Toolmingo
Guides20 min read

Rust Developer Roadmap 2025 (Step-by-Step Guide)

The complete Rust developer roadmap for 2025 — ownership, borrowing, traits, async, web APIs, embedded, and systems programming. Know exactly what to learn and in what order to build real Rust projects.

Rust Developer Roadmap 2025

Rust has been Stack Overflow's most admired language for nine consecutive years. Companies like AWS, Google, Microsoft, Meta, and the Linux kernel team now write Rust in production. This roadmap takes you from zero to job-ready — phase by phase, with honest timelines and real code examples.

At a Glance

Phase Topic Timeline
1 Rust fundamentals Weeks 1–4
2 Ownership & borrowing Weeks 5–8
3 Structs, enums & pattern matching Weeks 7–9
4 Traits & generics Weeks 9–11
5 Error handling Weeks 10–12
6 Collections & iterators Weeks 11–13
7 Concurrency & async Weeks 13–18
8 Web development (Axum) Weeks 17–22
9 Testing & tooling Weeks 20–24
10 Systems & embedded Weeks 22–30

Phase 1 — Rust Fundamentals

Installation & Toolchain

# Install via rustup (manages Rust versions + toolchains)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Core commands
rustc --version       # compiler version
cargo --version       # package manager + build tool
cargo new hello       # create project
cargo run             # build + run
cargo build --release # optimised build
cargo check           # type-check without compiling binary

Variables, Types, and Control Flow

fn main() {
    // Immutable by default — must use `mut` to allow mutation
    let x = 5;
    let mut y = 10;
    y += 1;

    // Type inference; explicit annotation also valid
    let pi: f64 = 3.14159;
    let greeting: &str = "Hello, Rust!";
    let is_active: bool = true;

    // Shadowing — rebind the same name (different type allowed)
    let spaces = "   ";
    let spaces = spaces.len(); // now usize, not &str

    // Compound types
    let tuple: (i32, f64, char) = (42, 6.28, 'R');
    let (a, b, c) = tuple;          // destructuring
    let array: [i32; 5] = [1, 2, 3, 4, 5];

    // Control flow
    if y > 5 {
        println!("y is greater than 5");
    } else {
        println!("y is 5 or less");
    }

    // if is an expression
    let description = if x > 0 { "positive" } else { "non-positive" };

    // Loops
    for elem in &array {
        println!("{}", elem);
    }

    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2; // loop returns a value
        }
    };
}

Functions

// Functions use snake_case; return type after ->
fn add(x: i32, y: i32) -> i32 {
    x + y // no semicolon = implicit return (expression)
}

// Multiple return values via tuple
fn min_max(values: &[i32]) -> (i32, i32) {
    let min = *values.iter().min().unwrap();
    let max = *values.iter().max().unwrap();
    (min, max)
}

// Closures (anonymous functions)
let square = |x: i32| x * x;
let doubled: Vec<i32> = vec![1, 2, 3].iter().map(|&x| x * 2).collect();

Phase 2 — Ownership & Borrowing

This is Rust's killer feature — and the steepest part of the learning curve. Master this early.

Ownership Rules

Rust's ownership system enforces memory safety at compile time — no garbage collector needed.

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

    // Rule 2: When the owner goes out of scope, the value is dropped
    // Rule 3: Ownership can be moved — s1 is no longer valid after this:
    let s2 = s1;  // move, not copy
    // println!("{}", s1); // ERROR: value moved

    // Clone to make a deep copy
    let s3 = s2.clone();
    println!("{} {}", s2, s3); // both valid

    // Copy types (stack-only data) are automatically copied, not moved
    let n1 = 5;
    let n2 = n1;   // copy — n1 still valid
    println!("{} {}", n1, n2);
}

References & Borrowing

// Immutable reference — borrow without taking ownership
fn calculate_length(s: &String) -> usize {
    s.len()
} // s goes out of scope but the String is NOT dropped

fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s); // pass reference
    println!("'{}' has {} characters", s, len); // s still valid

    // Mutable reference — only ONE mutable reference at a time
    let mut s = String::from("hello");
    let r1 = &mut s;
    // let r2 = &mut s; // ERROR: cannot borrow `s` as mutable more than once
    r1.push_str(", world");

    // Cannot mix mutable and immutable references in overlapping scopes
    let r1 = &s;
    let r2 = &s;
    // let r3 = &mut s; // ERROR while r1 and r2 are active
    println!("{} {}", r1, r2);
    // r1 and r2 no longer used after this point — r3 would be OK here
}

Slices

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

fn main() {
    let sentence = String::from("hello world");
    let word = first_word(&sentence);
    // sentence.clear(); // ERROR: cannot borrow while immutable ref exists
    println!("First word: {}", word);
}

Lifetimes

// Lifetime annotation ensures reference stays valid long enough
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// Struct holding a reference needs a lifetime
struct ImportantExcerpt<'a> {
    part: &'a str,
}

Phase 3 — Structs, Enums & Pattern Matching

Structs

#[derive(Debug, Clone)]
struct User {
    username: String,
    email: String,
    active: bool,
    login_count: u64,
}

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

    // Method — takes &self
    fn display_name(&self) -> &str {
        &self.username
    }

    // Mutable method — takes &mut self
    fn login(&mut self) {
        self.login_count += 1;
    }
}

// Tuple structs
struct Point(f64, f64, f64);
struct Colour(u8, u8, u8);

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

Enums & Pattern Matching

#[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,
        }
    }
}

// Option<T> — Rust's null-safe alternative
fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

fn main() {
    // Exhaustive match
    match divide(10.0, 2.0) {
        Some(result) => println!("Result: {}", result),
        None => println!("Cannot divide by zero"),
    }

    // if let — match one variant
    if let Some(v) = divide(5.0, 0.0) {
        println!("Got {}", v);
    } else {
        println!("No result");
    }

    // while let
    let mut stack = vec![1, 2, 3];
    while let Some(top) = stack.pop() {
        println!("Popped: {}", top);
    }
}

Phase 4 — Traits & Generics

Traits (Rust's answer to interfaces)

trait Summary {
    fn summarise(&self) -> String;

    // Default implementation
    fn preview(&self) -> String {
        format!("Read more: {}", self.summarise())
    }
}

struct Article {
    title: String,
    content: String,
}

impl Summary for Article {
    fn summarise(&self) -> String {
        format!("{}: {}...", self.title, &self.content[..50.min(self.content.len())])
    }
}

// Trait as parameter — any type implementing Summary
fn notify(item: &impl Summary) {
    println!("{}", item.summarise());
}

// Trait bounds — more explicit syntax
fn notify_verbose<T: Summary>(item: &T) {
    println!("{}", item.preview());
}

// Multiple trait bounds
fn notify_both<T: Summary + std::fmt::Display>(item: &T) { /* … */ }

// Where clause for readability
fn complex<T, U>(t: &T, u: &U)
where
    T: Summary + Clone,
    U: Summary + std::fmt::Debug,
{ /* … */ }

Key Standard Traits

Trait Purpose Example
Debug Format with {:?} #[derive(Debug)]
Display Format with {} impl fmt::Display for T
Clone Deep copy #[derive(Clone)]
Copy Implicit copy for stack types #[derive(Copy, Clone)]
PartialEq / Eq Equality comparison #[derive(PartialEq, Eq)]
PartialOrd / Ord Ordering #[derive(PartialOrd, Ord)]
Hash Use as HashMap key #[derive(Hash)]
Default Zero value #[derive(Default)]
From / Into Type conversion impl From<X> for Y
Iterator Custom iterators fn next(&mut self) -> Option<Item>
Send / Sync Thread safety markers Automatically derived for safe types

Generics

// Generic function
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest { largest = item; }
    }
    largest
}

// Generic struct
struct Pair<T> {
    first: T,
    second: T,
}

impl<T: std::fmt::Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.first >= self.second {
            println!("First is larger: {}", self.first);
        } else {
            println!("Second is larger: {}", self.second);
        }
    }
}

Phase 5 — Error Handling

Result<T, E> and the ? Operator

use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;

// Custom error type
#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
    Custom(String),
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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<io::Error> for AppError {
    fn from(e: io::Error) -> Self { AppError::Io(e) }
}

impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}

// ? operator — propagates errors automatically
fn read_number_from_file(path: &str) -> Result<i32, AppError> {
    let mut file = File::open(path)?;  // io::Error → AppError via From
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    let number: i32 = contents.trim().parse()?;  // ParseIntError → AppError
    Ok(number)
}

fn main() {
    match read_number_from_file("number.txt") {
        Ok(n) => println!("Got: {}", n),
        Err(e) => eprintln!("Failed: {}", e),
    }
}

thiserror & anyhow (Popular Crates)

[dependencies]
thiserror = "1"
anyhow = "1"
use thiserror::Error;
use anyhow::{Context, Result};

#[derive(Error, Debug)]
enum DatabaseError {
    #[error("connection failed: {0}")]
    Connection(String),
    #[error("query failed: {source}")]
    Query { source: Box<dyn std::error::Error + Send + Sync> },
}

// anyhow::Result for applications where you don't need typed errors
fn read_config(path: &str) -> Result<String> {
    std::fs::read_to_string(path)
        .with_context(|| format!("Failed to read config file: {}", path))
}

Phase 6 — Collections & Iterators

Essential Collections

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

fn main() {
    // Vec<T>
    let mut v: Vec<i32> = Vec::new();
    v.push(1); v.push(2); v.push(3);
    let third = &v[2];  // panics on out-of-bounds
    let third = v.get(2);  // returns Option<&T>

    // HashMap<K, V>
    let mut scores: HashMap<String, i32> = HashMap::new();
    scores.insert(String::from("Alice"), 100);
    scores.insert(String::from("Bob"), 85);

    // Entry API — insert if absent
    scores.entry(String::from("Charlie")).or_insert(70);

    // HashSet<T> — unique values, O(1) lookup
    let mut set: HashSet<i32> = HashSet::new();
    set.insert(1); set.insert(2); set.insert(1);
    println!("Size: {}", set.len()); // 2

    // VecDeque — efficient push/pop from both ends
    let mut deque: VecDeque<i32> = VecDeque::new();
    deque.push_front(1);
    deque.push_back(2);
    deque.pop_front();
}

Iterators (Zero-Cost Abstraction)

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Lazy — nothing runs until .collect() / .for_each() / etc.
    let result: Vec<i32> = numbers.iter()
        .filter(|&&x| x % 2 == 0)  // keep evens
        .map(|&x| x * x)            // square them
        .collect();

    let sum: i32 = numbers.iter().sum();
    let product: i32 = numbers.iter().product();

    let max = numbers.iter().max().unwrap();
    let any_over_five = numbers.iter().any(|&x| x > 5);
    let all_positive = numbers.iter().all(|&x| x > 0);

    // fold — reduce with accumulator
    let factorial: u64 = (1..=10).fold(1, |acc, x| acc * x);

    // zip — pair two iterators
    let names = vec!["Alice", "Bob", "Charlie"];
    let scores = vec![100, 85, 70];
    let paired: Vec<_> = names.iter().zip(scores.iter()).collect();

    // flat_map — flatten nested iterables
    let words = vec!["hello world", "foo bar"];
    let letters: Vec<&str> = words.iter()
        .flat_map(|s| s.split(' '))
        .collect();

    // Chain iterators
    let a = vec![1, 2];
    let b = vec![3, 4];
    let chained: Vec<_> = a.iter().chain(b.iter()).collect();

    // Custom iterator
    struct Counter { count: u32 }
    impl Counter { fn new() -> Self { Counter { count: 0 } } }
    impl Iterator for Counter {
        type Item = u32;
        fn next(&mut self) -> Option<u32> {
            if self.count < 5 { self.count += 1; Some(self.count) }
            else { None }
        }
    }
}

Phase 7 — Concurrency & Async

Threads

use std::thread;
use std::sync::{Arc, Mutex};
use std::sync::mpsc;

fn main() {
    // Spawn a thread — move closure takes ownership of captured values
    let handle = thread::spawn(|| {
        for i in 1..10 { println!("Thread: {}", i); }
    });

    handle.join().unwrap();

    // Shared state — Arc<Mutex<T>>
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let h = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(h);
    }

    for h in handles { h.join().unwrap(); }
    println!("Final: {}", *counter.lock().unwrap());

    // Message passing — mpsc (multiple producer, single consumer)
    let (tx, rx) = mpsc::channel();
    let tx2 = tx.clone();

    thread::spawn(move || { tx.send("hello from thread 1").unwrap(); });
    thread::spawn(move || { tx2.send("hello from thread 2").unwrap(); });

    for received in rx { println!("Got: {}", received); }
}

Async / Await with Tokio

[dependencies]
tokio = { version = "1", features = ["full"] }
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    // Concurrent tasks — run simultaneously, not sequentially
    let (r1, r2) = tokio::join!(
        fetch_data("https://api.example.com/users"),
        fetch_data("https://api.example.com/products"),
    );

    // Spawn background task
    let handle = tokio::spawn(async {
        sleep(Duration::from_secs(1)).await;
        42
    });

    let result = handle.await.unwrap();
    println!("Background result: {}", result);
}

async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
    reqwest::get(url).await?.text().await
}

Concurrency Primitives

Primitive Use Case Notes
Mutex<T> Shared mutable state Blocks thread while locked
RwLock<T> Multiple readers OR one writer Better read-heavy workloads
Arc<T> Shared ownership across threads Atomic reference counting
mpsc::channel Thread communication Multiple producers, one consumer
tokio::Mutex Async shared state Doesn't block executor thread
tokio::RwLock Async read-write lock
tokio::mpsc Async message passing Bounded or unbounded
Atomic* types Lock-free counters/flags AtomicUsize, AtomicBool, etc.

Phase 8 — Web Development with Axum

Axum is the most popular Rust web framework (from the Tokio team).

[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"] }
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-rustls", "chrono", "uuid"] }
uuid = { version = "1", features = ["v4", "serde"] }
tracing = "0.1"
tracing-subscriber = "0.3"
use axum::{
    extract::{Path, Query, State},
    http::StatusCode,
    response::Json,
    routing::{delete, get, post, put},
    Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::net::TcpListener;

#[derive(Clone)]
struct AppState {
    db: sqlx::PgPool,
}

#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: uuid::Uuid,
    username: String,
    email: String,
}

#[derive(Deserialize)]
struct CreateUser {
    username: String,
    email: String,
}

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

// GET /users?page=1&per_page=20
async fn list_users(
    State(state): State<Arc<AppState>>,
    Query(pagination): Query<Pagination>,
) -> Result<Json<Vec<User>>, StatusCode> {
    let page = pagination.page.unwrap_or(1);
    let per_page = pagination.per_page.unwrap_or(20);
    let offset = ((page - 1) * per_page) as i64;

    let users = sqlx::query_as!(
        User,
        "SELECT id, username, email FROM users ORDER BY username LIMIT $1 OFFSET $2",
        per_page as i64,
        offset,
    )
    .fetch_all(&state.db)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    Ok(Json(users))
}

// GET /users/:id
async fn get_user(
    State(state): State<Arc<AppState>>,
    Path(id): Path<uuid::Uuid>,
) -> Result<Json<User>, StatusCode> {
    sqlx::query_as!(User, "SELECT id, username, email FROM users WHERE id = $1", id)
        .fetch_optional(&state.db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}

// POST /users
async fn create_user(
    State(state): State<Arc<AppState>>,
    Json(payload): Json<CreateUser>,
) -> Result<(StatusCode, Json<User>), StatusCode> {
    let user = sqlx::query_as!(
        User,
        "INSERT INTO users (id, username, email) VALUES ($1, $2, $3) RETURNING id, username, email",
        uuid::Uuid::new_v4(),
        payload.username,
        payload.email,
    )
    .fetch_one(&state.db)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    Ok((StatusCode::CREATED, Json(user)))
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let pool = sqlx::PgPool::connect(&database_url).await.expect("Failed to connect");

    let state = Arc::new(AppState { db: pool });

    let app = Router::new()
        .route("/users", get(list_users).post(create_user))
        .route("/users/:id", get(get_user))
        .layer(tower_http::cors::CorsLayer::permissive())
        .layer(tower_http::trace::TraceLayer::new_for_http())
        .with_state(state);

    let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
    tracing::info!("Listening on port 3000");
    axum::serve(listener, app).await.unwrap();
}

Rust Web Framework Comparison

Framework Stars Style Best For
Axum 20k+ Macro-free, type-safe extractors Tokio ecosystem, production APIs
Actix-web 22k+ Actor-based, very fast Raw performance, mature ecosystem
Warp 10k+ Filter combinators Functional style
Rocket 24k+ Macro-heavy, developer-friendly Rapid prototyping
Poem 3k+ OpenAPI-first API-first development

Phase 9 — Testing & Tooling

Unit & Integration Tests

// Unit tests live in the same file, behind #[cfg(test)]
fn add(x: i32, y: i32) -> i32 { x + y }

#[cfg(test)]
mod tests {
    use super::*;  // import parent module items

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

    #[test]
    #[should_panic(expected = "divide by zero")]
    fn test_panic() {
        let _ = 1 / 0;
    }

    #[test]
    fn test_result() -> Result<(), Box<dyn std::error::Error>> {
        let result = "42".parse::<i32>()?;
        assert_eq!(result, 42);
        Ok(())
    }
}

// Integration tests go in tests/ directory
// tests/api_test.rs
#[tokio::test]
async fn test_create_user() {
    let response = reqwest::Client::new()
        .post("http://localhost:3000/users")
        .json(&serde_json::json!({"username": "alice", "email": "alice@test.com"}))
        .send()
        .await
        .unwrap();
    assert_eq!(response.status(), 201);
}

Essential Cargo Commands

cargo test                    # run all tests
cargo test test_name          # run specific test
cargo test -- --nocapture     # show println! output during tests
cargo bench                   # run benchmarks
cargo clippy                  # linter (warnings + idiomatic suggestions)
cargo fmt                     # auto-format code (rustfmt)
cargo doc --open              # generate + open documentation
cargo audit                   # check dependencies for vulnerabilities
cargo tree                    # show dependency tree
cargo outdated                # check for outdated deps
cargo expand                  # expand macros
RUST_BACKTRACE=1 cargo run    # full backtraces on panics

Testing Pyramid

Level Tool What It Tests
Unit Built-in #[test] Functions, modules
Integration tests/ directory Crate public API
HTTP/API reqwest + axum::test helpers Full server
Property-based proptest / quickcheck Edge cases via random inputs
Fuzzing cargo-fuzz (AFL/libFuzzer) Security + correctness

Phase 10 — Systems Programming & Embedded

Unsafe Rust

fn main() {
    // Unsafe is opt-in and always explicitly marked
    let mut num = 5;
    let r1 = &num as *const i32;  // raw pointer
    let r2 = &mut num as *mut i32;

    unsafe {
        println!("r1: {}", *r1);
        *r2 = 10;
        println!("r2: {}", *r2);
    }

    // Calling C functions via FFI
    unsafe {
        println!("abs(-3) = {}", libc::abs(-3));
    }
}

// Expose Rust functions to C
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
    a + b
}

Embedded (no_std)

#![no_std]
#![no_main]

use panic_halt as _;
use cortex_m_rt::entry;
use stm32f4xx_hal::{pac, prelude::*};

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let gpioa = dp.GPIOA.split();
    let mut led = gpioa.pa5.into_push_pull_output();

    loop {
        led.toggle();
        cortex_m::asm::delay(8_000_000);
    }
}

Specialisation Areas

Area Key Crates What You Build
Web APIs axum, actix-web, sqlx, tower REST/GraphQL servers
WebAssembly wasm-bindgen, wasm-pack Browser & edge compute
Embedded embassy, rtic, cortex-m Microcontrollers (STM32, RP2040)
CLI tools clap, indicatif, colored Unix utilities, dev tools
Networking tokio, quinn (QUIC), tonic (gRPC) Protocols, proxies
Databases sqlx, sea-orm, diesel Query engines, ORMs
OS / Kernels no_std, x86_64 crate Kernels, hypervisors
Cryptography ring, rustls, sha2 TLS, hashing, signing
Game dev bevy, macroquad 2D/3D games
ML / AI candle, tch-rs Inference engines

Full Technology Map

┌─ Language ──────────────────────────────────────────────┐
│  Rust (rustup, cargo, rustc, rustfmt, clippy)           │
└─────────────────────────────────────────────────────────┘
         │
┌─ Core Concepts ──────────────────────────────────────────┐
│  Ownership · Borrowing · Lifetimes · Traits · Generics  │
│  Pattern Matching · Iterators · Error Handling           │
└──────────────────────────────────────────────────────────┘
         │
┌─ Web ──────────────────────────────────────────────────┐  ┌─ Systems ──────────────────┐
│  Axum / Actix-web                                      │  │  no_std · FFI · unsafe     │
│  sqlx / sea-orm / diesel                               │  │  cortex-m · embassy        │
│  serde / serde_json                                    │  └────────────────────────────┘
│  tower / tower-http                                    │
└────────────────────────────────────────────────────────┘
         │
┌─ Async ──────────────────┐  ┌─ Testing ─────────────────┐  ┌─ Tooling ──────────────┐
│  Tokio runtime           │  │  #[test] · #[tokio::test]  │  │  cargo · clippy · fmt  │
│  async/await             │  │  proptest · cargo-fuzz     │  │  cargo-audit · expand  │
│  channels · mpsc         │  └────────────────────────────┘  └────────────────────────┘
└──────────────────────────┘
         │
┌─ DevOps ────────────────────────────────────────────────┐
│  Docker multi-stage · GitHub Actions CI/CD · cross-rs   │
└─────────────────────────────────────────────────────────┘

Realistic 30-Week Timeline

Weeks Milestone Deliverable
1–4 Rust fundamentals CLI calculator with basic types
5–8 Ownership mastered File parser, no clone abuse
7–9 Structs & enums Command pattern with ADTs
9–11 Traits & generics Generic data structure (linked list)
10–12 Error handling Robust CLI tool with Result chains
11–14 Collections & iterators Data processing pipeline
13–18 Async & Tokio Async file/network tool
17–22 Axum web API CRUD REST API with Postgres
20–24 Testing & tooling 80%+ test coverage, CI/CD
25–30 Specialisation Choose: WebAssembly / embedded / CLI

Portfolio Projects

Project Skills Demonstrated Complexity
ripgrep-style grep clone CLI, file I/O, iterators Beginner
Password manager (CLI) Crypto, file I/O, serde Beginner
Async URL shortener (Axum + Redis) Web, async, caching Intermediate
Real-time chat server (Tokio + WebSocket) Async, concurrency, tokio::broadcast Intermediate
Custom allocator unsafe, memory layout Advanced
Interpreted programming language Parsing, AST, eval Advanced

Rust Developer Roles & Salary

Role US Salary EU Salary Notes
Junior Rust Dev $90–120k €55–80k Rare — expect to prove Rust skills with projects
Mid Rust Dev $120–160k €75–110k 2–3 yrs Rust experience valued
Senior Rust Dev $160–220k €100–150k Systems + safety expertise
Staff / Principal $200–300k+ €130–200k Architecture, team enablement
Embedded Rust $110–170k €70–120k Hardware + no_std knowledge
WebAssembly Eng $130–190k €80–130k Browser/edge + wasm-bindgen

Rust commands a 20–40% salary premium vs. equivalent Go or Python roles at the same level due to supply-demand imbalance.


Common Mistakes

Mistake Problem Fix
Fighting the borrow checker Cloning everything Learn lifetimes + restructure code
unwrap() in production Panics on None/Err Use ?, if let, or proper error handling
async without understanding Tokio Deadlocks, executor confusion Read Tokio docs; don't block inside async
Skipping clippy Idiomatic issues stay hidden cargo clippy -- -D warnings in CI
impl Trait vs dyn Trait confusion Monomorphisation vs vtable Use impl for static, dyn for heterogeneous
String vs &str everywhere Unnecessary allocations Use &str for read-only, String for owned
Global mutable state via static mut Undefined behaviour Use OnceLock or Mutex<Option<T>>
Not using #[derive] Boilerplate Always derive standard traits where sensible

Rust vs Other Systems Languages

Dimension Rust C C++ Go Zig
Memory safety Compile-time Manual Manual (RAII helps) GC Manual + safety checks
Performance Near C C baseline Near C ~80% of C Near C
Null safety Option<T> Raw null Raw null nil Optional
Concurrency Send/Sync checked Manual Manual goroutines/GC async/await
Learning curve Steep Steep Very steep Gentle Steep
Ecosystem Growing fast Mature Mature Large Early
Build tool Cargo Make/CMake CMake/Bazel go build zig build
FFI Good C is FFI Good Good Excellent
Compile time Slow Fast Slow Fast Fast
Best for Systems + web OS/embedded Game engines Cloud tools Embedded

FAQ

Do I need to learn C or C++ before Rust? No. Many successful Rust developers came from Python, JavaScript, or Go. C experience helps you understand low-level concepts, but it's not a prerequisite. The borrow checker will teach you memory management.

How long does it take to get comfortable with ownership? Expect 4–8 weeks of daily practice before the borrow checker feels like a friend rather than an enemy. Reading The Rust Book (free at doc.rust-lang.org/book/) end-to-end is the fastest path.

Is Rust production-ready for web development? Yes. AWS Lambda, Cloudflare Workers, Discord, Dropbox, and Figma all run Rust in production web workloads. Axum and Actix-web are battle-tested.

What runtime should I use for async Rust? Start with Tokio — it has the largest ecosystem, the most documentation, and is what Axum uses. Async-std is an alternative; smol is lighter. For embedded, embassy is purpose-built.

How do I find Rust jobs? Rust jobs are concentrated in: systems software, cloud infrastructure, WebAssembly, embedded, blockchain/crypto, and compilers. Check jobs.rust-lang.org, LinkedIn (filter "Rust"), and company career pages at AWS, Microsoft, and Cloudflare.

Can I use Rust for machine learning? Yes, but Python dominates ML. Rust shines in inference engines (Candle by Hugging Face, tch-rs), data processing pipelines, and high-performance model serving. Interop with Python via PyO3 is excellent.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools