rust-dev
by @tenequm
Practical day-1 guide to building applications in Rust well. Covers the mental model (ownership, errors as values, traits-not-interfaces), day-1 decisions (S...
clawhub install rust-devπ About This Skill
name: rust-dev description: Practical day-1 guide to building applications in Rust well. Covers the mental model (ownership, errors as values, traits-not-interfaces), day-1 decisions (String vs &str, Box vs Rc vs Arc, dyn vs impl Trait, anyhow vs thiserror), idioms to internalize early, anti-patterns to avoid, and a tight crate shortlist (tokio, serde, anyhow, clap, reqwest, tracing, axum, sqlx). Use when starting a new Rust project, learning Rust coming from Python/JS/Go/Java/C++, deciding on types and lifetimes, choosing crates, structuring modules, configuring Cargo.toml/clippy/rustfmt, or whenever the user mentions Rust, cargo, ownership, borrow checker, lifetimes, traits, async Rust, or "writing this in Rust". metadata: version: "0.1.1" upstream: "rust@1.95.0"
Rust Development - Day 1
A practical foundation for writing Rust apps well from the first commit. Not a textbook. Focuses on the differences from other languages, the day-1 decisions that shape everything else, and the small set of crates that cover most real apps.
When to Use
anyhow vs thiserror)Cargo.toml, clippy, and rustfmtDay-1 Setup
# 1. Install the toolchain (rustup is the toolchain manager)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh2. Confirm components (rustfmt and clippy ship with stable, rust-src enables IDE features)
rustup component add rustfmt clippy rust-src3. Create a project
cargo new my-app # binary (src/main.rs)
cargo new --lib my-lib # library (src/lib.rs)4. The dev loop (memorize these four)
cargo check # fast type-check, no codegen
cargo run # build and run (binary)
cargo test # build and run tests (incl. doctests)
cargo clippy # lint (run before pushing)
cargo fmt # format5. Manage dependencies without editing Cargo.toml by hand
cargo add tokio --features full
cargo remove tokio
rust-analyzer is mandatory. It is the language server every editor uses (VS Code, Zed, Neovim, Helix, RustRover uses its own engine but is comparable). In VS Code, install the rust-analyzer extension and set rust-analyzer.check.command to "clippy" so you get lint feedback on save.
Want a file watcher later? cargo install bacon, then run bacon in your project. Not needed on day 1.
The Rust Mental Model in 5 Ideas
Rust trades two things you take for granted in most languages (a garbage collector and exceptions) for compile-time guarantees about memory, data races, and error handling. The shape of the language follows from that trade.
1. Ownership: every value has exactly one owner
Think of values like physical objects. A book, a file, a network connection. At any moment, one variable owns it. You can:
let b = a; hands ownership to b. a is gone.&a lets others look at it. Many readers allowed.&mut a lets one person modify it. Exclusive access.a.clone() makes a deep copy. Both keep their own.When the owner goes out of scope, the value is dropped (memory freed, file closed, lock released). No GC, no manual free. This is RAII, enforced by the compiler.
2. Aliasing XOR mutability
At any moment, a piece of data has either:
&mut T), or&T),never both. This single rule is what eliminates data races and most use-after-free bugs. The borrow checker enforces it. When it complains, it is telling you your data ownership story is unclear, not that the language is being difficult.
3. Errors are values, not exceptions
There is no try/catch. Functions that can fail return Result. Functions that can return nothing useful return Option. The compiler forces you to handle both. The ? operator propagates errors up the call stack with one character:
fn read_config() -> Result {
let bytes = std::fs::read("config.toml")?; // ? = early-return on Err
let config = toml::from_slice(&bytes)?;
Ok(config)
}
There is no null. Option is None or Some(value). The compiler will not let you forget the None case.
4. Traits are not Java interfaces
A trait defines behavior. Types impl traits. So far so familiar. The differences:
fn f(x: T) , the compiler generates a separate copy of f for each concrete T you call it with (monomorphization, like C++ templates). Zero runtime overhead.dyn Trait (typically Box or &dyn Trait). One vtable lookup per call.Deref to "extend" a type, stop and use composition or an enum.impl YourTrait for SomeoneElsesType or impl SomeoneElsesTrait for YourType, but not both foreign. This keeps dependency resolution sane.5. The borrow checker is a design oracle
The most common newcomer mistake is treating compiler errors as obstacles to silence. They are not. Almost every borrow-check error reveals a real issue with who owns what. When you get stuck, the question is rarely "how do I make this compile" and almost always "what is the actual ownership relationship I want here?" Read the error. The compiler is unusually informative.
The 3 Questions for Every Function Signature
Before writing a function, ask: does it need to own, read, or modify the input?
fn consume(s: String) // owns: function takes responsibility, caller loses it
fn read(s: &str) // reads: function looks at it, caller keeps it
fn modify(s: &mut String) // mutates: function changes it in place
Defaults that work 90% of the time:
&str over String, &[T] over Vec (these are slices, accept both owned and borrowed callers).String, Vec). Returning references means lifetimes; avoid until you need them.String, Vec). Storing &str in a struct is the single most common newcomer trap and it cascades lifetime annotations through every type that holds your struct.Day-1 Decision Table
One-line answers to the choices that come up first.
| Decision | Default | When to pick the other |
|---|---|---|
| String vs &str (struct field) | String | Almost never &str until you have a real reason and understand lifetimes |
| String vs &str (function param) | &str | Use String only if you must own/store it inside |
| Vec vs &[T] (param) | &[T] | Vec only if you must own |
| Box vs Rc vs Arc | Box (single owner, heap) | Arc for shared ownership across threads. Avoid Rc as default; use Arc so you do not refactor when you go async |
| RefCell vs Mutex | Mutex (or RwLock) | Same reason: works in async/threads, while RefCell does not |
| Option vs Result | Option for "no value", Result for "failed for a reason" | If the absence carries meaning the caller should handle, Result |
| dyn Trait vs impl Trait / | Generic ( or impl Trait) - static dispatch | Box when you need a heterogeneous collection (Vec) |
| Errors in app code | anyhow::Result everywhere | - |
| Errors in library code | thiserror-derived enum | Never Box in public library APIs - forces callers to downcast |
| &self vs &mut self vs self | &self for getters, &mut self for setters, self for builders/consuming ops | - |
| Module layout | Inline modules until a file gets long, then split | One module = one file is a Java/C# instinct, not a Rust one |
Idioms to Internalize Early
These appear in nearly every Rust program. Learn them in week 1.
? for error propagation. Replaces nine lines of match with one character.
let body = reqwest::get(url).await?.text().await?;
Iterator chains over manual loops. Compile to the same machine code as hand-written loops (LLVM inlines closures). Idiomatic Rust is functional in style.
let active_emails: Vec = users
.iter()
.filter(|u| u.active)
.map(|u| u.email.clone())
.collect();
match exhaustiveness. Add a new variant to an enum and every match that does not handle it becomes a compile error. Use this. It is one of the most powerful refactoring tools in any language.
if let and let else for the common single-arm match.
if let Some(name) = user.name { println!("hi {name}"); }let Some(name) = user.name else { return Err(anyhow!("no name")); };
// name is in scope from here on, no nesting
From / Into for type conversions. Implement From, get Into for free. ? uses From to convert error types automatically.
Combinators on Option / Result. Reach for .map, .and_then, .unwrap_or, .unwrap_or_else, .ok_or before reaching for match.
Derive macros. #[derive(Debug, Clone, PartialEq)] gets you 80% of the boilerplate for free. Add #[derive(Serialize, Deserialize)] for JSON.
Coming From X, Here Is What Bites You
From Python or JavaScript:
let b = a; for a heap value (like String, Vec) moves it. a is no longer usable. Use &a to borrow or a.clone() to copy.Option is forced on you.Result and ?. The compiler will not let you ignore errors.mut to mutate. Same for references: & vs &mut.usize.From Go:
? instead of if err != nil.nil. Option.tokio. The async model is cooperative (await is an explicit yield point), not preemptive.interface{} becomes traits. Default to generics for static dispatch; Box only when you need it.panic! should be reserved for unrecoverable bugs in app code; do not use it as Go-style "log and continue".From Java or C#:
is monomorphized. dyn Trait is the opt-in dynamic version.Option.Result and ?.Arc is the closest thing to a Java reference.From C++:
Clone is explicit and Copy is a marker trait for cheap bitwise copies.& is a compile-time-checked borrow, not a raw pointer. Raw pointers exist (*const T, *mut T) but require unsafe to dereference.Box (unique_ptr), Rc (shared_ptr, single thread), Arc (shared_ptr, thread-safe).serde, tokio::main, etc. work.The Crate Shortlist
These cover most real apps. Add them as needed; they are not all required.
| Crate | What it gives you |
|---|---|
| serde + serde_json | Serialization. #[derive(Serialize, Deserialize)] and you are done |
| tokio | Async runtime. #[tokio::main], tokio::spawn, async I/O |
| anyhow | App error type. anyhow::Result, bail!, context() |
| thiserror | Library error enums. #[derive(thiserror::Error)] |
| clap | CLI argument parsing. #[derive(Parser)] and you have a CLI |
| reqwest | HTTP client. Async by default, blocking feature available |
| tracing + tracing-subscriber | Structured logging. The default for any async code (replaces log) |
| axum | Web framework. Built on tokio + hyper + tower. The 2026 default |
| sqlx | Database access. Async, compile-time checked queries. PostgreSQL, MySQL, SQLite |
| chrono | Dates and times. (jiff is promising but not yet ecosystem-ready as of April 2026) |
See references/crate-shortlist.md for one minimal example each.
Top Anti-Patterns to Avoid
These are the mistakes that show up in every newcomer's code review. Avoid them.
1. Storing &str (or any reference) in a struct. Causes lifetime annotations to cascade through every caller. Use String until you have a profiler-backed reason not to.
2. Rc everywhere to simulate Python/JS object semantics. It works but is a code smell, and breaks the moment you need threading. Default to Arc instead so you do not refactor.
3. Box in library public APIs. Forces callers to downcast. Define a typed error enum with thiserror. Box is acceptable inside a binary, never in a published library.
4. .unwrap() and .expect() outside prototypes and tests. Use ? and propagate. Reserve unwrap for truly impossible cases and add a comment explaining why it cannot fail.
5. Brute-force .clone() until it compiles. Sometimes cloning is right, but if you are scattering .clone() to silence the borrow checker, the design is wrong. Step back and ask the 3 questions about who owns what.
6. Trying to inherit via Deref. Deref is for smart-pointer-like wrappers, not for OOP-style "extends". Use composition.
7. Reaching for unsafe. App developers should essentially never need it. unsafe does not turn off the borrow checker; it lets you do five specific things (deref raw pointers, call unsafe functions, access mutable statics, implement unsafe traits, access union fields) with the contract that you have manually verified the invariants.
What to Defer
You do not need these on day 1. Some you may never need.
Pin, Future internals, manual poll impls. Just write async fn and .await.unsafe and FFI. Almost never for app code.for<'a>), variance, PhantomData. Expert territory.Cell, OnceCell, LazyLock, MaybeUninit. Reach for these when you have a specific reason.Minimal Cargo.toml
Single-crate, edition 2024, opinionated lints. Drop into a fresh project.
[package]
name = "my-app"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"[dependencies]
[dev-dependencies]
[profile.release]
lto = "thin"
codegen-units = 1
=============================================================================
Lints. Loose-but-helpful: deny obvious bugs, warn on common smells, leave
room to learn. Upgrade to clippy::pedantic later if you want the full ride.
=============================================================================
[lints.rust]
unsafe_code = "forbid" # downgrade to "deny" if you do FFI
unreachable_pub = "warn"[lints.clippy]
all = { level = "deny", priority = -1 }
Idiomatic helpers
uninlined_format_args = "warn"
semicolon_if_nothing_returned = "warn"
implicit_clone = "warn"
Smells in non-prototype code
unwrap_used = "warn"
expect_used = "warn"
dbg_macro = "warn"
todo = "warn"
print_stdout = "warn" # use tracing::info! instead in real apps
rustfmt.toml
style_edition = "2024"
edition = "2024"
That is enough. rustfmt's defaults are good. Some teams add use_small_heuristics = "Max" to keep more code on single lines. Fancy options like imports_granularity and group_imports are still nightly-only as of April 2026.
rust-toolchain.toml (optional but recommended)
Pins the toolchain per-project so everyone on the team uses the same Rust.
[toolchain]
channel = "stable"
components = ["rustfmt", "clippy", "rust-src"]
profile = "minimal"
.gitignore
/target
**/*.rs.bk
Cargo.lock # for libraries only; commit Cargo.lock for binaries
Project Structure
my-app/
src/
main.rs # binary entry point: fn main()
lib.rs # OR a library crate root
config.rs # module: declared as mod config; in main.rs/lib.rs
api/ # nested module
mod.rs # OR api.rs next to api/ folder (2018+ style preferred)
users.rs
tests/ # integration tests (each file is its own crate)
smoke.rs
Cargo.toml
Cargo.lock
rust-toolchain.toml
rustfmt.toml
.gitignore
Inline modules with mod { ... } until a file gets long, then split. Do not pre-split.
Learning Path
1. The Rust Book (https://doc.rust-lang.org/book/) - canonical, free, current. The interactive Brown University version (https://rust-book.cs.brown.edu/) adds quizzes and visualizations.
2. Rustlings (https://github.com/rust-lang/rustlings) - exercises in parallel with The Book.
3. 100 Exercises to Learn Rust (https://rust-exercises.com/) - alternative or supplement to Rustlings, slightly newer.
4. Rust for Rustaceans (Jon Gjengset) - the post-beginner book. Read after you are comfortable.
5. Zero to Production in Rust (Luca Palmieri) - if you are building a backend service. Note: the book uses actix-web while axum is the 2026 default; the patterns translate cleanly.
For looking up syntax: Rust by Example (https://doc.rust-lang.org/rust-by-example/).
For curated crate recommendations: blessed.rs (https://blessed.rs/crates).
Reference Docs
Detailed material lives in references/. Read each when you hit the topic.
String/&str/Cow, slices, smart pointers, the self-referential struct trapResult, ?, anyhow vs thiserror patterns, custom error enums, when panic! is appropriatedyn vs impl Trait vs generics, common derives, From/Into/Display/Debug, blanket impls, the orphan ruletokio, #[tokio::main], .await, Send/Sync, common pitfalls (blocking in async, MutexGuard across .await)