These principles guide how technical tutorials and code should be written for this Rust project. Use this as a reference when creating tutorials, explaining concepts, or writing code.
Feed this document to Claude at the start of coding sessions to maintain consistent quality and approach.
Goal: Help developers understand and implement features efficiently in Rust, writing maintainable, idiomatic code without over-engineering.
Values:
- Clarity over cleverness
- Simplicity over flexibility
- Working code over theory
- Understanding over memorization
- Rust idioms over porting patterns from other languages
Start with the big picture, then drill down into details.
Structure:
- Show the end goal - What are we building?
- Explain the architecture - How do components fit together?
- Provide implementation - Actual working code
- Cover edge cases - Optional advanced topics
Example:
## Building a Task Tracker CLI
**Goal:** Command-line tool where users can add, list, and complete tasks with an interactive TUI.
**Architecture:**
┌──────────────┐
│ CLI Parser │ (clap - command parsing)
└──────┬───────┘
↓
┌──────────────┐
│ TUI Layer │ (ratatui - interactive display)
└──────┬───────┘
↓
┌──────────────┐
│ Task Manager │ (business logic)
└──────┬───────┘
↓
┌──────────────┐
│ Storage │ (JSON file persistence)
└──────────────┘
**Implementation:**
[Complete working code...]
**Advanced:**
- Cloud sync via API
- Task dependencies
- Recurring tasksWhy this works:
- Gives context before diving into code
- Shows how pieces connect
- Demonstrates layered architecture common in Rust CLI apps
- Allows skipping advanced topics if not needed
Follow Rust conventions and embrace the language's philosophy. This is critical for writing maintainable Rust code.
Rust Idioms:
- ✅ Use
Result<T, E>for errors, not exceptions or null - ✅ Leverage ownership and borrowing instead of cloning everywhere
- ✅ Use pattern matching with
matchandif let - ✅ Prefer iterators over manual loops
- ✅ Use traits for abstraction, not inheritance
- ✅ Follow naming conventions:
snake_casefor functions/variables,CamelCasefor types - ✅ Use
?operator for error propagation - ✅ Prefer
&strfor string slices,Stringfor owned strings - ✅ Use
Option<T>instead of null/None checks - ✅ Return early with guard clauses instead of deep nesting
For beginners:
When you're new to Rust, these patterns may feel unfamiliar. That's normal! Don't fight the language:
- Compiler errors are your friend - read them carefully
- If you're cloning everywhere, there's probably a better way
- If you're using
unwrap()frequently, use proper error handling instead - When the borrow checker complains, it's preventing real bugs
Example:
// Good: Idiomatic Rust with Result, borrowing, and iterators
pub fn process_items(items: &[Item]) -> Result<Vec<String>, ProcessError> {
items
.iter()
.filter(|item| item.is_active)
.map(|item| item.format())
.collect()
}
// Bad: Fighting Rust's idioms
pub fn process_items(items: Vec<Item>) -> Vec<String> {
let mut result = Vec::new();
for i in 0..items.len() {
let item = items[i].clone(); // Unnecessary clone
if item.is_active {
result.push(item.format().unwrap()); // Panics on error
}
}
result
}Common Rust patterns for beginners:
// Pattern 1: Use enums to represent states
#[derive(Debug)]
pub enum ConnectionState {
Disconnected,
Connecting { retry_count: u32 },
Connected { session_id: String },
Failed { reason: String },
}
impl ConnectionState {
pub fn is_active(&self) -> bool {
matches!(self, Self::Connected { .. })
}
}
// Pattern 2: Result with ? operator for error propagation
fn load_tasks() -> Result<Vec<Task>, Box<dyn std::error::Error>> {
let contents = std::fs::read_to_string("tasks.json")?;
let tasks = serde_json::from_str(&contents)?;
Ok(tasks)
}
// Pattern 3: Builder pattern with consuming methods
pub struct TaskBuilder {
title: String,
description: Option<String>,
priority: Priority,
}
impl TaskBuilder {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
description: None,
priority: Priority::Normal,
}
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn priority(mut self, priority: Priority) -> Self {
self.priority = priority;
self
}
pub fn build(self) -> Task {
Task {
title: self.title,
description: self.description,
priority: self.priority,
}
}
}
// Usage:
let task = TaskBuilder::new("Write docs")
.description("Update the README")
.priority(Priority::High)
.build();Why this matters:
- Rust's design prevents entire classes of bugs (null pointers, data races, use-after-free)
- Idiomatic code is easier for other Rust developers to understand
- Fighting the language leads to verbose, buggy code
- The compiler is teaching you to write better code
Break code into logical, reusable pieces, but avoid premature abstraction.
Good componentization:
- ✅ Separate concerns (UI, logic, API, storage)
- ✅ Each module has a single responsibility
- ✅ Clear interfaces between modules (use traits)
- ✅ Easy to test independently
- ✅ Reusable when actually needed
Bad componentization:
- ❌ Over-abstraction (generic handlers for one-time use)
- ❌ God structs that do everything
- ❌ Excessive trait hierarchies
- ❌ Excessive indirection
- ❌ "Future-proofing" that never gets used
Rule of thumb:
- Once: Inline it
- Twice: Consider extracting
- Three times: Definitely extract
Example:
// Good: Clear separation via modules and focused types
// src/storage/mod.rs
pub struct TaskStore {
path: PathBuf,
}
impl TaskStore {
pub fn load(&self) -> Result<Vec<Task>, StorageError> {
let contents = std::fs::read_to_string(&self.path)?;
Ok(serde_json::from_str(&contents)?)
}
pub fn save(&self, tasks: &[Task]) -> Result<(), StorageError> {
let json = serde_json::to_string_pretty(tasks)?;
std::fs::write(&self.path, json)?;
Ok(())
}
}
// src/manager/mod.rs
pub struct TaskManager {
store: TaskStore,
}
impl TaskManager {
pub fn add_task(&mut self, task: Task) -> Result<(), ManagerError> {
let mut tasks = self.store.load()?;
tasks.push(task);
self.store.save(&tasks)?;
Ok(())
}
pub fn list_active(&self) -> Result<Vec<&Task>, ManagerError> {
let tasks = self.store.load()?;
Ok(tasks.iter().filter(|t| !t.completed).collect())
}
}
// Bad: God struct doing everything
pub struct TaskManager {
// Also handles CLI parsing, rendering, validation, logging, metrics, etc.
// Too many responsibilities in one place
}Rust-specific tip:
Use modules (mod) to organize code by domain, making each module's public API clear:
// src/lib.rs or src/main.rs
mod storage;
mod manager;
mod cli;
mod ui;
pub use manager::TaskManager;
pub use storage::TaskStore;Why this matters:
- Easier to understand small, focused modules
- Can change one part without breaking others
- Testing is simpler
- But over-abstraction makes code hard to follow
Keep it simple. Don't add features "just in case."
Avoid:
- ❌ Configuration for things that don't need configuring
- ❌ Abstractions for "future flexibility" that may never come
- ❌ Complex trait hierarchies for simple operations
- ❌ Generic utilities that could be one-liners
- ❌ Over-engineered error handling for impossible scenarios
- ❌ Elaborate state machines for simple flows
Embrace:
- ✅ Direct, straightforward code
- ✅ Clear, explicit logic
- ✅ Simple data structures (structs, enums)
- ✅ Standard patterns
- ✅ Error handling only where errors can actually occur
Example:
// Good: Simple and clear
pub fn format_date(date: &DateTime<Utc>) -> String {
date.format("%Y-%m-%d").to_string()
}
// Bad: Over-engineered
pub struct DateFormatter {
config: FormatterConfig,
plugins: Vec<Box<dyn Plugin>>,
}
impl DateFormatter {
pub fn format(&self, date: &DateTime<Utc>, options: Option<FormatOptions>) -> String {
// 50 lines of complexity for basic date formatting
}
}Rust-specific tip:
Rust's type system lets you express constraints at compile time. Use it, but don't go overboard:
// Good: Simple generic constraint
pub fn print_item<T: Display>(item: &T) {
println!("{}", item);
}
// Overkill for most cases
pub fn print_item<T>(item: &T)
where
T: Display + Debug + Clone + Send + Sync + 'static,
{
println!("{}", item);
}Why this matters:
- Simpler code is easier to understand and maintain
- Less code means fewer bugs
- Complexity should match the actual problem
- Rust's zero-cost abstractions mean you don't pay for what you don't use
Provide real, runnable code rather than pseudo-code or fragments.
Do this:
// src/task.rs
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::fs;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
pub id: u64,
pub title: String,
pub completed: bool,
}
#[derive(Debug)]
pub enum TaskError {
IoError(std::io::Error),
ParseError(serde_json::Error),
NotFound(u64),
}
impl From<std::io::Error> for TaskError {
fn from(err: std::io::Error) -> Self {
TaskError::IoError(err)
}
}
impl From<serde_json::Error> for TaskError {
fn from(err: serde_json::Error) -> Self {
TaskError::ParseError(err)
}
}
pub struct TaskRepo {
path: PathBuf,
}
impl TaskRepo {
pub fn new(path: PathBuf) -> Self {
Self { path }
}
pub fn load_all(&self) -> Result<Vec<Task>, TaskError> {
if !self.path.exists() {
return Ok(Vec::new());
}
let contents = fs::read_to_string(&self.path)?;
let tasks = serde_json::from_str(&contents)?;
Ok(tasks)
}
pub fn save_all(&self, tasks: &[Task]) -> Result<(), TaskError> {
let json = serde_json::to_string_pretty(tasks)?;
fs::write(&self.path, json)?;
Ok(())
}
pub fn find_by_id(&self, id: u64) -> Result<Task, TaskError> {
let tasks = self.load_all()?;
tasks.into_iter()
.find(|t| t.id == id)
.ok_or(TaskError::NotFound(id))
}
}Not this:
// Pseudo-code
struct TaskRepo {
// ... fields
}
impl TaskRepo {
fn load_all() {
// Read from file
// Parse JSON
// Return tasks
}
}For Rust beginners:
Complete examples help you understand:
- What traits need to be derived (
Serialize,Deserialize,Debug,Clone) - How to define custom error enums
- How to implement
Fromfor error conversion - Error propagation with
?operator - When to return
Result<T, E> - Pattern matching with
find()andok_or()
Why this matters:
- Developers can copy-paste and run immediately
- Shows all the details (imports, derives, error handling)
- No ambiguity about implementation
- Demonstrates real Rust patterns (error enums, From trait, Result)
Leverage well-tested, community-maintained crates for common tasks.
Use existing solutions for:
- ✅ HTTP requests (
reqwest,hyper) - ✅ CLI frameworks (
clap,structopt) - ✅ Terminal UI (
ratatui,crossterm) - ✅ Testing (
cargo test, built-in) - ✅ Date/time (
chrono) - ✅ Serialization (
serde,serde_json,toml) - ✅ Error handling (
anyhow,thiserror) - ✅ Async runtime (
tokio,async-std)
Consider building custom when:
- Crate adds too much complexity for your needs
- No good crate exists for your specific use case
- Crate is unmaintained or has security issues
- Performance is critical and crate is too slow
Example:
// Good: Use clap for CLI argument parsing
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "tasks")]
#[command(about = "A simple task manager")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Add a new task
Add { title: String },
/// List all tasks
List,
/// Complete a task
Complete { id: u64 },
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Add { title } => add_task(&title),
Commands::List => list_tasks(),
Commands::Complete { id } => complete_task(id),
}
}
// Bad: Manual argument parsing
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("Usage: tasks <command>");
return;
}
// 100+ lines of manual parsing, validation, help text...
}Rust-specific tip:
Add crates to Cargo.toml:
[dependencies]
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
thiserror = "1.0"
# Optional: for async operations
tokio = { version = "1.0", features = ["full"] }
# Optional: for HTTP
reqwest = { version = "0.12", features = ["json"] }Why this matters:
- Popular crates are battle-tested
- Community provides support and updates
- Security vulnerabilities get fixed
- Focus on your unique business logic, not infrastructure
- Rust's package manager (Cargo) makes dependencies easy
Help learners understand the reasoning behind decisions.
When introducing a new concept:
- Explain the problem it solves
- Show alternatives and trade-offs
- Justify the chosen approach
Good:
We use an enum to represent task status instead of strings or booleans because:
```rust
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TaskStatus {
Todo,
InProgress { started_at: SystemTime },
Done { completed_at: SystemTime },
}- Type safety - Compiler prevents invalid states (can't have "Dne" typo)
- Pattern matching - Exhaustive matching catches missing cases at compile time
- Associated data - Each variant can carry relevant data
- Self-documenting - Clear what states exist without reading docs
Alternative: Could use String status, but then:
- Risk of typos and invalid values
- Need runtime validation
- Can't attach variant-specific data cleanly
In Rust, enums are powerful because:
- Zero-cost abstraction - No runtime overhead vs. manual tagging
- Match exhaustiveness - Compiler ensures you handle all cases
- Memory efficiency - Uses only space needed for largest variant + discriminant
**Not enough:**
```markdown
Here's the TaskStatus enum.
For Rust beginners, also explain:
- Why enums are better than booleans/strings for state
- How pattern matching provides compile-time guarantees
- When to use
Optionvs custom enums - How to add data to enum variants
When to skip explanations:
- Reader has stated knowledge level
- Concept is industry-standard (e.g., "REST API")
- Documentation is reference material, not tutorial
- Rust basics already covered in earlier tutorials
Why this matters:
- Understanding principles > memorizing patterns
- Developers can apply knowledge to new situations
- Easier to evaluate if approach fits their needs
- Rust's unique features (ownership, traits) need explanation for beginners
Start simple, add complexity only when needed.
Tutorial flow:
## Step 1: Basic Login (minimal, working)
[Simple code that works - even if it uses `.unwrap()` temporarily]
## Step 2: Add Proper Error Handling (practical necessity)
[Code with `Result` and `?` operator]
## Step 3: Add Loading States (better UX)
[Code with async progress indicators]
## Step 4: Make It Safe and Robust
[Code with proper ownership, no panics]
## Optional: Advanced Topics
- Token refresh with async
- Session management
- OAuth integrationFor Rust beginners:
It's okay to start with .unwrap() in early examples to focus on the main concept, then show proper error handling later. Just mark it clearly:
// Step 1: Focus on the main logic (we'll improve error handling next)
let config = load_config().unwrap(); // TODO: proper error handling in step 2Why this works:
- Can stop at any level if it meets needs
- Not overwhelmed by advanced features upfront
- Clear progression of complexity
- Beginners can learn ownership gradually
Show how pieces fit into the larger system.
Always include:
- ✅ File paths (
// src/api/client.rs) - ✅ All necessary imports and
usestatements - ✅ Cargo.toml dependencies needed
- ✅ Where the code runs (binary, library, module)
- ✅ What other parts interact with this code
- ✅ Module structure context
Example:
// src/commands/add.rs
// This command is called from src/main.rs when user runs: tasks add "Buy milk"
// It uses TaskManager from src/manager/mod.rs
use crate::manager::TaskManager;
use crate::task::{Task, TaskStatus};
use anyhow::Result;
pub fn execute(manager: &mut TaskManager, title: String) -> Result<()> {
let task = Task {
id: manager.next_id(),
title,
status: TaskStatus::Todo,
};
manager.add(task)?;
println!("✓ Task added successfully");
Ok(())
}Cargo.toml context:
[package]
name = "tasks"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"Module structure context:
src/
├── main.rs # CLI entry point, uses clap
├── lib.rs # Public API for library users
├── task.rs # Task struct and TaskStatus enum
├── manager/
│ ├── mod.rs # TaskManager struct
│ └── filters.rs # Query filtering logic
├── storage/
│ ├── mod.rs # TaskStore for persistence
│ └── json.rs # JSON serialization
└── commands/
├── mod.rs
├── add.rs ← We are here
├── list.rs
└── complete.rs
Why this matters:
- Understand where code lives in the project
- Know how to import and use the code
- See the bigger picture
- Rust's module system can be confusing for beginners
Follow project conventions and established Rust patterns.
In Rust projects:
- Commands:
src/commands/<group>/<command>.rs - Types:
src/types.rsor co-located in modules - Modules:
src/<module>/mod.rsorsrc/<module>.rs - Tests:
#[cfg(test)]mod tests in same file, ortests/directory - Binaries:
src/main.rsorsrc/bin/<name>.rs - Libraries:
src/lib.rs
Rust conventions:
snake_casefor functions, variables, modulesCamelCasefor types, structs, enums, traitsSCREAMING_SNAKE_CASEfor constants- Use
Result<T, E>for fallible operations - Use
Option<T>for optional values - Implement
Displayfor user-facing output - Implement
Debugfor debugging - Use
#[derive(...)]for common traits
Example:
// Following Rust conventions
const MAX_RETRIES: u32 = 3; // SCREAMING_SNAKE_CASE
pub struct ApiClient { // CamelCase for types
base_url: String, // snake_case for fields
}
impl ApiClient {
pub fn new(base_url: String) -> Self { // snake_case for methods
Self { base_url }
}
pub async fn fetch_data(&self) -> Result<Data, ApiError> { // Result for errors
// Implementation
}
}Why this matters:
- Predictable structure
- Easier onboarding
- Consistent codebase
- Follows Rust community standards
- Cargo and tooling expect these patterns
Write code that's easy to test, and show how to test it.
Testable code characteristics:
- Dependencies injected (not hardcoded)
- Pure functions where possible
- Side effects isolated
- Clear input/output contracts
- Use traits for mockability
Example:
// src/manager.rs
use crate::storage::Storage;
use crate::task::{Task, TaskStatus};
pub struct TaskManager<S> {
storage: S,
next_id: u64,
}
impl<S: Storage> TaskManager<S> {
pub fn new(storage: S) -> Self {
Self {
storage,
next_id: 1,
}
}
pub fn add(&mut self, mut task: Task) -> Result<(), String> {
task.id = self.next_id;
self.next_id += 1;
self.storage.save(&task)?;
Ok(())
}
pub fn complete(&mut self, id: u64) -> Result<(), String> {
let mut task = self.storage.load(id)?;
task.status = TaskStatus::Done {
completed_at: std::time::SystemTime::now(),
};
self.storage.save(&task)?;
Ok(())
}
pub fn count_active(&self) -> usize {
self.storage.all()
.into_iter()
.filter(|t| !matches!(t.status, TaskStatus::Done { .. }))
.count()
}
}
// Test with in-memory storage
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
struct InMemoryStorage {
tasks: HashMap<u64, Task>,
}
impl InMemoryStorage {
fn new() -> Self {
Self { tasks: HashMap::new() }
}
}
impl Storage for InMemoryStorage {
fn save(&mut self, task: &Task) -> Result<(), String> {
self.tasks.insert(task.id, task.clone());
Ok(())
}
fn load(&self, id: u64) -> Result<Task, String> {
self.tasks.get(&id).cloned()
.ok_or_else(|| format!("Task {} not found", id))
}
fn all(&self) -> Vec<Task> {
self.tasks.values().cloned().collect()
}
}
#[test]
fn test_add_task() {
let storage = InMemoryStorage::new();
let mut manager = TaskManager::new(storage);
let task = Task {
id: 0, // Will be overwritten
title: "Test task".to_string(),
status: TaskStatus::Todo,
};
assert!(manager.add(task).is_ok());
assert_eq!(manager.count_active(), 1);
}
#[test]
fn test_complete_task() {
let storage = InMemoryStorage::new();
let mut manager = TaskManager::new(storage);
let task = Task {
id: 0,
title: "Test".to_string(),
status: TaskStatus::Todo,
};
manager.add(task).unwrap();
assert!(manager.complete(1).is_ok());
assert_eq!(manager.count_active(), 0);
}
}Key testing principles in Rust:
- Use traits to abstract dependencies (file system, network, etc.)
- Create simple in-memory implementations for tests
- Test business logic without external dependencies
- Use
#[cfg(test)]to keep test code separate
Why this matters:
- Confidence in refactoring
- Catch bugs at compile time (Rust's type system)
- Living documentation
- Rust's testing is built-in with
cargo test
Different people learn differently.
Include:
- Quick Start: "Just show me the code"
- Tutorial: Step-by-step walkthrough
- Explanation: Why and how it works (especially Rust-specific features)
- Reference: Complete API documentation
Example structure:
## Quick Start (for experienced Rust developers)
[Complete code example]
## Tutorial (for Rust beginners)
### Step 1: Understanding Ownership in This Context
[Explain relevant Rust concepts]
### Step 2: Setting Up the Structure
[Build it step by step]
### Step 3: Adding Functionality
[Complete the implementation]
## How It Works (for the curious)
[Deep dive into internals, lifetime annotations, trait bounds]
## Common Rust Pitfalls
[Ownership issues, borrow checker errors, common mistakes]
## API Reference (for reference)
[Complete function signatures, trait bounds, documentation]Use this template for technical tutorials:
# [Feature Name]
Brief description of what we're building and why (1-2 sentences).
## What We're Building
- Clear goal statement
- What problem does this solve?
- Expected outcome
## Architecture
[Visual diagram or description of components and their relationships]
**Why this architecture:**
- Reason for design choices
- Trade-offs considered
- How ownership flows through the system
## Prerequisites
**Required:**
- Rust version (e.g., "Rust 1.70 or later")
- Crates to add to Cargo.toml
- Previous tutorials to complete
**Assumed knowledge:**
- "Basic Rust" (can skip explanations of ownership basics)
- "Familiar with async/await" (can skip tokio basics)
## Implementation
### Step 1: [Setup/Foundation]
**Goal:** [What this step achieves]
[Complete code with file path]
**Why this approach:**
[Brief explanation, including Rust-specific reasoning]
**Cargo.toml additions:**
```toml
[dependencies]
required-crate = "version"[Same structure...]
Common Rust issues at this step:
- [Borrow checker errors and how to fix them]
- [Lifetime annotations explained]
[Same structure...]
How to test the implementation (with examples).
#[cfg(test)]
mod tests {
// Complete test examples
}Run with: cargo test
Issue: [Problem description]
- Solution: [How to fix]
- Why it happens: [Explanation, especially Rust-specific]
- Compiler says: [What error message looks like]
Issue: "cannot borrow as mutable"
- Solution: Use interior mutability (
RefCell,Mutex) or restructure ownership - Why it happens: Violating borrowing rules
- Compiler says:
cannot borrow 'x' as mutable because it is also borrowed as immutable
Ideas for extending the feature.
- What to build next
- How to extend further
- Related tutorials
- Rust documentation links
- Related crates documentation
- External resources
## Code Examples Best Practices
### Always Include:
- ✅ File path: `// src/api/client.rs`
- ✅ All necessary `use` statements
- ✅ Complete type definitions with derives
- ✅ Error handling with `Result`
- ✅ Comments for non-obvious Rust-specific logic
- ✅ Cargo.toml dependencies needed
### Avoid:
- ❌ `// ... rest of the code`
- ❌ `// TODO: implement this`
- ❌ Partial code that won't compile
- ❌ Unexplained `.unwrap()` calls (explain or use `?`)
- ❌ Over-commenting obvious code
- ❌ Using `panic!` without explanation
### Format:
```rust
// src/path/to/file.rs
use std::fmt;
use serde::{Deserialize, Serialize};
/// Does something useful
///
/// # Arguments
/// * `input` - Description of input
///
/// # Returns
/// Description of return value
///
/// # Errors
/// Returns error if X happens
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub field: String,
}
impl Config {
pub fn new(field: String) -> Self {
Self { field }
}
}
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Config: {}", self.field)
}
}
- ✅ Concept is new to the reader (check prerequisites)
- ✅ Concept is project-specific
- ✅ Concept is Rust-specific (ownership, lifetimes, traits)
- ✅ Multiple approaches exist (explain trade-offs)
- ✅ Design decisions are non-obvious
- ✅ Common pitfalls exist
- ✅ Borrow checker implications
- ✅ Reader has stated knowledge level
- ✅ Industry-standard pattern (e.g., REST, MVC)
- ✅ Well-documented in official Rust docs (link instead)
- ✅ Concept already explained in previous tutorial
Example:
// Good: Assumes basic Rust knowledge, explains design
We use dependency injection with traits here to make the code testable:
```rust
pub struct ApiClient<H: HttpClient> {
http_client: H, // Trait bound allows mocking
}This allows us to pass a mock implementation in tests without changing production code.
The trait bound H: HttpClient means we can use any type that implements HttpClient.
Alternative: Could use concrete types, but would need #[cfg(test)] conditionals everywhere.
// Bad: Over-explaining Rust basics to intermediate developers
Rust uses angle brackets for generics. The `H: HttpClient` syntax means H is a generic
type parameter that must implement the HttpClient trait. Traits are like interfaces...
These principles are guidelines, not laws. Deviate when:
- Security concerns: Add complexity if needed for security
- Performance critical: Optimize even if it adds complexity (Rust excels here)
- External requirements: Library/API constraints may require specific patterns
- Team standards: Follow agreed-upon conventions
- Production systems: Add robustness appropriate to the context
- Reader expertise: Adjust depth based on stated knowledge level
- Unsafe code needed: Use
unsafewhen truly necessary, but explain thoroughly
Always explain why you're deviating from these principles.
Before publishing a tutorial, verify:
- Starts with clear goal and overview
- Shows architecture/component diagram
- States prerequisites and assumed Rust knowledge level
- Shows complete, compilable code
- Includes all
usestatements and Cargo.toml dependencies - Explains why, not just what (for new concepts)
- Explains Rust-specific features (ownership, lifetimes, traits)
- Uses popular crates appropriately
- Follows Rust naming conventions
- Uses idiomatic Rust patterns
- Includes proper error handling with
Result - Provides testing examples
- Has troubleshooting section for common Rust errors
- Code is properly modularized
- No unnecessary complexity
- Clear next steps provided
- Uses top-down structure
- Addresses common borrow checker issues
Before submitting code, verify:
- Compiles without warnings (
cargo build) - Passes
cargo clippywithout warnings - Formatted with
cargo fmt - Uses popular crates for common tasks
- Properly modularized (single responsibility)
- No premature abstraction
- Uses traits for abstraction when appropriate
- Dependencies injected for testability
- Includes tests (
cargo testpasses) - Has clear, focused functions/structs
- Proper error handling with
Result<T, E> - No
.unwrap()in production code without justification - Uses appropriate ownership (
&,&mut, owned) - Follows Rust naming conventions
- Follows project patterns and structure
- Documented why for non-obvious decisions
- No clippy warnings
- Idiomatic Rust code
Remember these core values:
- Top-down - Big picture first, details later
- Idiomatic Rust - Embrace ownership, traits, Result, iterators
- Proper componentization - Extract at 3 uses, not before
- No unnecessary complexity - Simple > clever
- Complete working examples - Compilable, not pseudo-code
- Use popular crates - Don't reinvent wheels
- Explain why - Reasoning > mechanics (especially for Rust-specific features)
Goal: Help developers be productive quickly while writing maintainable, idiomatic Rust code.
Motto: Clarity and simplicity trump cleverness. Let the compiler help you write better code.
For beginners: The borrow checker is your friend. Compiler errors teach you to write safer code.