원클릭으로
rust-patterns
Idiomatic Rust patterns, best practices, and conventions for building safe, efficient, and maintainable Rust applications.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Idiomatic Rust patterns, best practices, and conventions for building safe, efficient, and maintainable Rust applications.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use this skill when reviewing uncommitted or local code changes before commit/PR. Finds security, correctness, quality, and testing gaps, then reports only high-confidence actionable issues.
Systematically explore and present the full architecture, structure, and content of any repository. Use when onboarding to a new codebase, before major refactors, or when the user asks "how does this project work?"
Rust testing patterns including unit tests, integration tests, doc tests, benchmarks, property-based testing, and coverage. Follows TDD methodology with idiomatic Rust practices.
Use this skill when adding authentication, handling user input, working with secrets, creating API endpoints, or implementing payment/sensitive features. Provides comprehensive security checklist and patterns.
Use this skill when writing new features, fixing bugs, or refactoring code. Enforces test-driven development with 80%+ coverage including unit, integration, and E2E tests.
| name | rust-patterns |
| description | Idiomatic Rust patterns, best practices, and conventions for building safe, efficient, and maintainable Rust applications. |
Idiomatic Rust patterns and best practices for building safe, efficient, and maintainable applications.
Rust's ownership system eliminates data races at compile time. Understand and leverage it.
// Good: Borrow when you don't need ownership
fn process(data: &[u8]) -> Result<Output> {
// ...
}
// Good: Take ownership when you need to store the value
fn store(data: Vec<u8>) -> Handle {
Handle { data }
}
// Bad: Unnecessary clone
fn process(data: &Vec<u8>) -> Result<Output> {
let owned = data.clone(); // Why clone if you only read?
// ...
}
Encode invariants in types so invalid states are unrepresentable.
// Good: State machine via types — can't call send() before connect()
struct Disconnected;
struct Connected { session: Session }
impl Disconnected {
fn connect(self, addr: &str) -> Result<Connected> {
let session = Session::new(addr)?;
Ok(Connected { session })
}
}
impl Connected {
fn send(&self, msg: &[u8]) -> Result<()> {
self.session.write(msg)
}
}
// Bad: Runtime check for something the type system can enforce
struct Client {
session: Option<Session>,
}
impl Client {
fn send(&self, msg: &[u8]) -> Result<()> {
match &self.session {
Some(s) => s.write(msg),
None => Err(Error::NotConnected), // Preventable at compile time
}
}
}
// Good: Accept &str, works with both String and &str
fn greet(name: &str) -> String {
format!("Hello, {name}!")
}
// Good: Accept impl AsRef<Path> for maximum flexibility
fn read_config(path: impl AsRef<Path>) -> Result<Config> {
let content = std::fs::read_to_string(path)?;
// ...
}
// Bad: Requiring owned String when a borrow suffices
fn greet(name: String) -> String {
format!("Hello, {name}!")
}
use anyhow::{Context, Result};
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config from {path}"))?;
let config: Config = toml::from_str(&content)
.with_context(|| format!("failed to parse config from {path}"))?;
config.validate()
.context("config validation failed")?;
Ok(config)
}
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("user {id} not found")]
UserNotFound { id: u64 },
#[error("validation failed: {field} — {message}")]
Validation { field: String, message: String },
#[error("database error")]
Database(#[from] sqlx::Error),
#[error("configuration error")]
Config(#[from] config::ConfigError),
}
// Library crates: use thiserror for typed, matchable errors
#[derive(Debug, Error)]
pub enum ParseError {
#[error("invalid syntax at line {line}")]
Syntax { line: usize },
#[error("unexpected token: {0}")]
UnexpectedToken(String),
}
// Application crates: use anyhow for ergonomic error propagation
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = load_config("config.toml")?;
let db = connect_db(&config.database_url).await?;
run_server(db).await?;
Ok(())
}
// Bad: Panics on None/Err
let value = map.get("key").unwrap();
// Good: Handle the absence
let value = map.get("key")
.ok_or_else(|| anyhow!("missing required key 'key'"))?;
// Acceptable: When the invariant is provably true, use expect()
let regex = Regex::new(r"^\d+$")
.expect("hardcoded regex is valid");
use std::sync::{Arc, Mutex};
use std::thread;
fn parallel_counter() -> u64 {
let counter = Arc::new(Mutex::new(0u64));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = counter.lock().expect("mutex poisoned");
*num += 1;
}));
}
for handle in handles {
handle.join().expect("worker thread panicked");
}
*counter.lock().expect("mutex poisoned")
}
use std::sync::mpsc;
use std::thread;
fn worker_pool(jobs: Vec<Job>) -> Vec<Result<Output>> {
let (tx, rx) = mpsc::channel();
let num_workers = std::thread::available_parallelism()
.map(|n| n.get()).unwrap_or(1);
let jobs = Arc::new(Mutex::new(jobs.into_iter()));
for _ in 0..num_workers {
let tx = tx.clone();
let jobs = Arc::clone(&jobs);
thread::spawn(move || {
loop {
let job = {
let mut iter = jobs.lock().expect("mutex poisoned");
iter.next()
};
match job {
Some(job) => tx.send(process(job)).expect("receiver dropped"),
None => break,
}
}
});
}
drop(tx); // Close sender so rx iterator terminates
rx.into_iter().collect()
}
use tokio::time::{timeout, Duration};
async fn fetch_with_timeout(url: &str) -> Result<String> {
let response = timeout(
Duration::from_secs(5),
reqwest::get(url),
)
.await
.context("request timed out")?
.context("request failed")?;
response.text().await.context("failed to read body")
}
async fn fetch_all(urls: &[String]) -> Vec<Result<String>> {
let futures: Vec<_> = urls.iter()
.map(|url| fetch_with_timeout(url))
.collect();
futures::future::join_all(futures).await
}
#[derive(Debug)]
pub struct Server {
host: String,
port: u16,
max_connections: usize,
timeout: Duration,
}
#[derive(Default)]
pub struct ServerBuilder {
host: Option<String>,
port: Option<u16>,
max_connections: Option<usize>,
timeout: Option<Duration>,
}
impl ServerBuilder {
pub fn new() -> Self {
Self::default()
}
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 max_connections(mut self, max: usize) -> Self {
self.max_connections = Some(max);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn build(self) -> Result<Server> {
Ok(Server {
host: self.host.unwrap_or_else(|| "127.0.0.1".into()),
port: self.port.unwrap_or(8080),
max_connections: self.max_connections.unwrap_or(100),
timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
})
}
}
// Usage
let server = ServerBuilder::new()
.host("0.0.0.0")
.port(3000)
.timeout(Duration::from_secs(60))
.build()?;
// Prevent accidental mixing of semantically different values
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UserId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OrderId(pub u64);
// Compile error: can't pass OrderId where UserId expected
fn get_user(id: UserId) -> Result<User> { /* ... */ }
// Add behavior via Deref or explicit methods
impl UserId {
pub fn as_u64(&self) -> u64 { self.0 }
}
impl std::fmt::Display for UserId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "user-{}", self.0)
}
}
#[derive(Debug)]
enum Order {
Draft { items: Vec<Item> },
Submitted { items: Vec<Item>, submitted_at: DateTime<Utc> },
Paid { items: Vec<Item>, paid_at: DateTime<Utc>, amount: Money },
Shipped { tracking: String },
Delivered { delivered_at: DateTime<Utc> },
Cancelled { reason: String },
}
impl Order {
fn submit(self) -> Result<Self> {
match self {
Order::Draft { items } if !items.is_empty() => {
Ok(Order::Submitted {
items,
submitted_at: Utc::now(),
})
}
Order::Draft { .. } => Err(anyhow!("cannot submit empty order")),
_ => Err(anyhow!("can only submit draft orders")),
}
}
fn cancel(self, reason: String) -> Result<Self> {
match self {
Order::Draft { .. } | Order::Submitted { .. } => {
Ok(Order::Cancelled { reason })
}
_ => Err(anyhow!("cannot cancel order in current state")),
}
}
}
// Define behavior via traits (sync version — for async, use async-trait crate or Rust 1.75+ async fn in traits)
use anyhow::Result;
pub trait UserRepository: Send + Sync {
fn find_by_id(&self, id: UserId) -> Result<Option<User>>;
fn save(&self, user: &User) -> Result<()>;
fn delete(&self, id: UserId) -> Result<()>;
}
pub trait EmailService: Send + Sync {
fn send(&self, to: &str, subject: &str, body: &str) -> Result<()>;
}
// Service depends on trait, not concrete type
pub struct UserService<R: UserRepository, E: EmailService> {
repo: R,
email: E,
}
impl<R: UserRepository, E: EmailService> UserService<R, E> {
pub fn new(repo: R, email: E) -> Self {
Self { repo, email }
}
pub fn register(&self, name: &str, email_addr: &str) -> Result<User> {
let user = User::new(name, email_addr);
self.repo.save(&user)?;
self.email.send(email_addr, "Welcome!", "Thanks for joining.")?;
Ok(user)
}
}
my-project/
├── src/
│ ├── main.rs # Binary entry point
│ ├── lib.rs # Library root (re-exports)
│ ├── config.rs # Configuration
│ ├── error.rs # Error types
│ ├── models/
│ │ ├── mod.rs
│ │ ├── user.rs
│ │ └── order.rs
│ ├── services/
│ │ ├── mod.rs
│ │ └── user_service.rs
│ └── handlers/
│ ├── mod.rs
│ └── user_handler.rs
├── tests/ # Integration tests
│ └── api_test.rs
├── benches/ # Benchmarks
│ └── parsing_bench.rs
├── Cargo.toml
└── Cargo.lock
// lib.rs — Only expose what consumers need
pub mod models;
pub mod error;
mod services; // Internal implementation
mod handlers; // Internal implementation
// Re-export key types for ergonomic use
pub use error::AppError;
pub use models::{User, Order};
# Cargo.toml (workspace root)
[workspace]
members = [
"crates/core",
"crates/api",
"crates/cli",
]
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
// Bad: Allocates a String just to compare
fn is_admin(role: &str) -> bool {
role.to_string() == String::from("admin")
}
// Good: Compare string slices directly
fn is_admin(role: &str) -> bool {
role == "admin"
}
// Bad: C-style loop
let mut sum = 0;
for i in 0..values.len() {
sum += values[i];
}
// Good: Iterator — more idiomatic, often optimized better
let sum: i64 = values.iter().sum();
// Good: Chained iterators for transforms
let names: Vec<&str> = users.iter()
.filter(|u| u.active)
.map(|u| u.name.as_str())
.collect();
use std::borrow::Cow;
// Returns borrowed or owned depending on input
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input) // Zero allocation
}
}
// Bad: Grows vec multiple times
let mut results = Vec::new();
for item in &items {
results.push(process(item));
}
// Good: Single allocation
let mut results = Vec::with_capacity(items.len());
for item in &items {
results.push(process(item));
}
// Best: Use collect (handles capacity automatically)
let results: Vec<_> = items.iter().map(process).collect();
# Build and run
cargo build
cargo run
cargo build --release
# Testing
cargo test
cargo test -- --nocapture # Show println! output
cargo test test_name # Run specific test
# Linting and formatting
cargo fmt
cargo fmt -- --check # CI mode
cargo clippy
cargo clippy -- -D warnings # Treat warnings as errors
# Dependency management
cargo update
cargo audit # Security vulnerabilities
cargo tree # Dependency tree
cargo deny check # Policy enforcement
# Documentation
cargo doc --open
# clippy.toml or .clippy.toml
too-many-arguments-threshold = 5
type-complexity-threshold = 250
# Cargo.toml
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
unwrap_used = "deny"
expect_used = "warn"
| Idiom | Description |
|---|---|
Use ? for error propagation | Don't match on every Result — propagate with ? |
Prefer &str over &String | Accept the more general type in function parameters |
Use impl Trait in parameters | fn read(r: impl Read) is cleaner than generic bounds |
| Derive common traits | #[derive(Debug, Clone, PartialEq)] on data types |
Use Default trait | Implement Default for types with sensible defaults |
Prefer collect() over manual loops | Iterators are idiomatic and often faster |
Use if let / let else for single-variant matching | Cleaner than full match |
| Encode invariants in types | Make invalid states unrepresentable |
// Bad: Using .clone() to avoid fighting the borrow checker
fn process(data: &Data) {
let owned = data.clone(); // Understand lifetimes instead
}
// Bad: Stringly-typed APIs
fn set_status(status: &str) { /* "active", "inactive"... */ }
// Good: Use enums
enum Status { Active, Inactive }
fn set_status(status: Status) { /* ... */ }
// Bad: Ignoring Results
let _ = file.write_all(data); // Error silently dropped
// Good: Handle or propagate
file.write_all(data)?;
// Bad: Overusing Rc/Arc when ownership can be restructured
// Good: Restructure data to have clear ownership hierarchies
// Bad: Using unsafe to bypass the borrow checker
// Good: Restructure code to work with the borrow checker
Remember: Let the compiler guide you. Rust's strictness is a feature — fighting the borrow checker usually means your design needs rethinking. Lean into ownership, lifetimes, and the type system.