Rust Guide
Applies to: Rust 2021 edition+, Systems Programming, CLIs, WebAssembly, APIs
Core Principles
-
Ownership Clarity: Every value has one owner; borrowing is explicit and intentional
-
Result Over Panic: Return Result<T, E> for all fallible operations; panic! is a bug
-
Zero-Cost Abstractions: Use iterators, generics, and traits without runtime overhead
-
Minimal Unsafe: Default to #[forbid(unsafe_code)] ; justify every unsafe block in writing
-
Clippy Compliance: All code passes cargo clippy -- -D warnings with zero exceptions
Guardrails
Edition & Toolchain
-
Use Rust 2021 edition (edition = "2021" in Cargo.toml)
-
Set rust-version (MSRV) in Cargo.toml for all published crates
-
Use stable toolchain unless a nightly feature is explicitly justified
-
Pin dependency versions with ~ for compatible updates in libraries
-
Commit Cargo.lock for binaries; omit for libraries
-
Run cargo update weekly for security patches
Code Style
-
Run cargo fmt before every commit (no exceptions)
-
Run cargo clippy -- -D warnings before every commit
-
Follow Rust API Guidelines
-
snake_case for functions, variables, modules, crate names
-
PascalCase for types, traits, enums, type parameters
-
SCREAMING_SNAKE_CASE for constants and statics
-
Prefer exhaustive match over if let chains
-
No use super::* in non-test modules (explicit imports only)
-
Derive order: Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize
Error Handling
-
Return Result<T, E> for all operations that can fail
-
Use thiserror for library error types, anyhow for application code
-
Never use String as an error type (create enums or use thiserror )
-
Use ? operator for propagation; avoid manual match on Result when ? suffices
-
unwrap() is forbidden outside tests and examples
-
expect("reason") is allowed only with a comment explaining the invariant
-
Use #[must_use] on functions returning Result or important values
Lifetimes
-
Let the compiler infer lifetimes whenever possible (elision rules)
-
Add explicit annotations only when the compiler requires them
-
Prefer owned types in structs; use references only when zero-copy is measured to matter
-
Use Cow<'a, str> when a function may or may not allocate
-
Avoid 'static bounds unless truly needed (e.g., thread spawning, lazy statics)
Concurrency
-
Use tokio as the async runtime (unless project requires async-std )
-
All async operations must have timeouts via tokio::time::timeout
-
Use Arc<T> for shared ownership across threads; never Rc<T> in async code
-
Prefer tokio::sync::Mutex over std::sync::Mutex in async contexts
-
Use channels (mpsc , oneshot , broadcast ) over shared mutable state
-
Spawn tasks with tokio::spawn ; propagate errors via JoinHandle
-
Every select! branch must handle cancellation
Project Structure
Binary Crate
myproject/ ├── Cargo.toml ├── Cargo.lock ├── src/ │ ├── main.rs # Entry point, minimal (parse args, call run) │ ├── lib.rs # Public API and module declarations │ ├── config.rs # Configuration loading │ ├── error.rs # Custom error types │ ├── domain/ │ │ ├── mod.rs │ │ └── user.rs │ └── infra/ │ ├── mod.rs │ ├── db.rs │ └── http.rs ├── tests/ # Integration tests │ └── api_test.rs ├── benches/ # Benchmarks (criterion) │ └── throughput.rs └── examples/ └── basic_usage.rs
Cargo Workspace
workspace/ ├── Cargo.toml # [workspace] members ├── crates/ │ ├── core/ # Domain logic (no I/O) │ │ ├── Cargo.toml │ │ └── src/lib.rs │ ├── api/ # HTTP layer │ │ ├── Cargo.toml │ │ └── src/lib.rs │ └── cli/ # Binary entry point │ ├── Cargo.toml │ └── src/main.rs └── tests/ # Workspace-level integration tests
-
main.rs should be thin: parse CLI args, build config, call lib.rs entry
-
Put all business logic in lib.rs modules (testable without binary)
-
Use workspaces for projects with 3+ crates
-
error.rs at crate root defines the crate-level error enum
-
No global mutable state; use dependency injection via function arguments or structs
Error Handling Patterns
Library Errors (thiserror)
use thiserror::Error;
#[derive(Error, Debug)] pub enum StorageError { #[error("record {id} not found in {table}")] NotFound { table: &'static str, id: String },
#[error("duplicate key: {0}")]
Conflict(String),
#[error(transparent)]
Database(#[from] sqlx::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
}
Application Errors (anyhow)
use anyhow::{bail, Context, Result};
fn load_config(path: &str) -> Result<Config> { let raw = std::fs::read_to_string(path) .with_context(|| format!("reading config from {path}"))?;
let config: Config = toml::from_str(&raw)
.context("parsing TOML config")?;
if config.port == 0 {
bail!("port must be non-zero");
}
Ok(config)
}
Conversion With the ? Operator
// Define From impls via thiserror's #[from], then just use ? fn create_user(db: &Pool, input: &NewUser) -> Result<User, AppError> { validate_email(&input.email)?; // ValidationError -> AppError let user = db.insert(input)?; // sqlx::Error -> AppError Ok(user) }
Testing
Unit Tests (in-module)
#[cfg(test)] mod tests { use super::*;
#[test]
fn parse_valid_email_succeeds() {
let result = Email::parse("user@example.com");
assert!(result.is_ok());
}
#[test]
fn parse_invalid_email_returns_error() {
let err = Email::parse("not-an-email").unwrap_err();
assert!(matches!(err, ValidationError::InvalidEmail(_)));
}
#[test]
fn amount_cannot_be_negative() -> Result<(), Box<dyn std::error::Error>> {
let result = Amount::new(-5);
assert!(result.is_err());
Ok(())
}
}
Integration Tests (tests/ directory)
// tests/api_test.rs use myproject::app;
#[tokio::test] async fn health_endpoint_returns_ok() { let app = app::build_test_app().await; let resp = app.get("/health").await; assert_eq!(resp.status(), 200); }
Doc Tests
/// Adds two numbers together.
///
/// # Examples
///
/// /// use mycrate::add; /// assert_eq!(add(2, 3), 5); ///
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
Property-Based Testing (proptest)
use proptest::prelude::*;
proptest! { #[test] fn roundtrip_serialization(input in "[a-zA-Z0-9]{1,100}") { let encoded = encode(&input); let decoded = decode(&encoded)?; prop_assert_eq!(input, decoded); } }
Testing Standards
-
Test names describe behavior: fn rejected_order_cannot_be_shipped()
-
Unit tests live in #[cfg(test)] mod tests in the same file
-
Integration tests go in tests/ directory
-
Use #[should_panic(expected = "...")] for panic-testing (rare)
-
Use assert!(matches!(...)) for enum variant assertions
-
Coverage target: >80% for libraries, >60% for applications
-
Use cargo tarpaulin or cargo llvm-cov for coverage
-
Async tests use #[tokio::test]
Tooling
Essential Commands
cargo fmt # Format (non-negotiable) cargo clippy -- -D warnings # Lint with deny cargo test # All tests cargo test --all-features # Test all feature combinations cargo check # Fast type-check (no codegen) cargo doc --open # Generate and view docs cargo audit # Check for vulnerable deps cargo deny check # License + advisory check cargo bench # Run benchmarks (criterion)
Cargo.toml Essentials
[package] name = "myproject" version = "0.1.0" edition = "2021" rust-version = "1.75"
[dependencies] serde = { version = "1", features = ["derive"] } thiserror = "2" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tracing = "0.1"
[dev-dependencies] proptest = "1" tokio = { version = "1", features = ["test-util"] }
[profile.release] lto = true codegen-units = 1 strip = true
[lints.clippy] pedantic = { level = "warn", priority = -1 } unwrap_used = "deny" expect_used = "warn"
Rustfmt Configuration
rustfmt.toml
edition = "2021" max_width = 100 use_field_init_shorthand = true
MSRV Policy
-
Set rust-version in Cargo.toml for every published crate
-
Test MSRV in CI: cargo +1.75.0 check
-
Bump MSRV only when a dependency or language feature requires it
-
Document MSRV bumps in changelog
Advanced Topics
For detailed patterns and examples, see:
-
references/patterns.md -- Async server, builder, newtype, state machine patterns
-
references/pitfalls.md -- Common ownership mistakes, lifetime traps, async gotchas
-
references/security.md -- Unsafe auditing, dependency vetting, secret handling
External References
-
The Rust Programming Language
-
Rust by Example
-
Rust API Guidelines
-
The Async Book
-
Rustonomicon (Unsafe Rust)
-
Clippy Lint Reference
-
Error Handling in Rust (blog)
-
cargo-deny