Marketplace senior-rust-practices
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", "rust project", "scalable rust", "rust CI setup", or needs guidance on senior-level Rust development patterns, workspace design, code organization strategies, or production-ready Rust architectures.
git clone https://github.com/aiskillstore/marketplace
T=$(mktemp -d) && git clone --depth=1 https://github.com/aiskillstore/marketplace "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/clementwalter/senior-rust-practices" ~/.claude/skills/aiskillstore-marketplace-senior-rust-practices && rm -rf "$T"
skills/clementwalter/senior-rust-practices/SKILL.mdSenior Rust Development Practices
Battle-tested patterns for Rust workspace architecture, code organization, dependencies, and testing that scale from prototype to production.
Git Worktree Workflow Compliance
All coding work MUST happen in git worktrees. Before making any code changes:
- Create a worktree:
git worktree add ~/.claude/worktrees/$(basename $(pwd))/<task> -b feat/<task> - Work in that directory
- Use
to consolidate changes back to main/merge
Never edit files directly in the main worktree.
Completion Requirements
Before completing ANY Rust task, you MUST:
- Run tests:
cargo test --workspace - Run linting:
trunk check - Fix any issues before declaring done
If trunk has formatting issues, run
trunk fmt to auto-fix.
Workspace Architecture
Start from "One Product = One Repo = One Workspace"
Use a Rust workspace when you have:
- Multiple crates that ship together (binary + libraries)
- Shared tooling / CI
- Shared versioning policy
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
Keep Crates "Thin" and Boundaries "Hard"
Layered architecture:
- core: Pure logic, types, validation, algorithms. Minimal deps.
- adapters: IO boundaries (db, network, rpc, filesystem). Trait-based boundary, minimal leakage.
- app / service: Wiring (DI), config, runtime, orchestration.
- bins: CLI/daemon that just calls "app".
Critical rule: If
core imports tokio, reqwest, or sqlx, you've already lost the separation.
Default to a Small Number of Crates
Too many crates is busywork. Start with 2–5 max.
Split only when:
- Compile times are painful and boundaries are real
- You need separate release cadence
- You need different dependency profiles (no-std, wasm, etc.)
Workspace Dependencies: Centralize Versions, Not Architecture
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.
Be Ruthless with Features
- Prefer additive features (enable more capabilities) vs "feature flags that change semantics"
- Put "heavy" deps behind features (db, http, metrics)
- Avoid default features that pull the world
Pattern for optional dependencies:
[dependencies] sqlx = { workspace = true, optional = true } [features] db = ["dep:sqlx"]
Enforce a Policy: MSRV + Toolchain
- Pin toolchain with
rust-toolchain.toml - Decide MSRV (minimum supported Rust version) and test it in CI
- Keep clippy/rustfmt consistent
Code Organization
Modules Should Match How You Reason, Not File-Per-Type
Organize 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
Public API: Small Surface Area, Explicit Re-Exports
- Make most things
by defaultpub(crate) - Re-export a curated API from
lib.rs
mod payment; pub use payment::{Payment, PaymentError};
If everything is
pub you've created an accidental framework.
Avoid "Prelude" Unless You Truly Need It
Preludes tend to hide dependencies and make code review harder. Prefer explicit imports.
Error Strategy: Pick One and Stick to It
Common approach:
- Library crates:
for typed errorsthiserror - Binaries:
at the top levelanyhow
Don't leak
anyhow::Error across library boundaries unless you explicitly want "opaque".
Keep Async at the Edge
If you can keep core synchronous and pure, you gain:
- Simpler tests
- Portability
- Less lifetime/pinning headaches
Dependency Hygiene
Be Picky: Fewer Deps, Higher-Quality Deps
Every dependency adds:
- Build time
- Audit surface
- Semver risk
Prefer "boring" crates with strong maintenance.
Use cargo-deny
+ cargo-audit
cargo-denycargo-auditMake dependency issues visible early (licenses, advisories, duplicate versions).
Don't Use unwrap()
in Libraries
unwrap()In binaries/tests it's fine (especially in test scaffolding). In libraries, return errors with context.
Testing Strategy That Scales
Think "pyramid":
1. Unit Tests: Fast, Deterministic, Lots
- Put most tests close to code:
in the same file for private accessmod tests {} - Test invariants and edge cases, not just happy paths
- Avoid hitting the filesystem/network in unit tests
2. Integration Tests: Black-Box the Public API
Use
crates/<crate>/tests/*.rs for API-level tests.
- Treat it as "a consumer of the crate"
- Don't reach into private internals
3. End-to-End Tests: Few, But Real
If you have a service:
- Spin up dependencies (db) in CI (containers)
- Run a small set of scenario tests
4. Property Tests + Fuzzing When Correctness Matters
for invariants ("decode(encode(x)) == x")proptest
for parsers/decoders/inputs from outsidecargo-fuzz
5. Doctests Are Underrated
Doctests enforce that examples compile and keep your public API honest.
Logging and Tracing
Never Use println!
- Use Tracing Instead
println!NEVER use
, println!
, or eprintln!
for output. Always use the dbg!
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:
- Structured logging with levels (filter noise in production)
- Spans for distributed tracing
- Configurable output (JSON, pretty, etc.)
- Zero-cost when disabled
Use test-log
for Tests
test-logAlways 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 Rules to Follow
Inline Format Arguments (clippy::uninlined_format_args
)
clippy::uninlined_format_argsAlways 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.
CI / Quality Gates (Minimum Set)
cargo fmt --check cargo clippy --all-targets --all-features -D warnings cargo test --workspace --all-features
Additional gates:
- MSRV check (if you claim one)
/cargo denycargo audit- (optional)
for coverage, but don't worship %cargo llvm-cov
Compile Times and Ergonomics
- Use
and avoid unnecessary default featuresresolver = "2" - Split "heavy" crates (like DB codegen, protobuf) into separate crates if they dominate rebuild time
- Prefer incremental-friendly patterns: fewer proc-macros, fewer generics in hot paths unless needed
Practical Rules of Thumb
One-way dependencies:
→ (nothing)core
→adapterscore
→app
+adapterscore
→binapp
Visibility:
- Everything private by default
- Public API is a deliberate design artifact
IO placement:
- No IO in
core
Test distribution:
- Unit tests everywhere
- Integration tests at boundaries
- E2E tests sparingly
Tooling:
- Pin toolchain
- Centralize versions
- Police features
Project-Type Patterns
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