From rust-best-practices
Use when writing, reviewing, or modifying Rust code to ensure idiomatic patterns, proper error handling, type safety, and adherence to community standards
How this skill is triggered — by the user, by Claude, or both
Slash command
/rust-best-practices:rust-best-practicesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Reference guide for writing idiomatic, safe, and maintainable Rust code. Load topic-specific references based on your current task.
references/api-design.mdreferences/async.mdreferences/clippy-config.mdreferences/dead-code-in-serde-structs.mdreferences/enum-design.mdreferences/error-handling.mdreferences/fcis.mdreferences/finding-docs.mdreferences/lifetimes.mdreferences/patterns.mdreferences/send-sync.mdreferences/serde.mdreferences/testing.mdreferences/type-safety.mdreferences/unsafe.mdreferences/writing-docs.mdReference guide for writing idiomatic, safe, and maintainable Rust code. Load topic-specific references based on your current task.
| If you're... | Load |
|---|---|
Creating types, seeing String or i32 where domain types fit | references/type-safety.md |
Handling errors, seeing .unwrap() or .expect() | references/error-handling.md |
Using bool parameters, designing enums | references/enum-design.md |
| Designing public APIs, builders, trait implementations | references/api-design.md |
| Configuring Clippy, setting up lints | references/clippy-config.md |
| A lint fires and you're considering suppression | references/clippy-config.md — fix first, suppress only as last resort |
Adding #[allow]/#[expect] annotations, dead code warnings | references/clippy-config.md |
| Structuring code, applying design patterns | references/patterns.md |
| Organizing modules, separating pure logic from I/O | references/fcis.md |
| Unsure how to use a crate, need documentation | references/finding-docs.md |
| Writing doc comments, examples in docs | references/writing-docs.md |
| Writing or running tests, choosing test strategy | references/testing.md |
| Using async/await, tokio, channels, spawn, timeout | references/async.md |
| Choosing mutex types, graceful shutdown, cancellation | references/async.md |
Lifetime annotations, 'static, HRTBs | references/lifetimes.md |
| Writing or reviewing unsafe code, FFI, MaybeUninit | references/unsafe.md |
Send/Sync bounds, thread safety, Arc/Mutex | references/send-sync.md |
| Serde, JSON serialization, derive attributes | references/serde.md |
dead_code on Deserialize structs, DTO dead fields | references/dead-code-in-serde-structs.md |
Using aliri_braid, seeing new() conflicts or Infallible errors | references/type-safety.md |
| If you see... | Load |
|---|---|
| "future cannot be sent between threads safely" | references/send-sync.md + references/async.md |
| "cannot be shared between threads safely" | references/send-sync.md |
| "borrowed value does not live long enough" | references/lifetimes.md |
| "does not live long enough" | references/lifetimes.md |
| "missing lifetime specifier" | references/lifetimes.md |
"the trait Send is not implemented" | references/send-sync.md |
| "holding across an await point" | references/async.md |
"MutexGuard held across await" | references/async.md |
| "higher-ranked lifetime error" | references/lifetimes.md (HRTBs) |
| "cannot infer type" with serde | references/serde.md |
"duplicate definitions with name new" near a braid type | references/type-safety.md (aliri_braid gotchas) |
Type Safety: Prefer newtypes over primitives. UserId(String) > String. Identifiers should be strings (or KSUIDs/UUIDs), not integers — you don't do math on IDs.
Error Handling: thiserror for libraries, color_eyre for applications. Reserve .expect() for initialization only. Never .unwrap() or .expect() in production runtime code.
Enums over Bools: enum Visibility { Public, Private } > is_public: bool.
Make Illegal States Unrepresentable: Use the type system to prevent invalid data.
Validate at Construction: Use TryFrom/newtypes with validation in constructors. A Port(u16) that rejects 0 is better than validating port values at every call site. Once constructed, the value is always valid.
Separate Pure Logic from I/O (FCIS): Organize modules so pure domain logic is separate from I/O and side effects. Pure domain modules contain types, validation, and business rules. service modules handle I/O, persistence, and external calls. See references/fcis.md.
Prefer Minimal Visibility: Start with the most restrictive visibility. Use pub(super) for parent-module access, pub(crate) for crate-internal access, and pub only when external crates need it. Apply the same discipline to struct fields.
Before writing code that matches these patterns, STOP and reconsider.
| You're about to... | Common rationalization | What to do instead |
|---|---|---|
Use .unwrap() outside tests | "It can't fail here" / "I'll fix it later" | Use ?, .expect("reason"), or handle the error. Load references/error-handling.md. |
| Skip creating a newtype for a domain value | "It's just a String" / "Too much boilerplate" | Create the newtype. The boilerplate is the point — it prevents bugs. Load references/type-safety.md. |
| Skip input validation on a public constructor | "Callers will pass valid data" | Add TryFrom or a fn new() -> Result<Self, E>. Validate at construction, not at use. |
Hold a MutexGuard across .await | "The lock is quick" / "It won't deadlock" | Restructure: clone the data, drop the guard, then await. Load references/async.md. |
Write unsafe without a // SAFETY: comment | "It's obviously safe" / "I'll document later" | Write the SAFETY comment first. If you can't articulate the invariants, the code isn't safe. Load references/unsafe.md. |
Use bool for a two-state concept | "An enum is overkill" | Create the enum. Bools are meaningless at call sites: set_active(true) vs set_status(Status::Active). Load references/enum-design.md. |
Add a catch-all _ => to a match on your own enum | "I don't want to update every match" | That's exactly why you should — exhaustive matching catches forgotten variants at compile time. |
Use mem::transmute | "I know the layout" | You probably don't. Use from_ne_bytes, bytemuck, or zerocopy instead. Load references/unsafe.md. |
| Suppress a lint instead of fixing the code | "It's just a style lint" / "The code is more readable this way" | Fix the code. Lints exist to improve quality. If clippy says collapsible_if, collapse it. If it says manual_let_else, use let ... else. Suppression is only for structural constraints you can't change (framework signatures, verified false positives). Load references/clippy-config.md. |
Add #[allow(dead_code)] | "Conditionally dead — used in tests" / "Not used yet" | If only used in tests, the code IS dead — delete it. Refactor valuable tests to use live paths, or move test infrastructure behind #[cfg(test)]. Use #[expect(dead_code, reason = "...")] for interim work only, never #[allow]. Load references/clippy-config.md. |
Leave #[expect(dead_code)] at end of task | "Field exists but not yet used" / "Will be wired up later" | Clean it up NOW. Either wire it up or remove it. expect(dead_code) is a WIP marker, not a permanent annotation. |
Prefix a Serde field with _ to suppress dead_code | "It's just a naming convention" | Never. _field changes the expected JSON/SQL key. Delete the field or use #[expect(dead_code, reason = "...")] with a structural reason. Load references/dead-code-in-serde-structs.md. |
Keep unused fields on a Deserialize struct | "The field must match the schema" / "It documents the response" | Serde ignores unknown fields. Dead DTO fields aren't safety — they're coupling. Delete the field; comment the schema. Load references/dead-code-in-serde-structs.md. |
Assume Option<T> handles missing JSON keys | "Option means optional" / "null and missing are the same" | They are not. null → None is serde_json. Missing key → None is an implicit derive feature. Always use #[serde(default)] on Option<T> fields where the key may be absent. Load references/serde.md. |
Skip skip_serializing_if on Option<T> | "null in the output is fine" | It inflates payloads and breaks PATCH semantics. Pair #[serde(default)] with #[serde(skip_serializing_if = "Option::is_none")]. For three-state (missing/null/present), use optional_field::Field<T>. Load references/serde.md. |
npx claudepluginhub neoeinstein/claude-plugins --plugin rust-best-practicesEnforces idiomatic Rust patterns for ownership, error handling, enums, traits, concurrency, and crate structure.
Enforces idiomatic Rust patterns for ownership, borrowing, error handling with Result/anyhow/thiserror, traits, concurrency via Arc/Mutex/channels/async, and crate design. Use for writing, reviewing, refactoring Rust code.
Guides writing idiomatic Rust code with patterns for ownership, error handling, generics dispatch, and performance. Includes clippy configuration and documentation standards.