---
name: rust-patterns
description: "Rust: ownership, lifetimes, async (Tokio), Result/anyhow/thiserror, traits, unsafe. Triggers: Rust, borrow checker, lifetime, Tokio, cargo, trait, impl, Result, unsafe, clippy."
effort: medium
user-invocable: false
allowed-tools: Read
---

# Rust Patterns

## Project Structure

```
my-app/
├── Cargo.toml
├── src/
│   ├── main.rs           # Binary entry point
│   ├── lib.rs            # Library root (re-exports)
│   ├── error.rs          # Crate-level error types
│   ├── api/
│   │   ├── mod.rs
│   │   └── handlers.rs
│   └── domain/
│       ├── mod.rs
│       └── service.rs
├── tests/                # Integration tests (separate crate)
│   └── api_test.rs
├── benches/              # criterion benchmarks
│   └── throughput.rs
└── examples/
    └── demo.rs
```

Workspace layout for multi-crate projects:

```toml
# Cargo.toml (workspace root)
[workspace]
resolver = "2"
members = ["crates/core", "crates/api", "crates/cli"]

[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
```

---

## Idioms / Code Style

### Ownership and Borrowing

```rust
// Borrow when you only need to read
fn print_name(name: &str) { println!("{name}"); }

// Take ownership when storing or consuming the value
fn register_user(name: String) -> User {
    User { name, id: Uuid::new_v4() }
}
```

### Lifetimes

```rust
// Annotate only when the compiler cannot infer
struct Parser<'input> {
    source: &'input str,
    pos: usize,
}

impl<'input> Parser<'input> {
    fn next_token(&mut self) -> Option<&'input str> {
        let start = self.pos;
        // ... advance self.pos ...
        Some(&self.source[start..self.pos])
    }
}
```

### Trait-Based Design

```rust
trait Repository {
    fn find_by_id(&self, id: Uuid) -> Result<Option<User>, DbError>;
    fn save(&self, user: &User) -> Result<(), DbError>;
}

// Accept generics for testability
fn create_user(repo: &impl Repository, name: String) -> Result<User, AppError> {
    let user = User::new(name);
    repo.save(&user)?;
    Ok(user)
}
```

### Iterators, Pattern Matching, Newtype

```rust
// Iterator chains over manual loops
let active: Vec<&str> = users.iter()
    .filter(|u| u.is_active)
    .map(|u| u.email.as_str())
    .collect();

// Exhaustive matching
match command {
    Command::Start { port } => start_server(port),
    Command::Stop => shutdown(),
}

// let-else for early exit (Rust 1.65+)
let Some(cfg) = load_config() else { return Ok(Config::default()); };

// Newtype to prevent primitive misuse
struct UserId(Uuid);
struct Email(String);

impl Email {
    fn new(raw: &str) -> Result<Self, ValidationError> {
        if raw.contains('@') { Ok(Self(raw.to_lowercase())) }
        else { Err(ValidationError::InvalidEmail) }
    }
}
```

### Builder Pattern

```rust
#[derive(Default)]
struct RequestBuilder { url: String, timeout: Option<Duration> }

impl RequestBuilder {
    fn url(mut self, url: impl Into<String>) -> Self { self.url = url.into(); self }
    fn timeout(mut self, d: Duration) -> Self { self.timeout = Some(d); self }
    fn build(self) -> Result<Request, BuildError> {
        if self.url.is_empty() { return Err(BuildError::MissingUrl); }
        Ok(Request { url: self.url, timeout: self.timeout.unwrap_or(Duration::from_secs(30)) })
    }
}
```

---

## Error Handling

### thiserror (libraries) vs anyhow (binaries)

```rust
use thiserror::Error;

#[derive(Debug, Error)]
pub enum AppError {
    #[error("not found: {0}")]
    NotFound(String),
    #[error("validation failed: {0}")]
    Validation(String),
    #[error("database error")]
    Database(#[from] sqlx::Error),
    #[error(transparent)]
    Unexpected(#[from] anyhow::Error),
}
```

```rust
// anyhow for application / binary code -- adds context to any error
use anyhow::{Context, Result};

fn load_config(path: &Path) -> Result<Config> {
    let content = fs::read_to_string(path)
        .with_context(|| format!("failed to read {}", path.display()))?;
    toml::from_str(&content).context("invalid TOML")
}
```

### Error Propagation and Boundary Mapping

```rust
// ? converts and propagates via From impls
fn get_email(pool: &PgPool, id: Uuid) -> Result<String, AppError> {
    let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
        .fetch_optional(pool).await?
        .ok_or_else(|| AppError::NotFound(format!("user {id}")))?;
    Ok(user.email)
}

// Map domain errors to HTTP at the API boundary
impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        let (status, msg) = match &self {
            AppError::NotFound(m) => (StatusCode::NOT_FOUND, m.clone()),
            AppError::Validation(m) => (StatusCode::BAD_REQUEST, m.clone()),
            _ => (StatusCode::INTERNAL_SERVER_ERROR, "internal error".into()),
        };
        (status, Json(json!({ "error": msg }))).into_response()
    }
}
```

---

## Testing Patterns

### Unit Tests (inline module)

```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn email_rejects_invalid() { assert!(Email::new("bad").is_err()); }

    #[tokio::test]
    async fn fetches_user() {
        let pool = setup_test_db().await;
        let user = get_user(&pool, test_id()).await.unwrap();
        assert_eq!(user.name, "Alice");
    }
}
```

### Integration Tests (tests/ directory)

```rust
// tests/api_test.rs -- compiled as separate crate, only sees pub API
#[tokio::test]
async fn health_returns_200() {
    let app = my_app::app().await;
    let resp = app.oneshot(
        Request::builder().uri("/health").body(Body::empty()).unwrap()
    ).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
}
```

