Rust Best Practices

SkillDev tools

Gives your agent guidance for writing clean, idiomatic Rust code and reviewing it.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Rust Best Practices skill

About this capability

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

What this skill tells your AI

The instructions your AI receives, as published by pgdogdev/pgdog in .claude/skills/rust/SKILL.md and read by ahel’s review.

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

Code health

  1. Prefer smaller files, refactor large files into multi-file modules
  2. Refactor large modules into several
  3. Prefer re-usable functions. Don't create separate functions if re-use is not planned.

Error Handling

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()

// 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);
}

Import instead of using absolute paths

/// BAD
let v = tokio::net::TcpStream::connect("localhost:8080");

/// GOOD
use tokio::net::TcpStream;
let v = TcpStream::connect("localhost:8080");

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-PatternBetter Approach
.unwrap() everywhereUse ? operator
clone() to satisfy borrow checkerRestructure ownership
String parametersUse &str or impl Into<String>
Boolean parametersUse enums
Long function bodiesExtract to smaller functions
Deep nestingUse early returns
Magic numbersUse 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 nextest run     # Run tests
cargo doc --open      # Generate and view docs
cargo clippy --fix    # Auto-fix lint issues

Signals

GitHub stars
5k
Forks
277
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
rust-pgdogdev
Source
github.com/pgdogdev/pgdog