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 everyunsafeblock in writing - •Clippy Compliance: All code passes
cargo clippy -- -D warningswith 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.lockfor binaries; omit for libraries - •Run
cargo updateweekly for security patches
Code Style
- •Run
cargo fmtbefore every commit (no exceptions) - •Run
cargo clippy -- -D warningsbefore every commit - •Follow Rust API Guidelines
- •
snake_casefor functions, variables, modules, crate names - •
PascalCasefor types, traits, enums, type parameters - •
SCREAMING_SNAKE_CASEfor constants and statics - •Prefer exhaustive
matchoverif letchains - •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
thiserrorfor library error types,anyhowfor application code - •Never use
Stringas an error type (create enums or usethiserror) - •Use
?operator for propagation; avoid manualmatchonResultwhen?suffices - •
unwrap()is forbidden outside tests and examples - •
expect("reason")is allowed only with a comment explaining the invariant - •Use
#[must_use]on functions returningResultor 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
'staticbounds unless truly needed (e.g., thread spawning, lazy statics)
Concurrency
- •Use
tokioas the async runtime (unless project requiresasync-std) - •All async operations must have timeouts via
tokio::time::timeout - •Use
Arc<T>for shared ownership across threads; neverRc<T>in async code - •Prefer
tokio::sync::Mutexoverstd::sync::Mutexin async contexts - •Use channels (
mpsc,oneshot,broadcast) over shared mutable state - •Spawn tasks with
tokio::spawn; propagate errors viaJoinHandle - •Every
select!branch must handle cancellation
Project Structure
Binary Crate
code
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
code
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.rsshould be thin: parse CLI args, build config, calllib.rsentry - •Put all business logic in
lib.rsmodules (testable without binary) - •Use workspaces for projects with 3+ crates
- •
error.rsat 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)
rust
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)
rust
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
rust
// 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)
rust
#[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)
rust
// 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
rust
/// 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)
rust
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 testsin 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 tarpaulinorcargo llvm-covfor coverage - •Async tests use
#[tokio::test]
Tooling
Essential Commands
bash
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
toml
[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
toml
# rustfmt.toml edition = "2021" max_width = 100 use_field_init_shorthand = true
MSRV Policy
- •Set
rust-versionin 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