This skill should be used when the user asks about "rust workspace", "rust best practices", "cargo workspace setup", "rust code organization", "rust dependency management", "rust testing strategy",...
Battle-tested patterns for Rust workspace architecture, code organization, dependencies, and testing that scale from prototype to production.
All coding work MUST happen in git worktrees. Before making any code changes:
git worktree add ~/.claude/worktrees/$(basename $(pwd))/<task> -b feat/<task>/merge to consolidate changes back to mainNever edit files directly in the main worktree.
Before completing ANY Rust task, you MUST:
cargo test --workspacetrunk checkIf trunk has formatting issues, run trunk fmt to auto-fix.
Use a Rust workspace when you have:
Canonical workspace structure:
repo/
Cargo.toml # workspace root
crates/
core/ # pure domain logic (no IO)
storage/ # DB, filesystem, etc.
api/ # HTTP/GRPC handlers, DTOs
cli/ # binary
tools/ # optional: internal binaries (codegen, migration, etc.)
tests/ # optional: black-box integration tests
Layered architecture:
Critical rule: If core imports tokio, reqwest, or sqlx, you've already lost the separation.
Too many crates is busywork. Start with 2–5 max.
Split only when:
In root Cargo.toml, use workspace dependencies to keep versions aligned:
[workspace]
members = ["crates/*"]
resolver = "2"
[workspace.dependencies]
anyhow = "*" # use latest
thiserror = "*" # use latest
serde = { version = "*", features = ["derive"] } # use latest
tokio = { version = "*", features = ["macros", "rt-multi-thread"] } # use latest
In crate Cargo.toml:
[dependencies]
serde = { workspace = true }
This reduces version drift and security churn.
Pattern for optional dependencies:
[dependencies]
sqlx = { workspace = true, optional = true }
[features]
db = ["dep:sqlx"]
rust-toolchain.tomlOrganize by capability / domain, not by "models/handlers/utils" spaghetti.
Good organization:
core/
src/
lib.rs
payment/
mod.rs
validation.rs
pricing.rs
user/
mod.rs
id.rs
rules.rs
Avoid:
models.rs
handlers.rs
utils.rs
pub(crate) by defaultlib.rsmod payment;
pub use payment::{Payment, PaymentError};
If everything is pub you've created an accidental framework.
Preludes tend to hide dependencies and make code review harder. Prefer explicit imports.
Common approach:
thiserror for typed errorsanyhow at the top levelDon't leak anyhow::Error across library boundaries unless you explicitly want "opaque".
If you can keep core synchronous and pure, you gain:
Every dependency adds:
Prefer "boring" crates with strong maintenance.
cargo-deny + cargo-auditMake dependency issues visible early (licenses, advisories, duplicate versions).
unwrap() in LibrariesIn binaries/tests it's fine (especially in test scaffolding). In libraries, return errors with context.
Think "pyramid":
mod tests {} in the same file for private accessUse crates/<crate>/tests/*.rs for API-level tests.
If you have a service:
proptest for invariants ("decode(encode(x)) == x")cargo-fuzz for parsers/decoders/inputs from outsideDoctests enforce that examples compile and keep your public API honest.
println! - Use Tracing InsteadNEVER use println!, eprintln!, or dbg! for output. Always use the tracing crate:
use tracing::{debug, info, warn, error, trace};
// Good - structured logging
info!("Processing request for user {user_id}");
debug!("Cache hit: {key}");
warn!("Retry attempt {attempt} of {max_retries}");
error!("Failed to connect: {err}");
// Bad - never do this
println!("Processing request for user {}", user_id);
dbg!(value);
Why:
test-log for TestsAlways use test_log::test attribute for tests to capture tracing output:
use test_log::test;
#[test]
fn test_something() {
info!("This will be visible when test fails or with --nocapture");
assert!(true);
}
#[test(tokio::test)]
async fn test_async_something() {
debug!("Async test with tracing");
}
Add to Cargo.toml (use latest versions):
[dev-dependencies]
test-log = { version = "*", features = ["trace"] } # use latest
tracing-subscriber = { version = "*", features = ["env-filter"] } # use latest
Run tests with visible logs: RUST_LOG=debug cargo test -- --nocapture
clippy::uninlined_format_args)Always use variables directly in format strings instead of passing them as arguments:
// Good - variable inlined
let name = "world";
info!("Hello, {name}!");
format!("Value: {value}, Count: {count}")
// Bad - uninlined arguments
info!("Hello, {}!", name);
format!("Value: {}, Count: {}", value, count)
This improves readability and reduces potential argument ordering mistakes.
cargo fmt --check
cargo clippy --all-targets --all-features -D warnings
cargo test --workspace --all-features
Additional gates:
cargo deny / cargo auditcargo llvm-cov for coverage, but don't worship %resolver = "2" and avoid unnecessary default featuresOne-way dependencies:
core → (nothing)adapters → coreapp → adapters + corebin → appVisibility:
IO placement:
coreTest distribution:
Tooling:
CLI: Thin binary → lib (for testability)
Services: Separate protocol definitions; feature-flag transport layers
ZK/crypto: Isolate no_std core; separate proving/verification crates
WASM: Separate bindings; platform-agnostic core