Rust Development Best Practices
This skill outlines the industry standards and best practices for developing software in Rust.
1. Project Layout (The Cargo Standard)
Follow the standard Cargo project layout strictly:
- •
src/main.rs: Entry point for binary crates. - •
src/lib.rs: Entry point for library crates (and the library part of binaries). - •
src/bin/: Additional binaries. - •
tests/: Integration tests (treats your crate as an external dependency). - •
benches/: Benchmarks (usingcriterionusually). - •
examples/: Example usage code.
Guidelines
- •Split
main.rsandlib.rs: Even for binaries, put most logic inlib.rs.main.rsshould only parse args and call intolib.rs. This makes logic testable. - •Workspaces: For multi-crate projects, use a Cargo Workspace in the root
Cargo.toml.
2. Idiomatic Rust
- •Clippy is Law: Always run
cargo clippy. Fix all warnings. It catches non-idiomatic code. - •Result Handling:
- •Never use
.unwrap()or.expect()in production/library code (recoverable errors). - •Use
?operator for error propagation. - •Use
.expect("...")only in tests or prototypes to document why it shouldn't fail.
- •Never use
- •Iterators: Prefer iterator chains (
.map(),.filter(),.collect()) over rawforloops for transformation logic. It's often faster and more readable. - •Type Safety: Use the type system!
- •Newtypes:
struct UserId(String)instead of passing raw strings. - •Enums: Make invalid states unrepresentable.
- •Newtypes:
3. Error Handling
- •Libraries: Define a custom
Errorenum usingthiserror.rust#[derive(thiserror::Error, Debug)] pub enum MyError { #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("Invalid input: {0}")] InvalidInput(String), } - •Binaries/Apps: Use
anyhow::Resultfor flexible error bubbling in applications.rustfn main() -> anyhow::Result<()> { ... }
4. Async Rust (Tokio)
- •Runtime: Use
#[tokio::main]for the entry point. - •Blocking: NEVER call blocking functions (file IO, heavy computation,
std::thread::sleep) inside an async function. It blocks the executor thread.- •Use
tokio::fsinstead ofstd::fs. - •Use
tokio::task::spawn_blockingfor CPU-heavy or blocking synchronous APIs.
- •Use
- •Concurrency:
- •
tokio::spawn: Spawns a background task (fire and forget or await handle). - •
tokio::join!: Wait for multiple futures concurrently. - •
tokio::select!: Race multiple futures (cancellation safety is key here).
- •
- •Channels: Use
tokio::sync::mpscfor message passing between tasks.
5. Testing
- •Unit Tests: Put them in the same file as the code, in a
mod testsblock.rust#[cfg(test)] mod tests { use super::*; #[test] fn it_works() { ... } } - •Integration Tests: Put them in
tests/*.rs. These test your public API. - •Property Testing: Consider
proptestfor complex logic to automatically find edge cases.
6. Formatting & Documentation
- •Formatting: Always run
cargo fmt. No arguments. - •Docs:
- •Triple slash
///for public item documentation. - •Module level docs
//!at the top of files. - •Run
cargo doc --opento preview.
- •Triple slash
7. Common Dependencies
- •Serialization:
serde,serde_json - •Async:
tokio,futures - •Error:
thiserror(lib),anyhow(bin) - •Logging:
tracing,tracing-subscriber(do not uselogcrate directly anymore,tracingis the standard for async apps). - •CLI:
clap