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.