morganlinton/beautifulrust/rust-best-practices

Claude Code and agent skill by morganlinton/beautifulrust

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Write idiomatic, robust, maintainable Rust. Use this skill whenever you write, review, refactor, debug, or explain Rust code: new crates, modules, functions, structs, traits, error types, async code, unsafe or FFI code, Cargo.toml, tests, or rustdoc. Trigger on any mention of Rust, cargo, crates, .rs files, the borrow checker, lifetimes, traits, clippy, or rustfmt, even when the user does not ask for "best practices" or "idiomatic" code. Synthesized from the Google Rust Style Guide, the Rust AP…

Install

npx vskill@latest install morganlinton/beautifulrust/rust-best-practices

SKILL.md

Rust best practices

Distilled from five Rust style and design guides, with a digest of each in references/. Where they disagree, this file says which to follow and why.

1. Mindset

  • Write Rust-shaped code. Don't port C++, Java, C#, or Python constructs one-to-one.

Model errors with Result, resources with ownership and Drop, polymorphism with traits and enums, and nullability with Option. Inheritance-flavored tricks (Deref to a "base" struct, Manager/Factory types, throw_if_null helpers) are smells.

  • Make invalid states unrepresentable. Push invariants into types: enums instead of

bool flags and "must be X if Y" comments, newtypes for units and IDs, fallible constructors that validate once. Then the compiler enforces what a comment only requests.

  • Application code is not library code. Library code (anything with external users)

follows the API-design rules below strictly. Application code should favor simplicity and directness: concrete types, String or &str rather than impl Into<String> parameters, anyhow-style errors, Vec return values, no speculative generality.

  • Let the tools do the work. rustfmt owns formatting, clippy owns idiom checks, the

compiler owns memory safety. Spend your attention on design.

2. Before you hand back code

Run these and fix everything they report:

bash
cargo fmt
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo doc --no-deps # libraries: catches broken doc links and missing docs
  • Fix a lint rather than silencing it. When a lint is genuinely wrong for one item, use

#[expect(clippy::name, reason = "...")] on the narrowest item. expect warns when the lint stops firing, so stale suppressions get cleaned up. Use #[allow] only in generated code or macro output.

  • Never put #![deny(warnings)] in source: new toolchains add warnings and break the

build. Enforce warnings-as-errors in CI instead, with cargo clippy -- -D warnings.

  • Configure lints once in Cargo.toml ([lints] or [workspace.lints]), not with

scattered crate attributes. If a project has none, suggest lints/lints.toml and lints/clippy.toml from https://github.com/morganlinton/BeautifulRust; the suppression lints in it need rust-version 1.81 or later.

3. Ownership and borrowing

Prefer unique ownership. Reach for Rc/Arc only when shared ownership is part of the design, and for RefCell/Mutex only when interior mutability is. They are not failures, but they move checking from compile time to run time, so use them on purpose.

Don't clone to make the borrow checker stop complaining. A clone that exists only to end a borrow produces a silent copy whose later changes are lost. Try these first, in roughly this order:

  1. Extend a temporary with a named let, or shorten a borrow with a block.
  2. Split a long expression into typed intermediates so the failing step is visible.
  3. Move a field out of &mut self with std::mem::take (needs Default),

std::mem::replace, or Option::take instead of cloning it.

  1. Split a large struct into smaller ones so disjoint fields can be borrowed separately.

The compiler tracks disjoint field borrows inside one function, but not across method calls.

  1. Clone small, immutable data deliberately, and say so in a comment if it's not obvious.
  2. Use indices into an owning Vec, or Rc<RefCell<_>> with Weak back-pointers, as

a last resort. Avoid self-referential structs.

rust
// Move out of a &mut enum without cloning.
fn a_to_b(e: &mut MyEnum) {
if let MyEnum::A { name, x: 0 } = e {
*e = MyEnum::B { name: std::mem::take(name) };
}
}

Accept the most general borrowed type. &str not &String, &[T] not &Vec<T>, &T not &Box<T>, Option<&T> not &Option<T>. Deref coercion then lets callers pass owned values, borrows, or literals. Where you only read a path, string, or byte buffer, impl AsRef<Path> / impl AsRef<str> / impl AsRef<[u8]> is fine on parameters (never on struct fields; store PathBuf and String). Library code that only iterates should take impl IntoIterator<Item = T>; application code may simply take &[T].

Take ownership only when you need it, and then take it by value. If a function stores or consumes its argument, take T, not &T plus an internal .clone(). If it only reads, take &T; if it mutates in place, &mut T. Return multiple values as a tuple or struct, not through &mut out-parameters (reusing a caller-owned buffer, as Read::read does, is the exception).

Prefer owned data in structs. A struct holding &'a T forces a lifetime onto every container. Owning a String or Vec<u8> is usually simpler and rarely the bottleneck. When a type does carry a lifetime, give it a descriptive name ('src, 'conn); '_ and 'a are fine on simple functions.

Freeze after setup. Build with let mut, then rebind let data = data; so later code cannot mutate it by accident.

Keep `Rc`, `Arc`, `Box`, and `RefCell` out of public signatures. Take &T, &mut T, or T and let callers decide how to own things. A service that must be shared implements a cheap Clone by wrapping its state in Arc<Inner> internally.

4. Types and API design

Naming
ItemCaseNotes
Types, traits, enum variantsUpperCamelCaseAcronyms are one word: Uuid, HttpClient
Functions, methods, modules, localssnake_case
Constants, staticsSCREAMING_SNAKE_CASE
Type parametersshort, from the main boundT, R: Read, W: Write
Lifetimesshort lowercasedescriptive when on a type
  • No get_ prefix on getters: fn len(&self), fn name(&self) -> &str,

