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 |
Adding a pub use, choosing where a public item lives, re-exports, preludes |
references/re-exports.md |
| Choosing the lint set or the unsafe policy |
references/lint-setup.md |
Cargo workspace setup: inheriting package/lints/dependencies across members, [workspace.*], version drift, scaffolding a new member crate |
references/workspace.md |
A lint fired; considering suppression; #[allow]/#[expect]; dead_code; unfulfilled_lint_expectations |
references/responding-to-lints.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 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 |
Building a JSON/message payload you own — reaching for serde_json::json! or serde_json::to_vec of an ad-hoc value |
references/serde.md |
| Choosing a serialization stack (serde vs facet) |
references/serde.md |
dead_code on Deserialize structs, DTO dead fields |
references/serde.md |
Using aliri_braid, seeing new() conflicts or Infallible errors |
references/type-safety.md |
Deep facet-ecosystem work (#[derive(Facet)], facet-json/csv, figue, strid, rediff) is covered by the separate facet skill, which loads on its own triggers — never reference it as a references/ path.
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.
No stringly-typed dispatch: a closed vocabulary your own system produces is an enum. Deserialize it at the serde/storage boundary (rename_all) so unknown tokens die there; match the enum exhaustively; write each wire token in exactly one as_str-style method. match x.as_str() outside a boundary converter (a FromStr body, a SQL row edge) means the type was lost upstream — recover it there. An edit-time hook in this plugin flags fresh occurrences.
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.
Suppress with #[expect], never #[allow] in source. allow belongs only in Cargo.toml config; source uses #[expect(lint, reason = "…")] only. See references/lint-setup.md (config) and references/responding-to-lints.md (mechanics).
STOP — Anti-Rationalization Table
Before writing code that matches these patterns, STOP and reconsider.
| You're about to... |
Common rationalization |
What to do instead |
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. |
Match a string against literals (match x.as_str(), x == "…", let Some("…")) |
"The surrounding code already does it" / "it's just one comparison" |
The type was lost upstream — recover it (serde enum at the boundary). Surrounding code embodying the anti-pattern is the reason to fix it, not license to extend it. |
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" / "more readable this way" |
Fix the code. Suppression is only for structural constraints you can't change. Load references/responding-to-lints.md. |
Add #[allow(dead_code)] |
"Conditionally dead — used in tests" / "Not used yet" |
If only tests use it, it IS dead — delete it. #[expect(dead_code, reason = "…")] is for interim work only. Load references/responding-to-lints.md. |
Add a module-wide #![expect(…)] or suppress unfulfilled_lint_expectations |
"One suppression covers the module" / "the expectation warning is noise" |
The warning means an #[expect] is stale or mis-scoped — delete it or cfg_attr it to the cfg where the lint fires. Load references/responding-to-lints.md. |
| Re-export an item at an additional public path |
"Shorter import" / "easier to find" |
Every public item gets ONE canonical path; root promotion is for marquee items only. Load references/re-exports.md. |
| Declare a dependency version in a workspace member crate |
"Only this crate uses it" |
Hoist to [workspace.dependencies]; per-member versions drift and can duplicate in the tree. Load references/workspace.md. |
Authoritative Resources
1---2name: rust-best-practices3description: Use when writing, reviewing, or modifying Rust code to ensure idiomatic patterns, proper error handling, type safety, and adherence to community standards4---56# Rust Best Practices78## Overview910Reference guide for writing idiomatic, safe, and maintainable Rust code. Load topic-specific references based on your current task.1112## Quick Reference - What to Load1314| If you're... | Load |15|--------------|------|16| Creating types, seeing `String` or `i32` where domain types fit | `references/type-safety.md` |17| Handling errors, seeing `.unwrap()` or `.expect()` | `references/error-handling.md` |18| Using `bool` parameters, designing enums | `references/enum-design.md` |19| Designing public APIs, builders, trait implementations | `references/api-design.md` |20| Adding a `pub use`, choosing where a public item lives, re-exports, preludes | `references/re-exports.md` |21| Choosing the lint set or the unsafe policy | `references/lint-setup.md` |22| Cargo workspace setup: inheriting package/lints/dependencies across members, `[workspace.*]`, version drift, scaffolding a new member crate | `references/workspace.md` |23| A lint fired; considering suppression; `#[allow]`/`#[expect]`; `dead_code`; `unfulfilled_lint_expectations` | `references/responding-to-lints.md` |24| Structuring code, applying design patterns | `references/patterns.md` |25| Organizing modules, separating pure logic from I/O | `references/fcis.md` |26| Unsure how to use a crate, need documentation | `references/finding-docs.md` |27| Writing or running tests, choosing test strategy | `references/testing.md` |28| Using async/await, tokio, channels, spawn, timeout | `references/async.md` |29| Choosing mutex types, graceful shutdown, cancellation | `references/async.md` |30| Lifetime annotations, `'static`, HRTBs | `references/lifetimes.md` |31| Writing or reviewing unsafe code, FFI, MaybeUninit | `references/unsafe.md` |32| `Send`/`Sync` bounds, thread safety, Arc/Mutex | `references/send-sync.md` |33| Serde, JSON serialization, derive attributes | `references/serde.md` |34| Building a JSON/message payload you own — reaching for `serde_json::json!` or `serde_json::to_vec` of an ad-hoc value | `references/serde.md` |35| Choosing a serialization stack (serde vs facet) | `references/serde.md` |36| `dead_code` on `Deserialize` structs, DTO dead fields | `references/serde.md` |37| Using aliri_braid, seeing `new()` conflicts or `Infallible` errors | `references/type-safety.md` |3839Deep facet-ecosystem work (`#[derive(Facet)]`, facet-json/csv, figue, strid, rediff) is covered by the separate `facet` skill, which loads on its own triggers — never reference it as a `references/` path.4041## Error Message → Reference4243| If you see... | Load |44|---------------|------|45| "future cannot be sent between threads safely" | `references/send-sync.md` + `references/async.md` |46| "cannot be shared between threads safely" | `references/send-sync.md` |47| "borrowed value does not live long enough" | `references/lifetimes.md` |48| "does not live long enough" | `references/lifetimes.md` |49| "missing lifetime specifier" | `references/lifetimes.md` |50| "the trait `Send` is not implemented" | `references/send-sync.md` |51| "holding across an await point" | `references/async.md` |52| "`MutexGuard` held across await" | `references/async.md` |53| "higher-ranked lifetime error" | `references/lifetimes.md` (HRTBs) |54| "cannot infer type" with serde | `references/serde.md` |55| "duplicate definitions with name `new`" near a braid type | `references/type-safety.md` (aliri_braid gotchas) |5657## Core Principles5859**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.6061**Error Handling:** `thiserror` for libraries, `color_eyre` for applications. Reserve `.expect()` for initialization only. **Never** `.unwrap()` or `.expect()` in production runtime code.6263**Enums over Bools:** `enum Visibility { Public, Private }` > `is_public: bool`.6465**No stringly-typed dispatch:** a closed vocabulary your own system produces is an enum. Deserialize it at the serde/storage boundary (`rename_all`) so unknown tokens die there; match the enum exhaustively; write each wire token in exactly one `as_str`-style method. `match x.as_str()` outside a boundary converter (a `FromStr` body, a SQL row edge) means the type was lost upstream — recover it there. An edit-time hook in this plugin flags fresh occurrences.6667**Make Illegal States Unrepresentable:** Use the type system to prevent invalid data.6869**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.7071**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`.7273**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.7475**Suppress with `#[expect]`, never `#[allow]` in source.** `allow` belongs only in `Cargo.toml` config; source uses `#[expect(lint, reason = "…")]` only. See `references/lint-setup.md` (config) and `references/responding-to-lints.md` (mechanics).7677## STOP — Anti-Rationalization Table7879Before writing code that matches these patterns, STOP and reconsider.8081| You're about to... | Common rationalization | What to do instead |82|---------------------|------------------------|--------------------|83| 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. |84| Match a string against literals (`match x.as_str()`, `x == "…"`, `let Some("…")`) | "The surrounding code already does it" / "it's just one comparison" | The type was lost upstream — recover it (serde enum at the boundary). Surrounding code embodying the anti-pattern is the reason to fix it, not license to extend it. |85| Use `mem::transmute` | "I know the layout" | You probably don't. Use `from_ne_bytes`, `bytemuck`, or `zerocopy` instead. Load `references/unsafe.md`. |86| Suppress a lint instead of fixing the code | "It's just a style lint" / "more readable this way" | **Fix the code.** Suppression is only for structural constraints you can't change. Load `references/responding-to-lints.md`. |87| Add `#[allow(dead_code)]` | "Conditionally dead — used in tests" / "Not used yet" | If only tests use it, it IS dead — delete it. `#[expect(dead_code, reason = "…")]` is for interim work only. Load `references/responding-to-lints.md`. |88| Add a module-wide `#![expect(…)]` or suppress `unfulfilled_lint_expectations` | "One suppression covers the module" / "the expectation warning is noise" | The warning means an `#[expect]` is stale or mis-scoped — delete it or `cfg_attr` it to the cfg where the lint fires. Load `references/responding-to-lints.md`. |89| Re-export an item at an additional public path | "Shorter import" / "easier to find" | Every public item gets ONE canonical path; root promotion is for marquee items only. Load `references/re-exports.md`. |90| Declare a dependency version in a workspace member crate | "Only this crate uses it" | Hoist to `[workspace.dependencies]`; per-member versions drift and can duplicate in the tree. Load `references/workspace.md`. |9192## Authoritative Resources9394- [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) — the library team's design conventions95- [Clippy lint index](https://rust-lang.github.io/rust-clippy/master/index.html) — searchable list of every lint96- [Microsoft Pragmatic Rust Guidelines](https://microsoft.github.io/rust-guidelines/) — agent-consultable corpus (`agents/all.txt`)