TechLead
Lesson 28 of 28
5 min read
Rust

Rust Best Practices & Idioms

Write idiomatic Rust with clippy lints, rustfmt, API design guidelines, type-driven design, error handling patterns, and common pitfalls.

Writing Idiomatic Rust

Idiomatic Rust code leverages the type system, follows community conventions, and lets the compiler work for you. These best practices help you write code that is safe, readable, and performant — the Rust trifecta.

Clippy and rustfmt

# rustfmt — consistent code formatting
cargo fmt                    # Format all code
cargo fmt -- --check         # Check without modifying (CI)

# clippy — comprehensive linting
cargo clippy                 # Run linter
cargo clippy -- -W clippy::all        # Enable all lints
cargo clippy -- -W clippy::pedantic   # Even stricter
cargo clippy --fix           # Auto-fix suggestions

# Configure in rustfmt.toml:
# edition = "2021"
# max_width = 100
# tab_spaces = 4
# use_field_init_shorthand = true

# Configure in clippy.toml or Cargo.toml:
# [lints.clippy]
# pedantic = "warn"
# unwrap_used = "deny"
# expect_used = "warn"

Idiomatic Error Handling

// DO: Use Result and ? for error propagation
fn process_data(path: &str) -> Result {
    let content = std::fs::read_to_string(path)?;
    let data: Data = serde_json::from_str(&content)?;
    validate(&data)?;
    Ok(data)
}

// DON'T: Excessive unwrap() in production code
// let data = std::fs::read_to_string(path).unwrap(); // Will panic!

// DO: Use expect() with descriptive messages for impossible states
let home = std::env::var("HOME")
    .expect("HOME environment variable must be set");

// DO: Provide context with anyhow
use anyhow::Context;
let config = std::fs::read_to_string("config.toml")
    .context("Failed to read config.toml")?;

// DO: Use Option methods instead of matching
let name = user.name.as_deref().unwrap_or("Anonymous");
let age = user.age.filter(|&a| a > 0).unwrap_or(0);
let display = user.nickname.as_ref().unwrap_or(&user.name);

struct Data;
struct AppError;
impl From for AppError { fn from(_: std::io::Error) -> Self { AppError } }
impl From for AppError { fn from(_: serde_json::Error) -> Self { AppError } }
fn validate(_: &Data) -> Result<(), AppError> { Ok(()) }
struct User { name: String, nickname: Option, age: Option }

API Design Guidelines

// 1. Accept borrowed data, return owned data
fn process(input: &str) -> String {
    input.to_uppercase()
}

// 2. Use Into/AsRef for flexible inputs
fn greet(name: impl AsRef) {
    println!("Hello, {}!", name.as_ref());
}
// Works with &str, String, &String, Cow

// 3. Implement standard traits
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct UserId(u64);

impl std::fmt::Display for UserId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "user:{}", self.0)
    }
}

// 4. Use the type system to prevent invalid states
enum ConnectionState {
    Connected(Connection),
    Disconnected,
}
// You can't use a disconnected connection!

struct Connection;

// 5. Prefer &str over &String in function parameters
fn good(s: &str) {}      // Accepts &str, &String, and String slices
fn bad(s: &String) {}     // Only accepts &String — too restrictive

// 6. Return iterators instead of collecting
fn even_numbers(max: u32) -> impl Iterator {
    (0..max).filter(|n| n % 2 == 0)
}
// Caller decides: collect to Vec, take first N, sum, etc.

Common Pitfalls and Anti-Patterns

// PITFALL 1: Unnecessary cloning
// BAD:
fn bad_clone(items: &[String]) {
    for item in items {
        let owned = item.clone(); // Unnecessary clone!
        println!("{owned}");
    }
}
// GOOD:
fn good_borrow(items: &[String]) {
    for item in items {
        println!("{item}"); // Just borrow
    }
}

// PITFALL 2: Using index loops instead of iterators
// BAD:
fn bad_loop(v: &[i32]) -> i32 {
    let mut sum = 0;
    for i in 0..v.len() {
        sum += v[i]; // Bounds-checked on every access
    }
    sum
}
// GOOD:
fn good_iter(v: &[i32]) -> i32 {
    v.iter().sum() // No bounds checking, may auto-vectorize
}

// PITFALL 3: Stringly-typed APIs
// BAD:
fn set_color_bad(color: &str) {} // Any string accepted
// GOOD:
enum Color { Red, Green, Blue, Custom(u8, u8, u8) }
fn set_color_good(color: Color) {} // Type-safe

// PITFALL 4: Boolean parameters
// BAD:
fn process_bad(data: &[u8], verbose: bool, validate: bool) {}
// GOOD:
struct ProcessOptions {
    verbose: bool,
    validate: bool,
}
fn process_good(data: &[u8], options: ProcessOptions) {}

Documentation Best Practices

/// Calculates the nth Fibonacci number using iteration.
///
/// # Arguments
/// * n - The index of the Fibonacci number to calculate
///
/// # Returns
/// The nth Fibonacci number
///
/// # Examples
/// // let fib = fibonacci(10);
/// // assert_eq!(fib, 55);
///
/// # Panics
/// Does not panic for any valid input.
pub fn fibonacci(n: u32) -> u64 {
    let (mut a, mut b) = (0u64, 1u64);
    for _ in 0..n {
        let temp = a + b;
        a = b;
        b = temp;
    }
    a
}

// Module-level documentation
//! # My Crate
//!
//! This crate provides utilities for...
//!
//! ## Quick Start
//! // use my_crate::process;
//! // let result = process("input");

// Run doc tests: cargo test --doc
// Generate docs: cargo doc --open

Type-Driven Design

// Make illegal states unrepresentable

// BAD: Possible invalid state
struct BadUser {
    email: String,           // Could be empty or invalid
    email_verified: bool,    // Could be true with invalid email
    password_hash: String,   // Could be empty
}

// GOOD: States encoded in types
enum UserState {
    Unverified {
        email: Email,
        verification_token: Token,
    },
    Verified {
        email: Email,
        password_hash: PasswordHash,
    },
    Suspended {
        email: Email,
        reason: String,
    },
}

struct Email(String);    // Validated on construction
struct Token(String);
struct PasswordHash(String);

impl Email {
    fn new(raw: &str) -> Result {
        if raw.contains('@') && raw.contains('.') {
            Ok(Email(raw.to_string()))
        } else {
            Err(ValidationError("Invalid email format".into()))
        }
    }
}

struct ValidationError(String);

// Now it's IMPOSSIBLE to have a verified user without a valid email
// The compiler enforces your business rules!

Key Takeaways

  • ✅ Run cargo clippy and cargo fmt on every project — enforce in CI
  • ✅ Use the type system to make illegal states unrepresentable
  • ✅ Accept borrowed data (&str, &[T]) in parameters, return owned data
  • ✅ Prefer iterators over index loops — they're safer and often faster
  • ✅ Write doc comments with examples that double as tests via cargo test --doc

Continue Learning