Agent Skills: Rust Best Practices

Rust coding best practices for idiomatic, efficient, and maintainable code. Use when writing Rust code, reviewing code, or learning Rust patterns.

UncategorizedID: thrashr888/thrashr888-agent-kit/rust-best-practices

Install this agent skill to your local

pnpm dlx add-skill https://github.com/thrashr888/thrashr888-agent-kit/tree/HEAD/skills/rust-best-practices

Skill Files

Browse the full folder contents for rust-best-practices.

Download Skill

Loading file tree…

skills/rust-best-practices/SKILL.md

Skill Metadata

Name
rust-best-practices
Description
Rust coding best practices for idiomatic, efficient, and maintainable code. Use when writing Rust code, reviewing code, or learning Rust patterns.

Rust Best Practices

Guidelines for writing idiomatic, efficient, and maintainable Rust code.

Core Principles

  1. Leverage the type system - Make invalid states unrepresentable
  2. Prefer compile-time checks - Catch errors before runtime
  3. Be explicit about ownership - Don't fight the borrow checker
  4. Write code that passes fmt/clippy first - Not after fixing

Error Handling

Application Code: Use anyhow

use anyhow::{Context, Result, bail};

fn load_config(path: &Path) -> Result<Config> {
    let contents = std::fs::read_to_string(path)
        .context("Failed to read config file")?;

    let config: Config = toml::from_str(&contents)
        .context("Failed to parse config")?;

    if config.port == 0 {
        bail!("Port cannot be 0");
    }

    Ok(config)
}

Library Code: Use thiserror

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("Failed to read config: {0}")]
    Io(#[from] std::io::Error),

    #[error("Failed to parse config: {0}")]
    Parse(#[from] toml::de::Error),

    #[error("Invalid configuration: {message}")]
    Invalid { message: String },
}

Never Use .unwrap() in Library Code

// BAD
let value = map.get("key").unwrap();

// GOOD
let value = map.get("key").ok_or_else(|| Error::MissingKey("key"))?;

// GOOD (when None is truly impossible)
let value = map.get("key").expect("key always present after init");

Ownership & Borrowing

Prefer Borrowing Over Cloning

// BAD - unnecessary clone
fn process(data: String) { ... }
process(my_string.clone());

// GOOD - borrow when possible
fn process(data: &str) { ... }
process(&my_string);

Use Cow for Flexible Ownership

use std::borrow::Cow;

fn process(data: Cow<'_, str>) -> Cow<'_, str> {
    if data.contains("bad") {
        Cow::Owned(data.replace("bad", "good"))
    } else {
        data  // No allocation if unchanged
    }
}

Return Owned Data from Constructors

// GOOD - clear ownership
impl User {
    pub fn new(name: impl Into<String>) -> Self {
        Self { name: name.into() }
    }
}

API Design

Builder Pattern for Complex Configuration

#[derive(Default)]
pub struct ServerBuilder {
    host: Option<String>,
    port: Option<u16>,
    timeout: Option<Duration>,
}

impl ServerBuilder {
    pub fn host(mut self, host: impl Into<String>) -> Self {
        self.host = Some(host.into());
        self
    }

    pub fn port(mut self, port: u16) -> Self {
        self.port = Some(port);
        self
    }

    pub fn build(self) -> Result<Server, ConfigError> {
        Ok(Server {
            host: self.host.unwrap_or_else(|| "localhost".into()),
            port: self.port.ok_or(ConfigError::MissingPort)?,
            timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
        })
    }
}

Newtype Pattern for Type Safety

// BAD - easy to mix up
fn transfer(from: i64, to: i64, amount: i64) { ... }

// GOOD - compile-time safety
pub struct AccountId(i64);
pub struct Amount(i64);

fn transfer(from: AccountId, to: AccountId, amount: Amount) { ... }

Use #[must_use] for Important Returns

#[must_use]
pub fn validate(&self) -> Result<(), ValidationError> {
    // ...
}

Collections & Iterators

Prefer Iterators Over Loops

// BAD
let mut results = Vec::new();
for item in items {
    if item.is_valid() {
        results.push(item.transform());
    }
}

// GOOD
let results: Vec<_> = items
    .into_iter()
    .filter(|item| item.is_valid())
    .map(|item| item.transform())
    .collect();

Use collect() Type Inference

// Collect into Vec
let vec: Vec<_> = iter.collect();

// Collect into HashMap
let map: HashMap<_, _> = iter.collect();

// Collect Results
let results: Result<Vec<_>, _> = iter.collect();

Async Patterns

Use tokio for Async Runtime

#[tokio::main]
async fn main() -> Result<()> {
    let result = fetch_data().await?;
    Ok(())
}

Avoid Blocking in Async Code

// BAD - blocks the runtime
async fn bad() {
    std::thread::sleep(Duration::from_secs(1));
}

// GOOD - async sleep
async fn good() {
    tokio::time::sleep(Duration::from_secs(1)).await;
}

// GOOD - spawn blocking for CPU-intensive work
async fn compute() -> i32 {
    tokio::task::spawn_blocking(|| expensive_computation()).await.unwrap()
}

Testing

Unit Tests in Same File

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

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

    #[test]
    fn test_edge_case() {
        assert!(validate("").is_err());
    }
}

Integration Tests in tests/

// tests/integration_test.rs
use my_crate::public_api;

#[test]
fn test_full_workflow() {
    let result = public_api::process("input");
    assert!(result.is_ok());
}

Use assert! Macros Effectively

assert!(condition);
assert_eq!(left, right);
assert_ne!(left, right);
assert!(result.is_ok());
assert!(result.is_err());
assert_matches!(value, Pattern::Variant { .. });

Performance

Avoid Premature Allocation

// BAD - allocates even if not needed
fn maybe_string() -> String {
    String::from("default")
}

// GOOD - return static str when possible
fn maybe_string() -> &'static str {
    "default"
}

