| name | rust |
| description | [Applies to: **/*] Definitive guidelines for writing idiomatic, performant, and maintainable Rust code, focusing on modern best practices for structure, patterns, and error handling. |
| source | cursor_mdc |
Rust Best Practices
This guide outlines our team's definitive best practices for writing Rust code. Adhere to these principles to ensure consistency, performance, and maintainability across all projects.
1. Code Organization and Structure
1.1 Module Structure: Feature-Driven, Not File-Type-Driven
Organize modules by feature or functionality, not by separating types from implementations. Keep related structs, enums, and their impl blocks together within the same module.
❌ BAD:
pub struct User { pub id: u64 }
impl User {
pub fn new(id: u64) -> Self { Self { id } }
}
✅ GOOD:
pub struct User { pub id: u64 }
impl User {
pub fn new(id: u64) -> Self { Self { id } }
pub fn get_id(&self) -> u64 { self.id }
}
1.2 Small, Cohesive Structs
Design structs to be small and focused. Break down complex data into multiple smaller, composable structs. This improves borrow-checker ergonomics and API flexibility.
❌ BAD:
pub struct OrderProcessor {
pub order_id: u64, pub customer_id: u64, pub items: Vec<String>,
pub total_amount: f64, pub payment_status: String,
pub shipping_address_street: String, pub shipping_address_city: String,
}
✅ GOOD:
pub struct Order {
pub id: u64, pub customer_id: u64, pub items: Vec<String>,
pub total_amount: f64, pub payment_status: String,
}
pub struct Address { pub street: String, pub city: String }
pub struct OrderProcessor {
pub order: Order,
pub shipping_address: Address,
}
2. Common Patterns and Anti-patterns
2.1 Newtype Pattern for Type Safety
Wrap primitive types in newtype structs to enforce strong typing and prevent logical errors from mixing incompatible IDs or values.
❌ BAD:
fn process_transaction(user_id: u64, account_id: u64) { }
✅ GOOD:
#[derive(Debug, PartialEq, Eq, Copy, Clone)] pub struct UserId(u64);
#[derive(Debug, PartialEq, Eq, Copy, Clone)] pub struct AccountId(u64);
fn process_transaction(user_id: UserId, account_id: AccountId) { }
2.2 Builder Pattern for Complex Construction
Use the Builder pattern for structs with many optional fields or complex initialization logic. Avoid it for simple structs with one or two clear constructors.
❌ BAD (for many fields):
pub struct Config { pub timeout: u64, pub retries: u8 }
impl Config {
pub fn new(timeout: Option<u64>, retries: Option<u8>) -> Self { }
}
✅ GOOD:
pub struct Config { pub timeout: u64, pub retries: u8 }
impl Config { pub fn builder() -> ConfigBuilder { ConfigBuilder::default() } }
#[derive(Default)] pub struct ConfigBuilder { pub timeout: Option<u64>, pub retries: Option<u8> }
impl ConfigBuilder {
pub fn timeout(mut self, timeout: u64) -> Self { self.timeout = Some(timeout); self }
pub fn retries(mut self, retries: u8) -> Self { self.retries = Some(retries); self }
pub fn build(self) -> Result<Config, & > {
(Config { timeout: .timeout.(), retries: .retries.() })
}
}
2.3 Minimal Generic Bounds on Types
Apply generic bounds only when they are intrinsic to the type definition. Place stricter, derivable bounds on impl blocks or specific functions, not on the struct/enum itself.
❌ BAD:
pub struct Cache<K: std::hash::Hash + Eq + std::fmt::Debug, V: Clone> { }
✅ GOOD:
pub struct Cache<K, V> { }
impl<K: std::hash::Hash + Eq, V> Cache<K, V> {
pub fn new() -> Self { }
}
impl<K: std::fmt::Debug, V: std::fmt::Debug> Cache<K, V> {
pub fn print_keys(&self) { }
}
3. Performance Considerations
3.1 Default to Vec and HashMap
Start with Vec for sequences and HashMap for maps. They are highly optimized and cover most use cases. Only switch to other collections (VecDeque, BTreeMap, LinkedList, etc.) when profiling shows a specific performance bottleneck or a unique functional requirement (e.g., sorted keys for BTreeMap).
❌ BAD:
use std::collections::LinkedList;
fn process_items(items: LinkedList<String>) { }
✅ GOOD:
use std::collections::HashMap;
fn process_items(items: Vec<String>) { }
fn lookup_data(data: HashMap<String, u32>) { }
3.2 Pre-allocate Collection Capacity
When the approximate size of a collection is known beforehand, pre-allocate its capacity to avoid frequent reallocations, which can be costly.
❌ BAD:
let mut vec = Vec::new();
for i in 0..1000 { vec.push(i); }
✅ GOOD:
let mut vec = Vec::with_capacity(1000);
for i in 0..1000 { vec.push(i); }
4. Common Pitfalls and Gotchas
4.1 Error Handling: Result for Recoverable Errors
Always use Result<T, E> for errors that can be handled or propagated. Reserve panic! for unrecoverable bugs or situations indicating a programming error.
❌ BAD:
fn load_config(path: &str) -> String {
std::fs::read_to_string(path).expect("Failed to read config")
}
✅ GOOD:
fn load_config(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
4.2 Document unsafe Blocks
Every unsafe block must be preceded by a // SAFETY: comment explaining why the code inside is sound and upholds Rust's safety invariants.
❌ BAD:
let mut vec = vec![0];
let ptr = vec.as_mut_ptr();
unsafe { *ptr = 42; }
✅ GOOD:
let mut vec = vec![0];
let ptr = vec.as_mut_ptr();
unsafe { *ptr = 42; }
5. Testing Approaches
5.1 Unit Tests with #[test]
Use #[test] for isolated unit tests, typically placed in a tests module within the same file as the code being tested.
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
}
}
5.2 Example Tests with rustdoc
Leverage rustdoc for documentation examples. These examples are compiled and run as tests, ensuring your documentation stays up-to-date and correct.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}