ワンクリックで
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.