Use Vec::with_capacity for Known Sizes

// BAD - multiple reallocations
let mut vec = Vec::new();
for i in 0..1000 {
    vec.push(i);
}

// GOOD - single allocation
let mut vec = Vec::with_capacity(1000);
for i in 0..1000 {
    vec.push(i);
}

Profile Before Optimizing

cargo build --release
cargo flamegraph  # requires cargo-flamegraph

Module Organization

Keep Modules Focused

// src/lib.rs
pub mod config;
pub mod client;
pub mod error;

// Re-export public API
pub use config::Config;
pub use client::Client;
pub use error::Error;

Use pub(crate) for Internal APIs

// Public to crate, not external users
pub(crate) fn internal_helper() { ... }

Documentation

Document Public APIs

/// Creates a new client with the given configuration.
///
/// # Arguments
///
/// * `config` - The client configuration
///
/// # Errors
///
/// Returns an error if the configuration is invalid.
///
/// # Examples
///
/// ```
/// let client = Client::new(Config::default())?;
/// ```
pub fn new(config: Config) -> Result<Self> {
    // ...
}

Anti-Patterns to Avoid

| Anti-Pattern | Better Approach | |--------------|-----------------| | .unwrap() everywhere | Use ? operator | | clone() to satisfy borrow checker | Restructure ownership | | String parameters | Use &str or impl Into<String> | | Boolean parameters | Use enums | | Long function bodies | Extract to smaller functions | | Deep nesting | Use early returns | | Magic numbers | Use named constants |

Quick Reference

# Quality gates
cargo fmt -- --check && cargo clippy -- -D warnings && cargo test

# Common cargo commands
cargo check          # Fast syntax/type check
cargo build          # Debug build
cargo build --release # Release build
cargo test           # Run tests
cargo doc --open     # Generate and view docs
cargo clippy --fix   # Auto-fix lint issues