rust-best-practices

Installation
SKILL.md

Rust Best Practices

Overview

Reference guide for writing idiomatic, safe, and maintainable Rust code. Load topic-specific references based on your current task.

Quick Reference - What to Load

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.mdfix 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

Error Message → Reference

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)

Core Principles

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.

STOP — Anti-Rationalization Table

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. nullNone 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.

Authoritative Resources

Related skills
Installs
4
GitHub Stars
3
First Seen
Mar 30, 2026
Security Audits