### Mocking (mockall) and Property Testing (proptest)

```rust
use mockall::automock;

#[automock]
trait UserRepo { fn find(&self, id: Uuid) -> Result<Option<User>, DbError>; }

#[test]
fn returns_not_found_when_missing() {
    let mut mock = MockUserRepo::new();
    mock.expect_find().returning(|_| Ok(None));
    assert!(matches!(UserService::new(mock).get_user(id), Err(AppError::NotFound(_))));
}
```

```rust
use proptest::prelude::*;
proptest! {
    #[test]
    fn roundtrips(s in "[a-z]{1,20}@[a-z]{1,10}\\.[a-z]{2,4}") {
        let e = Email::new(&s).unwrap();
        assert_eq!(e.to_string(), s.to_lowercase());
    }
}
```

```bash
cargo test                        # All tests
cargo test --lib                  # Unit only
cargo test --test api_test        # Single integration file
cargo nextest run                 # Parallel runner, better output
```

---

## Common Libraries

| Crate | Purpose | Notes |
|-------|---------|-------|
| **serde** / serde_json | Serialization | `#[derive(Serialize, Deserialize)]` on DTOs |
| **tokio** | Async runtime | `features = ["full"]` for apps |
| **axum** | HTTP framework | Tower-based, extractors, `State` |
| **clap** | CLI parsing | `#[derive(Parser)]` |
| **reqwest** | HTTP client | Async, rustls TLS |
| **tracing** | Structured logging | Replaces `log`; use `tracing-subscriber` |
| **sqlx** | Async SQL | Compile-time checked queries |
| **tower** | Middleware | Layers, timeouts, rate limiting |
| **rayon** | Data parallelism | `.par_iter()` drop-in |
| **criterion** | Benchmarking | Statistical, HTML reports |

### Axum + Tracing Minimal Server

```rust
use axum::{extract::State, routing::get, Json, Router};
use std::sync::Arc;

struct AppState { db: PgPool }

async fn list_users(State(s): State<Arc<AppState>>) -> Json<Vec<User>> {
    Json(sqlx::query_as!(User, "SELECT * FROM users")
        .fetch_all(&s.db).await.unwrap())
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .json().init();
    let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()).await.unwrap();
    let app = Router::new().route("/users", get(list_users)).with_state(Arc::new(AppState { db: pool }));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
```

---

## Performance Tips

```rust
// Avoid cloning -- return references when possible
fn names(data: &[Record]) -> Vec<&str> {
    data.iter().map(|r| r.name.as_str()).collect()
}

// Cow for conditional ownership -- zero alloc on the happy path
use std::borrow::Cow;
fn normalize(input: &str) -> Cow<'_, str> {
    if input.contains(' ') { Cow::Owned(input.replace(' ', "_")) }
    else { Cow::Borrowed(input) }
}

// Rayon for CPU-bound parallelism
use rayon::prelude::*;
let out: Vec<_> = inputs.par_iter().map(|i| compute(i)).collect();

// Preallocate collections
let mut v = Vec::with_capacity(items.len());

// Zero-cost abstractions: iterators compile to the same code as manual loops
let sum: u64 = values.iter().filter(|v| **v > 0).sum();

// #[inline] only on small hot functions crossing crate boundaries
```

### Benchmarking (criterion)

```rust
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn bench_parse(c: &mut Criterion) {
    let input = include_str!("../fixtures/large.json");
    c.bench_function("parse_json", |b| b.iter(|| parse(black_box(input))));
}
criterion_group!(benches, bench_parse);
criterion_main!(benches);
```

---

## Build / Package Management

### Cargo.toml

```toml
[package]
name = "my-app"
version = "0.1.0"
edition = "2021"
rust-version = "1.75"              # MSRV

[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
mockall = "0.13"

[[bench]]
name = "throughput"
harness = false
```

### Feature Flags

```toml
[features]
default = ["json"]
json = ["serde_json"]
postgres = ["sqlx/postgres"]
```

```rust
#[cfg(feature = "postgres")]
pub mod pg_backend;
```

### Release Profile

```toml
[profile.release]
lto = true
codegen-units = 1
strip = true
panic = "abort"

[profile.dev.package."*"]
opt-level = 2                      # Optimize deps even in dev
```

### Cross-Compilation

```bash
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
# Or use cross for Docker-based builds
cargo install cross && cross build --release --target aarch64-unknown-linux-gnu
```

### Essential Cargo Commands

```bash
cargo clippy -- -D warnings       # Lint (treat warnings as errors)
cargo fmt --check                  # Format check
cargo doc --open                   # Generate docs
cargo tree -d                      # Duplicate dependencies
cargo audit                        # Known vulnerabilities
cargo deny check                   # License and advisory
cargo bloat --release              # Binary size analysis
```

### CI Pipeline (minimum)

```bash
cargo fmt --check && cargo clippy -- -D warnings && cargo test && cargo audit
```

---

## Anti-Patterns

| Anti-Pattern | Problem | Fix |
|--------------|---------|-----|
| `.unwrap()` in lib code | Panics | Return `Result`/`Option` |
| `.clone()` everywhere | Hidden allocs | Borrow, `Cow`, restructure ownership |
| `String` in every field | Unnecessary allocs | `&str` or `Cow<str>` where lifetime allows |
| `Box<dyn Error>` only | No matching | `thiserror` enums |
| `Arc<Mutex<T>>` first | Contention | Channels (`tokio::sync::mpsc`) or actors |
| Ignoring `#[must_use]` | Silent drops | Handle or `let _ =` explicitly |
