dotmd
// 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
    CLAUDE.md
// File Content
CLAUDE.md
1# CLAUDE.md — Rust
2
3You are working in a Rust codebase. Follow these conventions exactly.
4
5## Build & Check Commands
6
7```bash
8cargo check # Fast type-checking — run this first, always
9cargo build # Debug build
10cargo build --release # Release build (optimized)
11cargo test # All tests (unit + integration + doc tests)
12cargo test -p <crate> # Test a single workspace member
13cargo test <test_name> # Run a specific test by name
14cargo clippy -- -D warnings # Lint — treat all warnings as errors
15cargo fmt --check # Verify formatting without modifying
16cargo fmt # Auto-format
17cargo doc --no-deps --open # Build and view docs
18```
19
20Run `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.
21
22If this is a workspace (multiple `[[members]]` in root `Cargo.toml`), prefer `cargo check -p <crate>` for faster feedback during focused work.
23
24## Error Handling
25
26This is the single most important Rust convention. Get it right.
27
28**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 variants
32- Expose error variants that callers can meaningfully act on; collapse internal details
33
34```rust
35use thiserror::Error;
36
37#[derive(Debug, Error)]
38pub enum Error {
39 #[error("invalid configuration: {0}")]
40 Config(String),
41
42 #[error("connection failed: {addr}")]
43 Connection { addr: String, #[source] source: std::io::Error },
44
45 #[error(transparent)]
46 Io(#[from] std::io::Error),
47}
48```
49
50**Applications (binaries, CLI tools, servers):**
51- Use `anyhow::Result` in `main()` and top-level orchestration
52- 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 variants
54- Never print errors manually and then return `Ok(())` — let the error propagate
55
56**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 only
60
61## Module Structure & Visibility
62
63```
64src/
65├── lib.rs # Public API surface — re-exports, no logic
66├── error.rs # Crate error type
67├── config.rs # Configuration types
68├── core/ # Internal implementation
69│ ├── mod.rs
70│ └── engine.rs
71└── util.rs # Internal helpers
72```
73
74- `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`.
79
80## Crate Choices
81
82Use these crates. Don't reinvent what they provide.
83
84| 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"] }` |
97
98Don't add crates that duplicate std functionality. Check `std` first — `std::fs`, `std::collections`, `std::path` cover a lot.
99
100## Testing
101
102**Unit tests** go in the same file as the code, inside a `#[cfg(test)]` module:
103
104```rust
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn parses_valid_input() {
111 let result = parse("hello").unwrap();
112 assert_eq!(result.value, "hello");
113 }
114}
115```
116
117**Integration tests** go in `tests/` at the crate root. Each file is a separate test binary:
118
119```
120tests/
121├── integration.rs # or split by feature area:
122├── api_tests.rs
123└── common/
124 └── mod.rs # Shared test helpers (this pattern avoids cargo treating it as a test)
125```
126
127**Doc tests** go on public API items. They serve as both documentation and tests:
128
129```rust
130/// Parses the input string into a [`Config`].
131///
132/// # Examples
133///
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```
142
143**Test guidelines:**
144- Use `#[test]` for sync, `#[tokio::test]` for async tests
145- Prefer `assert_eq!` and `assert_ne!` over `assert!` — better error messages
146- Test error cases, not just happy paths. Use `assert!(result.is_err())` or match on specific error variants
147- Name tests descriptively: `fn rejects_empty_input()` not `fn test1()`
148- Don't use `#[should_panic]` when you can match on `Result::Err` instead
149
150## Patterns to Follow
151
152**Use iterators instead of manual loops:**
153
154```rust
155// Yes
156let names: Vec<_> = users.iter().filter(|u| u.active).map(|u| &u.name).collect();
157
158// No
159let mut names = Vec::new();
160for user in &users {
161 if user.active {
162 names.push(&user.name);
163 }
164}
165```
166
167**Use `impl Trait` in argument position for flexibility:**
168
169```rust
170pub fn load(path: impl AsRef<Path>) -> Result<Config, Error> {
171 let content = std::fs::read_to_string(path.as_ref())?;
172 // ...
173}
174```
175
176**Builder pattern for complex construction:**
177
178```rust
179pub struct ServerBuilder {
180 port: u16,
181 host: String,
182 max_connections: Option<usize>,
183}
184
185impl ServerBuilder {
186 pub fn new(port: u16) -> Self {
187 Self { port, host: "127.0.0.1".into(), max_connections: None }
188 }
189
190 pub fn host(mut self, host: impl Into<String>) -> Self {
191 self.host = host.into();
192 self
193 }
194
195 pub fn max_connections(mut self, n: usize) -> Self {
196 self.max_connections = Some(n);
197 self
198 }
199
200 pub fn build(self) -> Server {
201 Server { port: self.port, host: self.host, max_connections: self.max_connections.unwrap_or(100) }
202 }
203}
204```
205
206**Enum design — make invalid states unrepresentable:**
207
208```rust
209// Yes — the type system enforces valid states
210enum ConnectionState {
211 Disconnected,
212 Connecting { attempt: u32 },
213 Connected { session_id: String },
214}
215
216// No — boolean flags with implicit invariants
217struct Connection {
218 is_connected: bool,
219 is_connecting: bool,
220 session_id: Option<String>, // Only valid when connected? Who knows
221}
222```
223
224**Pattern matching — be exhaustive, avoid wildcards on enums you control:**
225
226```rust
227// Yes — compiler catches new variants
228match 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}
234
235// No — silently ignores new variants
236match state {
237 ConnectionState::Connected { session_id } => send(session_id),
238 _ => {} // What about Connecting? What about future variants?
239}
240```
241
242## Trait Design
243
244- 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.
249
250## Performance Defaults
251
252- 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`).
260
261## Unsafe Code
262
263- 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.
266
267## Clippy & Formatting
268
269Respect existing `clippy.toml`, `rustfmt.toml`, or `rust-toolchain.toml` if present. If not:
270
271- Run `cargo clippy -- -D warnings` — zero warnings policy
272- Common useful lints to enable in `Cargo.toml` or `lib.rs`:
273
274```rust
275#![warn(clippy::pedantic)]
276#![allow(clippy::module_name_repetitions)] // Too noisy for module::ModuleThing
277#![allow(clippy::must_use_candidate)] // Not every fn needs #[must_use]
278```
279
280- `cargo fmt` uses default rustfmt settings unless a `rustfmt.toml` exists. Don't fight the formatter.
281
282## What NOT to Do
283
284**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.
285
286**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.
287
288**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.
289
290**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.
291
292**Don't use `println!` for logging.** Use `tracing` or at minimum `eprintln!` for diagnostic output in binaries. Libraries should never print directly.
293
294**Don't make everything `pub`.** Start private. Promote to `pub(crate)`. Promote to `pub` only when there's a consumer.
295
296**Don't implement `Display` by hand when `thiserror` can derive it.** The `#[error("...")]` attribute generates `Display` — that's the whole point.
297
298**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."
299
300**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.
301
302## Cargo.toml Conventions
303
304```toml
305[package]
306name = "mycrate"
307version = "0.1.0"
308edition = "2021"
309rust-version = "1.75" # Set MSRV explicitly
310
311[dependencies]
312serde = { version = "1", features = ["derive"] }
313tokio = { version = "1", features = ["macros", "rt-multi-thread"] } # Minimal features
314
315[dev-dependencies]
316assert_matches = "1"
317tempfile = "3"
318
319[lints.clippy]
320pedantic = { level = "warn", priority = -1 }
321module_name_repetitions = "allow"
322must_use_candidate = "allow"
323```
324
325- 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.
328
329## When You're Unsure
330
331Read 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