Stack conventions
Rust
Rust 2021/2024 crates (services, command-line tools and libraries) built with Cargo, linted with Clippy and tested with cargo test.
Rust 2021/2024 crates (services, command-line tools and libraries) built with Cargo, linted with Clippy and tested with cargo test.
Used for: rust.
Principles
- Design APIs that borrow when they only read (&str, &[T], &T, impl AsRef) and take ownership only when they store or consume.
- Make invalid states unrepresentable with enums and newtypes; parse untrusted data into validated types at the boundary.
- Return Result for recoverable failures; never unwrap, expect or panic in library code paths that callers can trigger.
- Libraries define precise error enums (thiserror); binaries aggregate with context (anyhow or eyre).
- Prefer iterators and combinators over manual indexing; let the compiler remove bounds checks and keep intent clear.
- Keep unsafe minimal, encapsulated behind a safe API, and justify every unsafe block with a // SAFETY comment.
- Treat cargo clippy warnings as errors in CI and keep the code rustfmt-clean.
- Implement standard traits eagerly (Debug, Clone, PartialEq, Eq, Hash, Default, Display, From) where they make sense.
- Do not block inside async code; use the runtime's async I/O and spawn_blocking for CPU-bound or blocking work.
- Mark functions whose result must be used with #[must_use]; builders and pure computations especially.
Conventions
Accessibility
Concurrency
- Make types Send + Sync by construction; share state with Arc<Mutex<>> or Arc<RwLock<>> only when message passing (channels) is not simpler.
- Never hold a std::sync::Mutex guard across an .await; use a scoped block or tokio::sync::Mutex when it must be held.
- Use tokio::task::spawn_blocking for blocking I/O or CPU-heavy work inside async contexts.
- Propagate cancellation; structure tasks with JoinSet and stop them on shutdown.
Dependencies
- Commit Cargo.lock for applications (and, per current Cargo guidance, it is fine for libraries too); declare rust-version (MSRV) in Cargo.toml.
- Minimise default features of dependencies (default-features = false) and expose your own optional features additively.
- Set the edition explicitly (edition = "2024" for new crates on Rust 1.85+).
- Configure lints in [lints] / [workspace.lints] in Cargo.toml rather than scattered crate attributes.
Documentation
- Every public item has a /// doc comment with a summary line; crate-level docs in lib.rs with //!.
- Include "# Errors", "# Panics" and "# Safety" sections where they apply (clippy::missing_errors_doc, missing_panics_doc).
- Write runnable examples in doc comments; use #![warn(missing_docs)] in libraries.
Errors
- No unwrap/expect in library code except where an invariant is proven; then use expect("reason the invariant holds").
- Propagate with ?; add context in applications with anyhow::Context::context or with_context.
- Public error types implement std::error::Error, Display, Debug, Send and Sync, and are #[non_exhaustive] when they may grow.
- Do not use panics for control flow; document panics under a "# Panics" heading.
- Do not stringify errors early; keep the source chain (#[source] / #[from]).
Logging
- Use the tracing crate (spans plus structured fields) in async services; log in libraries only through tracing or log facades, never println!.
- Instrument request handlers with #[tracing::instrument(skip(secret_args))] and skip large or sensitive arguments.
- Initialise the subscriber once in main; libraries never install a global subscriber.
Naming
- Follow RFC 430 casing; UpperCamelCase types and traits, snake_case functions, SCREAMING_SNAKE_CASE constants.
- Conversion methods follow as_ (cheap borrow), to_ (expensive), into_ (consuming) prefixes.
- Getters have no get_ prefix (fn len(&self), fn name(&self) -> &str).
- Iterator-producing methods are iter, iter_mut and into_iter; constructors are new or with_*.
- Feature names describe what they enable, without use- or with- prefixes.
Performance
- Avoid needless clone() and to_string(); borrow, or use Cow when ownership is conditional.
- Preallocate with Vec::with_capacity when the size is known; prefer iterators over collect-then-iterate.
- Benchmark with criterion (or divan) before optimising; profile release builds, not debug.
- Use Box
only where dynamic dispatch is needed; avoid Rc<RefCell<_>> webs, restructure ownership instead.
Security
- Every unsafe block has a // SAFETY comment explaining why the invariants hold; enable clippy::undocumented_unsafe_blocks.
- Use #![forbid(unsafe_code)] in crates that do not need unsafe.
- Audit dependencies with cargo audit or cargo deny (advisories, licences, duplicate versions, banned crates).
- Use parameterised queries (sqlx query! macros or bind parameters); never format SQL strings.
- Zeroise secrets in memory where it matters (zeroize, secrecy crates) and never derive Debug that prints them.
- Validate untrusted input at the boundary by parsing it into types that cannot hold invalid values (newtypes with
TryFrom), with size limits on what is read.
Structure
- Keep lib.rs as the crate's public facade; re-export the public API deliberately with pub use; keep everything else pub(crate) or private.
- Thin main.rs that parses arguments and calls into the library crate so logic is testable.
- Use a Cargo workspace with workspace.dependencies and workspace.lints for multi-crate repositories.
- Unit tests in a #[cfg(test)] mod tests in the same file; integration tests in tests/; examples in examples/.
- Prefer module files (foo.rs with foo/ subdirectory) over mod.rs for new code.
Testing
- Unit tests next to the code; integration tests in tests/ exercise only the public API.
- Doc tests double as examples; keep them compiling (cargo test runs them).
- Use property-based tests (proptest or quickcheck) for parsers and invariants; cargo-fuzz for untrusted input decoders.
- Use #[tokio::test] (or the runtime's equivalent) for async tests; avoid sleeping in tests, use time pausing where supported.
- Run tests with --all-features and with default features in CI when features change behaviour.
- Name test functions after the behaviour (
rejects_order_without_lines); one behaviour per test. - Done means tested: new or changed behaviour ships with tests in the same change, and a fixed bug gets a regression test that fails without the fix.
Review checklist
- Does cargo clippy --all-targets --all-features -- -D warnings pass, and is cargo fmt --check clean?
- Is there any unwrap/expect/panic reachable from library or request code without a proven invariant?
- Does every unsafe block have a // SAFETY comment, and could the code avoid unsafe entirely?
- Do library errors use a typed enum (thiserror) and application errors carry context (anyhow)?
- Do functions borrow (&str, &[T]) when they only read, avoiding unnecessary clone or to_string?
- Are domain values modelled with enums/newtypes rather than loose strings and integers?
- Are standard traits (Debug, Clone, PartialEq, Default, Display) implemented where useful?
- Is #[must_use] applied to builders and functions whose result must not be ignored?
- In async code, are there no blocking calls and no locks held across .await?
- Are public items documented with Errors/Panics/Safety sections where relevant?
- Are new tests present (unit, integration or doc), including error paths?
- Are new dependencies justified, with minimal features and no duplicate major versions?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
cargo-fmt |
format | rustfmt | no |
cargo-clippy |
lint | clippy | no |
cargo-deny |
security | cargo-deny | no |
cargo-audit |
security | cargo-audit | no |
cargo-doc |
static | rustdoc | no |
gitleaks |
security | gitleaks | no |