// Config Record
>Rust
Claude Code instructions for Rust projects covering idiomatic error handling, module design, testing, and performance patterns.
author:
dotmd Team
license:CC0
published:Feb 23, 2026
// Installation
>Add this file to your project repository:
- Claude Code--path=
CLAUDE.md
// File Content
CLAUDE.md
1# CLAUDE.md — Rust23You are working in a Rust codebase. Follow these conventions exactly.45## Build & Check Commands67```bash8cargo check # Fast type-checking — run this first, always9cargo build # Debug build10cargo build --release # Release build (optimized)11cargo test # All tests (unit + integration + doc tests)12cargo test -p <crate> # Test a single workspace member13cargo test <test_name> # Run a specific test by name14cargo clippy -- -D warnings # Lint — treat all warnings as errors15cargo fmt --check # Verify formatting without modifying16cargo fmt # Auto-format17cargo doc --no-deps --open # Build and view docs18```1920Run `cargo check` after every change before moving on. It's faster than `build` and catches most errors. Run `cargo clippy -- -D warnings` before considering any task done.2122If this is a workspace (multiple `[[members]]` in root `Cargo.toml`), prefer `cargo check -p <crate>` for faster feedback during focused work.2324## Error Handling2526This is the single most important Rust convention. Get it right.2728**Libraries (anything published as a crate):**29- Define a crate-level error enum in `src/error.rs` using `thiserror`30- Every public fallible function returns `Result<T, CrateError>`31- Never use `anyhow` in library code — callers need to match on error variants32- Expose error variants that callers can meaningfully act on; collapse internal details3334```rust35use thiserror::Error;3637#[derive(Debug, Error)]38pub enum Error {39 #[error("invalid configuration: {0}")]40 Config(String),4142 #[error("connection failed: {addr}")]43 Connection { addr: String, #[source] source: std::io::Error },4445 #[error(transparent)]46 Io(#[from] std::io::Error),47}48```4950**Applications (binaries, CLI tools, servers):**51- Use `anyhow::Result` in `main()` and top-level orchestration52- Use `anyhow::Context` to add human-readable context to errors: `.context("failed to load config")?`53- It's fine to use `thiserror` for domain errors even in apps when you need to match on variants54- Never print errors manually and then return `Ok(())` — let the error propagate5556**Universal rules:**57- Never use `.unwrap()` or `.expect()` outside of tests and infallible cases (e.g., static regex, hardcoded values proven at compile time)58- Use `?` for propagation. If the types don't align, add a `From` impl or use `.map_err()`59- If you're tempted to `.unwrap()` because "it can't fail," add a comment explaining why, and still prefer `.expect("reason")` in binary code only6061## Module Structure & Visibility6263```64src/65├── lib.rs # Public API surface — re-exports, no logic66├── error.rs # Crate error type67├── config.rs # Configuration types68├── core/ # Internal implementation69│ ├── mod.rs70│ └── engine.rs71└── util.rs # Internal helpers72```7374- `lib.rs` defines `pub mod` declarations and `pub use` re-exports. Keep it thin.75- Default to private. Mark fields and functions `pub` only when they're part of the intended API.76- Use `pub(crate)` for items shared across modules but not exported.77- Never use `pub use *` glob re-exports — be explicit about what's in the public API.78- For binaries: `main.rs` should be thin — parse args, set up logging, call into `lib.rs`.7980## Crate Choices8182Use these crates. Don't reinvent what they provide.8384| Need | Crate | Notes |85|---|---|---|86| Serialization | `serde` + `serde_json`/`toml` | Derive `Serialize`/`Deserialize`. Use `#[serde(rename_all = "camelCase")]` for JSON APIs |87| CLI args | `clap` (derive) | Use the derive API, not the builder API. Add `#[command(about, version)]` |88| Async runtime | `tokio` | Use `#[tokio::main]` for binaries. Feature-gate: `tokio = { features = ["full"] }` only in binaries, minimal features in libraries |89| HTTP client | `reqwest` | Use the async API with `tokio` |90| HTTP server | `axum` | Tower-based, pairs naturally with `tokio` |91| Logging/tracing | `tracing` + `tracing-subscriber` | Use `tracing::info!()`, not `println!()` or `log` crate. Structured fields: `tracing::info!(user_id = %id, "request received")` |92| Error handling | `thiserror` (lib) / `anyhow` (bin) | See Error Handling section above |93| Date/time | `chrono` or `time` | `time` is lighter; `chrono` has broader ecosystem support |94| Regex | `regex` | Compile patterns once with `std::sync::LazyLock` (1.80+) or `once_cell::sync::Lazy` |95| Randomness | `rand` | Don't implement your own |96| UUID | `uuid` | Use `uuid = { features = ["v4"] }` |9798Don't add crates that duplicate std functionality. Check `std` first — `std::fs`, `std::collections`, `std::path` cover a lot.99100## Testing101102**Unit tests** go in the same file as the code, inside a `#[cfg(test)]` module:103104```rust105#[cfg(test)]106mod tests {107 use super::*;108109 #[test]110 fn parses_valid_input() {111 let result = parse("hello").unwrap();112 assert_eq!(result.value, "hello");113 }114}115```116117**Integration tests** go in `tests/` at the crate root. Each file is a separate test binary:118119```120tests/121├── integration.rs # or split by feature area:122├── api_tests.rs123└── common/124 └── mod.rs # Shared test helpers (this pattern avoids cargo treating it as a test)125```126127**Doc tests** go on public API items. They serve as both documentation and tests:128129```rust130/// Parses the input string into a [`Config`].131///132/// # Examples133///134/// ```135/// use mycrate::parse;136///137/// let config = parse("key=value").unwrap();138/// assert_eq!(config.key, "key");139/// ```140pub fn parse(input: &str) -> Result<Config, Error> {141```142143**Test guidelines:**144- Use `#[test]` for sync, `#[tokio::test]` for async tests145- Prefer `assert_eq!` and `assert_ne!` over `assert!` — better error messages146- Test error cases, not just happy paths. Use `assert!(result.is_err())` or match on specific error variants147- Name tests descriptively: `fn rejects_empty_input()` not `fn test1()`148- Don't use `#[should_panic]` when you can match on `Result::Err` instead149150## Patterns to Follow151152**Use iterators instead of manual loops:**153154```rust155// Yes156let names: Vec<_> = users.iter().filter(|u| u.active).map(|u| &u.name).collect();157158// No159let mut names = Vec::new();160for user in &users {161 if user.active {162 names.push(&user.name);163 }164}165```166167**Use `impl Trait` in argument position for flexibility:**168169```rust170pub fn load(path: impl AsRef<Path>) -> Result<Config, Error> {171 let content = std::fs::read_to_string(path.as_ref())?;172 // ...173}174```175176**Builder pattern for complex construction:**177178```rust179pub struct ServerBuilder {180 port: u16,181 host: String,182 max_connections: Option<usize>,183}184185impl ServerBuilder {186 pub fn new(port: u16) -> Self {187 Self { port, host: "127.0.0.1".into(), max_connections: None }188 }189190 pub fn host(mut self, host: impl Into<String>) -> Self {191 self.host = host.into();192 self193 }194195 pub fn max_connections(mut self, n: usize) -> Self {196 self.max_connections = Some(n);197 self198 }199200 pub fn build(self) -> Server {201 Server { port: self.port, host: self.host, max_connections: self.max_connections.unwrap_or(100) }202 }203}204```205206**Enum design — make invalid states unrepresentable:**207208```rust209// Yes — the type system enforces valid states210enum ConnectionState {211 Disconnected,212 Connecting { attempt: u32 },213 Connected { session_id: String },214}215216// No — boolean flags with implicit invariants217struct Connection {218 is_connected: bool,219 is_connecting: bool,220 session_id: Option<String>, // Only valid when connected? Who knows221}222```223224**Pattern matching — be exhaustive, avoid wildcards on enums you control:**225226```rust227// Yes — compiler catches new variants228match state {229 ConnectionState::Disconnected => reconnect(),230 ConnectionState::Connecting { attempt } if attempt > 3 => give_up(),231 ConnectionState::Connecting { .. } => wait(),232 ConnectionState::Connected { session_id } => send(session_id),233}234235// No — silently ignores new variants236match state {237 ConnectionState::Connected { session_id } => send(session_id),238 _ => {} // What about Connecting? What about future variants?239}240```241242## Trait Design243244- Keep traits small and focused. One method is fine. Two is common. Five is a smell.245- Use default method implementations to reduce boilerplate for implementors.246- Prefer `&self` over `&mut self` in trait methods when possible — it enables sharing.247- Provide a blanket impl for references when it makes sense: `impl<T: MyTrait> MyTrait for &T`.248- If a trait has an obvious "do nothing" implementation, provide it as a `Default` impl or a `Noop` struct.249250## Performance Defaults251252- Prefer `&str` over `String` in function parameters; accept `impl AsRef<str>` if you need flexibility.253- Prefer `&[T]` over `Vec<T>` in function parameters.254- Use `String` and `Vec<T>` for owned data in structs — that's what they're for.255- Use iterators and `.collect()` instead of pre-allocating and pushing. The compiler optimizes this well.256- When you do need pre-allocation: `Vec::with_capacity(n)` if you know the size.257- Never clone to satisfy the borrow checker without trying to restructure first. Cloning is a valid tool but it should be a conscious choice, not a reflex.258- Prefer `Cow<'_, str>` when a function sometimes needs to own and sometimes can borrow.259- Use `Arc<T>` for shared ownership across threads — not `Rc<T>` (which isn't `Send`).260261## Unsafe Code262263- Do not write `unsafe` blocks unless explicitly asked or the task is inherently unsafe (FFI, SIMD, raw pointer manipulation).264- If `unsafe` is necessary, wrap it in a safe abstraction and document the safety invariants with a `// SAFETY:` comment.265- Never use `unsafe` to work around borrow checker issues — that means the design is wrong.266267## Clippy & Formatting268269Respect existing `clippy.toml`, `rustfmt.toml`, or `rust-toolchain.toml` if present. If not:270271- Run `cargo clippy -- -D warnings` — zero warnings policy272- Common useful lints to enable in `Cargo.toml` or `lib.rs`:273274```rust275#![warn(clippy::pedantic)]276#![allow(clippy::module_name_repetitions)] // Too noisy for module::ModuleThing277#![allow(clippy::must_use_candidate)] // Not every fn needs #[must_use]278```279280- `cargo fmt` uses default rustfmt settings unless a `rustfmt.toml` exists. Don't fight the formatter.281282## What NOT to Do283284**Don't clone to escape the borrow checker.** Restructure the code. Split borrows across different struct fields. Use indices instead of references if lifetimes get complex. Cloning is sometimes correct — but it should never be your first move.285286**Don't use `Box<dyn Error>` as a return type.** Use `thiserror` or `anyhow`. `Box<dyn Error>` erases information callers need and doesn't compose well.287288**Don't write Java in Rust.** No `AbstractFactoryProvider` traits. No getter/setter pairs on structs with public fields. No single-method interfaces wrapped in `Arc<Mutex<>>` when a closure would do.289290**Don't litter code with `to_string()` / `to_owned()` / `clone()`.** If every other line is converting types, the function signature is wrong. Accept borrowed types or use generics.291292**Don't use `println!` for logging.** Use `tracing` or at minimum `eprintln!` for diagnostic output in binaries. Libraries should never print directly.293294**Don't make everything `pub`.** Start private. Promote to `pub(crate)`. Promote to `pub` only when there's a consumer.295296**Don't implement `Display` by hand when `thiserror` can derive it.** The `#[error("...")]` attribute generates `Display` — that's the whole point.297298**Don't return `Option` when `Result` is more appropriate.** If the absence has a reason (file not found, parse failure), the caller needs that reason. `Option` means "absent, and that's fine."299300**Don't write `async` functions that never actually await.** If there's no `.await` in the body, it shouldn't be `async`. This adds overhead and confuses callers.301302## Cargo.toml Conventions303304```toml305[package]306name = "mycrate"307version = "0.1.0"308edition = "2021"309rust-version = "1.75" # Set MSRV explicitly310311[dependencies]312serde = { version = "1", features = ["derive"] }313tokio = { version = "1", features = ["macros", "rt-multi-thread"] } # Minimal features314315[dev-dependencies]316assert_matches = "1"317tempfile = "3"318319[lints.clippy]320pedantic = { level = "warn", priority = -1 }321module_name_repetitions = "allow"322must_use_candidate = "allow"323```324325- Use workspace dependencies (`[workspace.dependencies]`) in workspace projects to keep versions in sync.326- Feature-gate heavy dependencies. Don't pull in `tokio` full features in a library.327- Set `rust-version` (MSRV) so downstream consumers know what they need.328329## When You're Unsure330331Read the existing code first. Match the patterns already established. If the project uses `log` instead of `tracing`, follow suit. If it has its own error type without `thiserror`, extend it consistently. The project's conventions override these defaults.332