Idiomatic Rust development with ownership, borrowing, lifetimes, error handling (thiserror/anyhow), async Tokio patterns, traits, generics, Clippy linting, cargo workspaces, and production patterns for web services (axum/actix-web), CLI tools (clap), and systems programming. Use when writing, reviewing, or debugging Rust code.
The three rules that prevent 90% of borrow checker fights:
// BAD: moving out of a vector element
let names = vec!["alice".to_string(), "bob".to_string()];
let first = names[0]; // ERROR: cannot move out of index
// GOOD: borrow or clone
let first = &names[0]; // borrow
let first = names[0].clone(); // clone if you need ownership
Lifetime annotations - only needed when the compiler can't infer:
// The compiler needs to know: does the return value live as long as `a` or `b`?
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
// Struct holding a reference MUST declare the lifetime
struct Config<'a> {
name: &'a str,
}
Lifetime scopes visualized:
┌─────────────────────────────────────────┐
│ fn example() { │
│ let owner = String::from("data"); │ ← 'owner lives here
│ ├─────────────────────────┐ │
│ │ let borrowed = &owner; │ │ ← 'borrowed valid
│ │ println!("{borrowed}"); │ │ while owner alive
│ └─────────────────────────┘ │
│ } ← owner dropped, borrowed invalid │
└─────────────────────────────────────────┘
Multiple borrows:
┌─────────────────────────────────────────┐
│ fn multi() { │
│ let mut data = vec![1, 2, 3]; │
│ ├───────────────────┐ │
│ │ let r1 = &data; │ │ ← immutable borrows OK
│ │ let r2 = &data; │ │
│ │ println!("{r1}"); │ │
│ └───────────────────┘ │
│ ├───────────────────┐ │
│ │ let r3 = &mut data; │ │ ← mutable borrow alone
│ │ r3.push(4); │ │
│ └───────────────────┘ │
│ } │
└─────────────────────────────────────────┘
Common pitfall - returning references to local data:
// BAD: returns reference to local String
fn bad() -> &str {
let s = String::from("hello");
&s // ERROR: `s` dropped here
}
// GOOD: return owned data
fn good() -> String {
String::from("hello")
}
Use thiserror for library crates (typed errors), anyhow for application crates (ergonomic propagation).
// Library crate: thiserror
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("database error: {0}")]
Db(#[from] sqlx::Error),
#[error("not found: {entity} with id {id}")]
NotFound { entity: &'static str, id: i64 },
#[error("validation failed: {0}")]
Validation(String),
}
// Application crate: anyhow
use anyhow::{Context, Result};
async fn load_config(path: &str) -> Result<Config> {
let contents = tokio::fs::read_to_string(path)
.await
.context("failed to read config file")?;
let config: Config = toml::from_str(&contents)
.context("failed to parse config TOML")?;
Ok(config)
}
Never use .unwrap() in production code. Use .expect("reason") only for truly impossible cases. Prefer ? propagation everywhere else.
// Cargo.toml: tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Spawn concurrent tasks
let (users, orders) = tokio::try_join!(
fetch_users(),
fetch_orders(),
)?;
// CPU-heavy work: use spawn_blocking, never block the async runtime
let hash = tokio::task::spawn_blocking(move || {
argon2::hash_password(&password)
}).await??;
Ok(())
}
Async pitfalls:
std::thread::sleep in async code. Use tokio::time::sleep.MutexGuard (std or tokio) across an .await point.tokio::sync::Mutex only when you need to hold it across .await. Otherwise std::sync::Mutex is faster.Send + Sync bounds: if a future isn't Send, it can't be spawned with tokio::spawn. Common fix: drop non-Send types before .await.// Define behavior with traits
trait Repository: Send + Sync {
async fn find_by_id(&self, id: i64) -> Result<Option<User>>;
async fn save(&self, user: &User) -> Result<()>;
}
// Use impl Trait for simple cases
fn process(items: &[impl AsRef<str>]) { /* ... */ }
// Use trait objects for dynamic dispatch (runtime polymorphism)
fn get_repo(config: &Config) -> Box<dyn Repository> {
match config.db_type {
DbType::Postgres => Box::new(PgRepo::new()),
DbType::Sqlite => Box::new(SqliteRepo::new()),
}
}
// Prefer generics (static dispatch) when performance matters
fn serialize<T: Serialize>(value: &T) -> Result<Vec<u8>> { /* ... */ }
Extension traits - add methods to foreign types:
trait StringExt {
fn truncate_ellipsis(&self, max_len: usize) -> String;
}
impl StringExt for str {
fn truncate_ellipsis(&self, max_len: usize) -> String {
if self.len() <= max_len { return self.to_string(); }
format!("{}...", &self[..max_len.saturating_sub(3)])
}
}
use axum::{Router, Json, extract::{State, Path}, http::StatusCode};
#[derive(Clone)]
struct AppState {
db: sqlx::PgPool,
}
async fn get_user(
State(state): State<AppState>,
Path(id): Path<i64>,
) -> Result<Json<User>, AppError> {
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
.fetch_optional(&state.db)
.await?
.ok_or(AppError::NotFound { entity: "user", id })?;
Ok(Json(user))
}
fn app(state: AppState) -> Router {
Router::new()
.route("/users/{id}", axum::routing::get(get_user))
.layer(tower_http::trace::TraceLayer::new_for_http())
.layer(tower_http::cors::CorsLayer::permissive())
.with_state(state)
}
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "mytool", version, about = "A production CLI")]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
verbose: u8,
}
#[derive(Subcommand)]
enum Commands {
/// Run the migration
Migrate {
#[arg(short, long, default_value = "postgres://localhost/mydb")]
database_url: String,
},
/// Check system health
Health,
}
# clippy.toml or .clippy.toml
avoid-breaking-exported-api = false
# Cargo.toml — deny warnings in CI
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
unwrap_used = "deny"
expect_used = "warn"
dbg_macro = "deny"
todo = "warn"
Run: cargo clippy --all-targets --all-features -- -D warnings
Key Clippy lints to always enable: unwrap_used, dbg_macro, print_stdout (for libraries), large_enum_variant (box variants >200 bytes), needless_pass_by_value.
# Root Cargo.toml
[workspace]
members = ["crates/*"]
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2024"
[workspace.dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
anyhow = "1"
# crates/api/Cargo.toml
[package]
name = "myapp-api"
version.workspace = true
edition.workspace = true
[dependencies]
tokio.workspace = true
myapp-core = { path = "../core" }
Workspace layout: crates/core (domain logic, no IO), crates/api (HTTP layer), crates/cli (binary), crates/db (persistence). Core depends on nothing. API and CLI depend on core.
&str for parameters, String for owned data. Never &String.Arc<Mutex<T>>: Fine for shared mutable state, but consider channels or dashmap for concurrent maps..collect::<Vec<_>>() then iterate again: Chain iterators instead. Collect only at boundaries.#[must_use]: Add it to functions whose return value should never be ignored (especially Result).Box it. Check with std::mem::size_of.