fn name_mut(&mut self). Use bare get only when there is one obvious thing to get (Cell::get). Related: set_foo(T), take_foo(), with_foo(|f| ..) for closure access, and unsafe _unchecked siblings for checked accessors.

  • Conversion prefixes signal cost and ownership: as_ (free, borrow to borrow), to_

(expensive, borrow to owned or borrow to borrow), into_ (consumes self). Put mut in type order: as_mut_slice, not as_slice_mut.

  • Constructors: new (primary), with_capacity / new_with_foo (variants), try_new

(fallible), from_foo (conversion; pair important ones with a From impl). I/O resources use domain verbs: File::open, TcpStream::connect.

  • iter / iter_mut / into_iter return types named Iter / IterMut / IntoIter;

keys() returns Keys.

  • Error types read verb-object-error: ParseBoolError, not BoolParseError.
  • Avoid weasel suffixes Service, Manager, Factory, Helper, Util. Name the thing

it is (Bookings, Dispatcher, FooBuilder). Don't repeat the module name in the type (foo::Id, not foo::FooId). Avoid abbreviations an outsider wouldn't know.

  • If you implement len(), implement is_empty().
  • Feature names are bare: std, serde, alloc, never use-std or no-std.
Model the domain with types
  • Newtypes for semantic distinctions: struct Miles(f64) and struct Kilometers(f64)

cannot be mixed, where a type alias can. Add #[repr(transparent)] when layout matters, and derive or forward the traits you need (derive_more cuts the boilerplate). A newtype that encodes an invariant (Month) validates in a fallible constructor (TryFrom, FromStr, try_new) and never offers an infallible From from the raw type.

  • No `bool` parameters in public or shared signatures. report(true) is unreadable at

the call site and cannot grow a third mode. Use a two-variant enum (Verbosity::Summary, Verbosity::Full) or two methods (report(), report_verbose()): Widget::new(Size::Small, Shape::Round), not Widget::new(true, false). The only exception is a private helper whose call sites sit a few lines away. When refactoring, an existing bool parameter is one of the smells to remove, not a detail to preserve.

  • Named fields over tuples, except single-field newtypes and very local code.
  • `Option` for absence, never sentinels such as -1 or "". Data-carrying enums

instead of "field X is only meaningful when Y is set".

  • Use the strongest std type: PathBuf / &Path for paths, Duration for spans,

NonZeroU32 internally only (keep plain integers at API boundaries).

  • Independent flags use the bitflags crate; an enum models exactly one choice.
Derive and implement the standard traits eagerly

Downstream crates cannot add std traits to your types (orphan rule), so you must. Derive whenever every field supports it: Debug (on every public type; hand-write it to redact secrets), Clone (except unique resources and key material), Copy (only small bitwise-copyable types, because the compiler copies silently), PartialEq / Eq, PartialOrd / Ord (never ship PartialOrd without Ord when a total order exists), Hash (consistent with Eq), Default. Implement Display for anything meant to be read: error types, string-like wrappers. Add #[must_use] to pure functions and to types, such as errors and lazy values, whose discarding is a bug.

Implement From / TryFrom, never Into / TryInto directly (blanket impls give you those). Prefer from / into over as, which silently allows lossy casts. Implement FromStr rather than an ad-hoc from_str method, FromIterator and Extend on collections, and IntoIterator for both owned and borrowed forms. Implement Deref only on smart pointers and owning collections (Vec<T> to [T]), never to fake inheritance. Overload operators only for their mathematical meaning.

Don't put derivable trait bounds on struct definitions: #[derive(Clone)] struct Good<T>, not struct Bad<T: Clone + Debug>. Bounds belong on the impl blocks that need them.

If you hand-write Ord, hand-write PartialEq, PartialOrd, and Hash to agree with it (compare the same fields, ignore the same fields). A derived PartialEq next to a custom Ord breaks the std contract and misbehaves in BTreeMap and sort/dedup. Say in the docs which fields equality ignores.

Constructors, builders, defaults
  • Provide new() even when Default exists, and make them agree (one delegates to the

other). Derive or implement Default so callers can write ..Default::default(); bag-of-options structs with public fields should always have it.

  • Up to two optional parameters: inherent constructors (new, with_a, with_b).

Beyond that, or when construction validates or has side effects, add a builder: Foo::builder() returns FooBuilder, setters are named after fields (timeout(d), not set_timeout) and infallible, and build() -> Result<Foo, Error> does all validation including cross-field checks. Prefer non-consuming setters (&mut self -> &mut Self) unless build must move a value out.

  • A constructor with four or more loose arguments should group them into semantic types:

Deposit::new(account, amount) rather than four &str / u64 parameters.

Visibility and evolution
  • Private by default. Fields stay private unless the struct is passive C-style data.

Widen to pub(crate) or pub(super) before pub. Making an item more public later is non-breaking; the reverse is breaking, and every pub item is a promise.

  • Mark public enums and public-field structs that cross crate boundaries

#[non_exhaustive] unless the set is a stable ontology. When matching one, list the variants you handle and add a wildcard arm.

  • Seal traits you don't want implemented downstream, and say so in the docs:

``rust pub trait TheTrait: private::Sealed { /* ... */ } mod private { pub trait Sealed {} impl Sealed for usize {} } ``

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/morganlinton/beautifulrust/rust-best-practices/badge)](https://verified-skill.com/skills/morganlinton/beautifulrust/rust-best-practices)

Something wrong with this skill or its page? Tell us.