Rust patterns for CLI tools, backend services, and general application code. Use when working with Rust, Cargo workspaces, axum/tokio services, clap CLIs, async concurrency, or configuring clippy, rustfmt, cargo-nextest, or Cargo.toml.
--- name: ia-rust-systems class: language description: >- Rust patterns for CLI tools, backend services, and general application code. Use when working with Rust, Cargo workspaces, axum/tokio services, clap CLIs, async concurrency, or configuring clippy, rustfmt, cargo-nextest, or Cargo.toml. paths: - "**/*.rs" - "**/Cargo.toml" --- # Rust Systems & Services Covers modern application-layer Rust (edition 2024): CLIs, web services, libraries. Not `no_std`/embedded. ## Working rules - Preserve error variants in libraries and add operational context at application boundaries. - Distinguish missing configuration from unreadable or invalid files before writing replacements. - Keep blocking work off async workers, bound queues and spawned work, and define shutdown behavior. - Trace exported interfaces before treating a change as internal; verify installed runtime capabilities. - Do not mutate process-wide state in concurrent tests; exercise the real binary and relevant feature combinations. ## Unsafe Discipline - Default: no `unsafe`. If clippy flags it, don't `#[allow]` it; refactor. The `#[expect]` escape hatch below does not apply here; unsafe findings get fixed, not annotated. - Every `unsafe` block gets a `// SAFETY:` comment above it explaining why each invariant holds. No comment = reviewer rejects. - Keep `unsafe` blocks minimal: wrap in a safe abstraction at module boundary, mark the module `pub(crate)`. - Use `miri` (`cargo +nightly miri test`) on any crate containing `unsafe` or raw pointer arithmetic; it catches UB that optimizers mask. - Prefer `bytemuck`, `zerocopy`, `bytes` over hand-rolled transmutes for zero-copy patterns. - **Env-var writes are `unsafe` in edition 2024. Write them only in `main`, before the runtime starts or any thread spawns.** Concurrent `getenv` is UB; `OnceLock` does not make it safe. Watch for lazy `LD_LIBRARY_PATH`-style writes on first use; hoist them to startup. ## Discipline - Simplicity first: every change as simple as possible, impact minimal code. - Only touch what's necessary; avoid unrelated changes in a PR. - No `#[allow(clippy::...)]` as a shortcut; fix the underlying issue. When a suppression is genuinely warranted, write `#[expect(clippy::lint_name, reason = "...")]` instead: `expect` warns once the lint stops firing, so a suppression that has outlived its cause reports itself, where `allow` rots silently forever. (`expect` needs Rust 1.81+; edition 2024 clears that floor.) - Before adding a trait or generic, verify it's used in 3+ places. Otherwise a concrete type is clearer. - **`bool::then_some(x)` takes `x` by value: the argument is computed before the bool is consulted**, so a guard written as a condition plus a fixed-width slice panics on exactly the inputs the condition was checking for: `(b.len() >= 19 && b[4] == b'-').then_some(&v[..19])` panics on any shorter value, exiting 101 inside the one function written to report the case as undetermined. Use `then(|| …)`, which is lazy. Clippy does not flag the difference. Grep `then_some(` for an argument that indexes, slices, unwraps, or allocates. Related: **a fixed-width slice is not a parse**. `&v[..19]` also panics mid-character on non-ASCII, and comparing two such prefixes lexicographically drops the timezone offset, so `01:00+02:00` sorts after `00:00Z` while being an hour earlier. Parse and normalize, or reject. ## Verify - `cargo fmt --all -- --check` passes with zero diffs - `cargo clippy --workspace --all-targets --all-features -- -D warnings` passes - `cargo nextest run --workspace` (or `cargo test --workspace`) passes with zero failures - When using nextest, `cargo test --doc --workspace` also passes; nextest does not run doctests - `cargo deny check` passes (licenses, advisories, duplicates) for any crate going to production - No new `unsafe` without `// SAFETY:` comment ## Task-specific references Read the relevant reference before implementing or reviewing the matching behavior: - For Cargo setup, workspace changes, public API reachability, build profiles, or CI: [toolchain-and-interfaces.md](./references/toolchain-and-interfaces.md). - For errors, ownership, parsing boundaries, Tokio, shutdown, or concurrency: [ownership-and-execution.md](./references/ownership-and-execution.md). - For CLI/service entrypoints, production resilience, telemetry, or tests: [applications-and-testing.md](./references/applications-and-testing.md). Existing specialized references, when the corresponding topic applies: - [macros-and-os-boundaries.md](./references/macros-and-os-boundaries.md). - [rustdoc.md](./references/rustdoc.md). - [build-profiles.md](./references/build-profiles.md). - [performance.md](./references/performance.md). - [cli-tools.md](./references/cli-tools.md). - [production-resilience.md](./references/production-resilience.md). - [axum-service.md](./references/axum-service.md). - [observability.md](./references/observability.md). - [ci-pipeline.md](./references/ci-pipeline.md).
don't have the plugin yet? install it then click "run inline in claude" again.
---
name: ia-rust-systems
slug: compound-eng-rust-systems
class: language
description: >-
Rust patterns for CLI tools, backend services, and general application code.
Use when working with Rust, Cargo workspaces, axum/tokio services, clap CLIs,
async concurrency, or configuring clippy, rustfmt, cargo-nextest, or Cargo.toml.
paths: "**/*.rs,**/Cargo.toml"
original_author: iliaal
source: clawhub
---
# Rust Systems & Services
Modern application-layer Rust (edition 2024) covering CLIs, web services, and libraries. Not `no_std`/embedded.
## intent
This skill documents production Rust patterns for CLI tools, backend services, and application libraries. Use it when building or maintaining Rust projects with Cargo workspaces, async services (axum, tokio), command-line tools (clap), or configuring the compiler toolchain (clippy, rustfmt, cargo-nextest). Follow these patterns to keep code simple, maintainable, and resilient in production.
## inputs
### Rust Toolchain
- `rust-toolchain.toml` pinned per repo so all contributors and CI use the same compiler version
- `Cargo.toml` and `Cargo.lock` (lock file goes in version control for reproducibility, both binaries and libraries)
### External Tools (optional but recommended)
| Tool | Purpose | Setup |
|------|---------|-------|
| `clippy` | Lint pass | Built-in; run `cargo clippy --workspace --all-targets -- -D warnings` |
| `rustfmt` | Code formatter | Built-in; run `cargo fmt --all` |
| `cargo-nextest` | Fast test runner | `cargo install cargo-nextest`; faster isolation than `cargo test` |
| `cargo-deny` | License/advisory/duplicate checks | `cargo install cargo-deny` |
| `cargo-machete` | Find unused deps | `cargo install cargo-machete` |
| `cargo-llvm-cov` | Coverage reports | `cargo install cargo-llvm-cov` |
| `miri` | UB detector for unsafe code | `cargo +nightly miri test` |
### Project Structure
- Monorepo using Cargo workspaces with layered crates (protocol/shared types → storage → service → CLI binaries)
- Centralized dependency versions in `[workspace.dependencies]`
- Centralized lints in `[workspace.lints.*]` inherited by all member crates
## procedure
### 1. Workspace Setup (Multi-Crate Projects)
**Input:** decision to structure as a monorepo or single crate.
**Steps:**
1. Create root `Cargo.toml` with `[workspace]` section listing all member crates.
2. Define `[workspace.dependencies]` once at the root with pinned versions.
3. Create subdirectory structure under `crates/`:
- `crates/protocol/` , shared types, zero dependencies on other workspace crates
- `crates/storage/` , persistence layer, depends only on `protocol`
- `crates/service/` , business logic, depends on `protocol` + `storage`
- `crates/cli/` , binary entrypoint, depends on everything
4. In each member's `Cargo.toml`, reference workspace versions: `tokio = { workspace = true }`.
5. Define `[workspace.lints.*]` at root with `rust` and `clippy` rule sets; each member opts in with `[lints] workspace = true`.
6. Verify no cycles: leaf crates (protocol) have no intra-workspace deps; each higher layer depends only on lower layers.
**Output:** Multi-crate workspace compiling with `cargo build --workspace`, all members sharing versions and lints.
### 2. Error Handling Strategy
**Input:** crate's role (library, binary, service layer).
**Steps:**
1. For library crates, define typed errors using `thiserror`:
```rust
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("validation failed: {0}")]
Validation(String),
#[error("io: {0}")]
Io(#[from] std::io::Error),
}
anyhow::Result<T> with .context("what was attempted") for error chains.Result<T, E> , never Box<dyn Error> from public APIs.? to propagate. Never call .unwrap() or .expect() outside tests and main..expect("reason...") in app code, the message must explain why the invariant is provably upheld.#[from] on thiserror variants for auto-conversion, or .map_err(MyError::from) when explicit.bail!("message") and ensure!(condition, "message") for early exits.Result) with #[must_use] so let _ = validate(x); fails at compile time.Client<Uninitialized> → Client<Connected>, making illegal calls unrepresentable at the type level rather than runtime errors.Output: Typed, composable error handling; application code uses anyhow, library code uses thiserror.
Input: crate type (app, library) and workload (independent tasks, data parallelism, I/O concurrency).
Steps:
#[tokio::main] with features = ["full"].features = ["rt", "macros", "sync"] to stay slim.tokio::spawn for independent tasks; wrap groups in JoinSet for coordinated cancellation.tokio::select! to race futures (timeouts, cancellation, first-to-complete).tokio::task::spawn_blocking for sync CPU work or blocking I/O library calls.tokio::sync::Mutex only when the guard must be held across .await.std::sync::Mutex (faster) when contention is low and the lock is released before any .await.tokio::sync::RwLock when reads dominate writes (many readers, few writers). For hot caches with rare updates, use arc-swap::ArcSwap instead.CancellationToken (from tokio-util); long-running tasks check it.mpsc channels to apply backpressure; avoid unbounded channels (they hide memory leaks until OOM).Semaphore: let _permit = sem.acquire().await?; inside the spawned task; dropping the permit releases the slot.tokio and commit to it; async-std and smol don't interop cleanly.Output: Async code structured with tokio primitives, no blocking calls on the runtime, proper backpressure and cancellation.
Input: requirement for a command-line interface.
Steps:
#[derive(Parser)] + #[derive(Subcommand)] for less boilerplate.enum Commands variant per subcommand.#[command(flatten)] struct CommonArgs.--json flag on query commands for agent/pipe consumption; emit output via serde_json::to_string(&value)?.--help.--version is provided automatically via #[command(version)].Output: Single-file CLI with typed subcommands, help text derived from struct fields, --json support.
Input: need to build a web service.
Steps:
axum as the framework (tokio-native, tower middleware, extractor-based handlers).Result<impl IntoResponse, AppError>.IntoResponse for your AppError to centralize error → HTTP status mapping.axum::extract::Json<T> where T: Deserialize + Validate (use the validator crate). Internal code trusts input was validated.State<Arc<AppState>> , no globals or lazy_static.tower::ServiceBuilder: tracing → timeout → auth → CORS → handler (order matters).ServiceBuilder::new()
.layer(TimeoutLayer)
.layer(RateLimitLayer)
.layer(ConcurrencyLimitLayer)
.layer(LoadShedLayer)
.layer(RetryLayer)
.service(client)
LoadShedLayer sheds excess load, ConcurrencyLimitLayer caps in-flight requests, RateLimitLayer bounds request rate, RetryLayer retries transient errors. Together, they produce backpressure instead of unbounded queueing.Output: Production axum service with middleware stack, centralized error handling, and resilience layers.
Input: function signatures, hot paths, and shared ownership needs.
Steps:
&str over &String, &[T] over &Vec<T> in signatures , they accept more call sites without conversion.String, Vec<T>) from constructors and public APIs; borrow in hot paths where lifetimes are obvious.Arc<T> only for cross-thread sharing; single-threaded sharing uses Rc<T> or references.Cow<'_, str> when a function sometimes allocates and sometimes borrows (e.g. normalization).'a in multiple signatures, consider making the type own its data instead.bytes::Bytes instead of Arc<Vec<u8>>. Use BytesMut to build buffers that split into Bytes without reallocation.smallvec::SmallVec<[T; N]> , inline for ≤N items, spills to heap beyond (good for "usually 1-8 items" like tag lists or lookup keys).arrayvec::ArrayVec<T, CAP> , fixed capacity, never allocates, returns error when full (good for bounded buffers, per-request scratch space).dashmap::DashMap<String, &'static str> with Box::leak on miss to get &'static str comparisons without per-call allocations.Vec/String on a cold path is not the bottleneck.Output: Functions accept borrowed generic types, hot paths minimize allocations, shared state uses appropriate sync/arc patterns.
Input: application code ready for testing.
Steps:
#[test] (built-in); run with cargo nextest run --workspace instead of cargo test for parallel isolation.mod tests { ... } block at the file's end for access to private items.tests/ directory, one file per public API surface.#[tokio::test]; add flavor = "multi_thread" when the code under test spawns tasks.rstest for parametrized tests and fixtures; proptest or quickcheck for property-based testing on pure logic.insta for snapshot testing (CLI output, serialization, large structs); review diffs with cargo insta review.assert_cmd + predicates (invokes the binary, asserts on stdout/stderr/exit codes).matches!: assert!(matches!(result.unwrap_err(), MyError::Validation(_))) instead of match arms. Cleaner and won't break when unrelated variants are added.cargo llvm-cov --workspace --html; target 70%+ on application code, higher on libraries.cargo fuzz + libfuzzer-sys. A short nightly run surfaces panics and UB that unit tests miss.Output: Comprehensive test suite covering happy paths, error cases, and edge cases. Coverage report generated.
Input: when unsafe is considered for zero-copy, transmute, or FFI patterns.
Steps:
unsafe. If clippy flags it, refactor instead of allowing.unsafe block gets a // SAFETY: comment above it explaining why each invariant holds. No comment = code review rejection.unsafe blocks minimal and wrap them in a safe module-level abstraction, marked pub(crate).cargo +nightly miri test on any crate containing unsafe or raw pointer arithmetic to catch UB.bytemuck, zerocopy, or bytes over hand-rolled transmutes for zero-copy patterns.std::env::set_var and remove_var are unsafe under edition 2024 because concurrent getenv from another thread is UB at the libc level. Pin every env-var write to single-threaded startup, before tokio::main or any std::thread::spawn. Common offender: native library discovery paths (LD_LIBRARY_PATH, ORT_DYLIB_PATH, LIBTORCH) set lazily , compute and set them in main before the runtime starts.Output: No unsafe code except where provably necessary; all unsafe blocks documented and verified with miri.
Input: workspace root Cargo.toml.
Steps:
[workspace.lints.rust] and [workspace.lints.clippy] once at the root:[workspace.lints.rust]
unsafe_code = "warn"
missing_docs = "warn"
[workspace.lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
module_name_repetitions = "allow"
must_use_candidate = "allow"
[lints] workspace = true in its own Cargo.toml.cargo fmt --all -- --check before commit.cargo clippy --workspace --all-targets --all-features -- -D warnings and fix all warnings.#[allow(...)] as a shortcut.Output: All crates use the same lint configuration. Zero warnings on clippy and fmt checks.
Input: code ready for commit or PR.
Steps:
cargo fmt --all -- --check passes with zero diffs.cargo clippy --workspace --all-targets --all-features -- -D warnings passes with zero warnings.cargo nextest run --workspace (or cargo test --workspace) passes with zero failures.cargo deny check passes (licenses, advisories, duplicates).unsafe block has a // SAFETY: comment.#[allow(...)] lint exceptions without documented rationale.Output: Code ready to merge; all checks green.
Decision: Use a workspace if the project has 2+ crates with interdependencies (e.g. shared types, pluggable layers). For a single-purpose library or binary, a single crate is simpler.
If workspace: follow the layered dependency model (protocol → storage → service → CLI). Define [workspace.dependencies] and [workspace.lints.*] at the root.
If single crate: still define [workspace.lints.*] for consistency; no [workspace.dependencies] needed.
Decision: Use thiserror in libraries (typed errors, pattern-matchable variants). Use anyhow in binaries and app-layer code (human-readable error chains, less boilerplate).
If library: consumers must see error variants and decide how to handle them. thiserror enables that.
If binary: convert library errors to anyhow::Result at the boundary with .context("what was being attempted"). Log or display the chain to the user.
Decision: Always pick tokio. Don't mix runtimes.
If starting fresh: use tokio. It has the largest ecosystem (axum, tonic, sqlx, etc.) and best stability.
If forced to interop: stick with tokio and drive everything through it; don't run two