Complete guide to Cargo workspaces, ownership & lifetimes, async Tokio, Axum/Actix-web, error handling, Clippy, and WebAssembly — with 40+ copy-paste prompts.
The most important first step is a well-structured CLAUDE.md that teaches Claude your Rust edition, MSRV, workspace layout, and which Clippy lints to enforce. Claude will read this every session and follow it consistently.
# Project: My Rust Service
## Rust Environment
- Edition: 2021
- MSRV: 1.75 (stable)
- Async runtime: Tokio (current-thread for tests)
- Error handling: thiserror in lib crates, anyhow in binaries
## Workspace Layout
- crates/core — domain types and business logic (no I/O)
- crates/db — database layer via sqlx (Postgres)
- crates/api — Axum HTTP server
- crates/cli — binary, thin wrapper over core + db
## Commands
- Test: cargo test --all-features --workspace
- Lint: cargo clippy --all-targets --all-features -- -D warnings
- Format: cargo fmt --all
- Build: cargo build --release
- Generate: cargo sqlx prepare (run after schema migrations)
## Conventions
- Avoid unwrap()/expect() outside tests; use ? or proper error types
- No blocking I/O in async functions — use spawn_blocking
- Prefer Arc over Rc unless guaranteed single-thread
- All public API items need doc comments
- Do NOT touch crates/generated/ — it is auto-generated from protobuf
Claude Code has deep knowledge of Rust's ownership model and can both fix borrow-checker errors and proactively write code that won't trigger them. Paste the full compiler error — Claude reads the error code and span to understand the exact problem.
When you hit error[E0502] (cannot borrow while already borrowed) or error[E0505] (value moved), paste the full error including the note lines. Claude identifies the root cause and rewrites just the relevant scope — it won't blindly add .clone() everywhere.
// Ask: "Refactor this to avoid the borrow conflict when iterating and mutating"
// Claude recognizes when split_at_mut or index-based access fixes the issue
let (left, right) = slice.split_at_mut(mid);
for item in left.iter_mut() {
item.process(&right); // no conflict — separate slices
}
// Ask: "Use the HashMap Entry API to avoid double-lookup"
// Claude replaces the contains_key + insert pattern with one atomic operation
let count = map.entry(key).or_insert(0);
*count += 1;
// Ask: "Use Cow to avoid allocating when the input needs no transformation"
use std::borrow::Cow;
fn normalize(s: &str) -> Cow<str> {
if s.chars().all(|c| c.is_ascii_lowercase()) {
Cow::Borrowed(s) // zero allocation
} else {
Cow::Owned(s.to_lowercase())
}
}
Claude understands Cargo.toml deeply — features, optional dependencies, workspace inheritance, and build scripts.
Claude Code writes idiomatic async Rust — not just async fn wrappers, but correct Tokio patterns that avoid blocking, handle cancellation, and compose well.
Tip: Add TOKIO_WORKER_THREADS=1 to your test environment in CLAUDE.md — it makes async tests deterministic and easier for Claude to reason about.
// Ask: "Add a timeout to this operation using tokio::select!"
use tokio::time::{timeout, Duration};
let result = timeout(Duration::from_secs(5), fetch_data()).await?;
// Or with select! for more control:
tokio::select! {
res = fetch_data() => res?,
_ = tokio::time::sleep(Duration::from_secs(5)) => {
return Err(MyError::Timeout);
}
};
// Ask: "Fan out these tasks with JoinSet and collect all results"
use tokio::task::JoinSet;
let mut set = JoinSet::new();
for url in urls {
set.spawn(fetch(url));
}
let mut results = Vec::new();
while let Some(res) = set.join_next().await {
results.push(res??);
}
results
// Ask: "Use an mpsc channel to pipeline processing across tasks"
use tokio::sync::mpsc;
let (tx, mut rx) = mpsc::channel::- (128);
tokio::spawn(async move {
for item in source { tx.send(item).await.unwrap(); }
});
while let Some(item) = rx.recv().await {
process(item).await?;
}
// Ask: "Wrap this blocking file operation safely for async context"
use tokio::task::spawn_blocking;
let data = spawn_blocking(|| {
// blocking I/O runs in a thread-pool, not on the async executor
std::fs::read_to_string("large_file.txt")
}).await??; // double ? — JoinError then io::Error
Claude supports both Axum and Actix-web idioms — including state injection, middleware, structured errors, and integration testing.
// Ask: "Add a POST /api/items endpoint with shared database state"
use axum::{extract::{Json, State}, http::StatusCode, response::IntoResponse};
use std::sync::Arc;
async fn create_item(
State(db): State<Arc<Database>>,
Json(payload): Json<CreateItem>,
) -> impl IntoResponse {
match db.insert(payload).await {
Ok(item) => (StatusCode::CREATED, Json(item)).into_response(),
Err(e) => AppError::from(e).into_response(),
}
}
// Router wiring
Router::new()
.route("/api/items", post(create_item))
.with_state(Arc::new(db))
// Ask: "Add a POST endpoint with web::Data state"
use actix_web::{web, HttpResponse, Responder};
async fn create_item(
db: web::Data<Database>,
body: web::Json<CreateItem>,
) -> impl Responder {
match db.insert(body.into_inner()).await {
Ok(item) => HttpResponse::Created().json(item),
Err(e) => HttpResponse::InternalServerError().json(e.to_string()),
}
}
// App config
App::new()
.app_data(web::Data::new(db))
.route("/api/items", web::post().to(create_item))
Claude follows the Rust ecosystem's two-crate convention: thiserror for library error types (gives callers something to match on), anyhow for application code (maximum ergonomics, context-rich messages).
// Library crate — thiserror (callers can match on variants)
#[derive(Debug, thiserror::Error)]
pub enum DatabaseError {
#[error("record not found: {id}")]
NotFound { id: u64 },
#[error("connection failed: {0}")]
Connection(#[from] sqlx::Error),
#[error("serialization failed: {0}")]
Serialization(#[from] serde_json::Error),
}
// Application crate — anyhow (human-readable error chain)
use anyhow::{Context, Result};
async fn load_config(path: &Path) -> Result<Config> {
let raw = tokio::fs::read_to_string(path).await
.with_context(|| format!("failed to read config from {}", path.display()))?;
serde_json::from_str(&raw)
.context("config file is not valid JSON")
}
Rust's testing story is built into Cargo. Claude writes unit tests, integration tests in tests/, doc-tests, and Criterion benchmarks — all without external test runners.
Tip: Add cargo test --doc to your CLAUDE.md test command — doc tests catch documentation examples that drift from the actual API.
Claude writes idiomatic Clap v4 CLIs using the derive API — complete with subcommands, value parsing, environment variable fallbacks, and shell completions.
// Ask: "Generate a CLI with two subcommands using clap derive"
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "mytool", about = "Does things")]
struct Cli {
#[arg(short, long, env = "MYTOOL_VERBOSE")]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Run { #[arg(short)] port: u16 },
Deploy { #[arg(long)] env: String },
}
Claude Code handles the full WASM stack — wasm-bindgen type conversions, async interop with JavaScript Promises, and wasm-pack build configuration.
// Ask: "Expose a Rust parser to JavaScript as a WASM module"
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn parse_document(input: &str) -> Result<JsValue, JsError> {
let doc = my_parser::parse(input)
.map_err(|e| JsError::new(&e.to_string()))?;
serde_wasm_bindgen::to_value(&doc)
.map_err(|e| JsError::new(&e.to_string()))
}
// Async: returning a JS Promise
#[wasm_bindgen]
pub async fn fetch_and_parse(url: String) -> Result<JsValue, JsError> {
let text = fetch_text(&url).await?;
parse_document(&text)
}
Claude Code writes unsafe Rust conservatively — it always adds a // SAFETY: comment explaining why an invariant holds, and wraps unsafe blocks in safe abstraction layers.
Best practice: In your CLAUDE.md, write: "All unsafe blocks must have a SAFETY comment. Create safe wrapper functions around every unsafe operation." Claude will follow this consistently.
Rust's zero-cost abstractions don't guarantee maximum performance — allocation patterns, cache behavior, and algorithmic choices all matter. Claude helps identify and fix hot paths.
Yes — Claude Code has deep understanding of Rust's ownership model. It correctly places lifetimes, chooses between &T, &mut T, Box<T>, Rc<T>, and Arc<T>, and explains why a borrow-checker error occurs. When you paste a compiler error, Claude identifies whether it's a lifetime issue, a moved value, or a simultaneous mutable/immutable borrow, and rewrites the code to fix it — not just move the error elsewhere.
Claude reads your Cargo.toml and workspace Cargo.toml to understand crate boundaries, shared dependencies, and feature flags. It navigates multi-crate workspaces correctly — knowing which crate owns a type and how to import it across crate boundaries. Add your workspace layout to CLAUDE.md with a one-line description of each crate's role.
Claude Code understands async/await idioms, Tokio's runtime model, and common async patterns: tokio::spawn, tokio::select!, JoinSet, and the full channel family (mpsc, broadcast, watch, oneshot). It avoids common pitfalls like blocking the async executor with sync I/O — it suggests spawn_blocking when appropriate. It also understands the async-trait crate and Rust 2024's native async-in-traits.
Claude follows idiomatic Rust error handling convention: thiserror in library crates (gives callers matchable variants), anyhow in application code (maximum ergonomics with context chains). It uses .context() and .with_context() to add actionable error messages at each layer, avoids string-typed errors, and never uses unwrap() outside tests or initialization code.
Claude Code treats Clippy as a first-class tool. Add cargo clippy --all-targets --all-features -- -D warnings to your CLAUDE.md test command and Claude fixes Clippy warnings it introduces before completing a task. It understands every Clippy lint by name — including pedantic and nursery lints — and can explain why a lint fired. Prompt: "Fix all Clippy warnings in src/parser/ without changing public API or behavior" to clean up a legacy module.
Claude Code supports both Axum and Actix-web. For Axum, it understands the extractor pattern (State<T>, Json<T>, Path<T>), Router composition, middleware via tower layers, and error responses via IntoResponse. For Actix-web, it knows web::Data<T>, Responder, and the App/ServiceConfig pattern. Both frameworks get full integration test support with test clients.
Yes. Claude Code understands wasm-bindgen, wasm-pack, and the js-sys/web-sys crates. It writes #[wasm_bindgen] exported functions with correct type conversions, handles JsValue / JsError, and sets up wasm-pack build pipelines. For async WASM it uses wasm-bindgen-futures to bridge Rust Futures with JavaScript Promises.
Essential CLAUDE.md items for Rust: (1) Rust edition and MSRV, (2) test command: cargo test --all-features --workspace, (3) lint command: cargo clippy --all-targets --all-features -- -D warnings, (4) format: cargo fmt --all, (5) build command, (6) workspace crate descriptions, (7) async runtime choice, (8) error handling convention (anyhow vs thiserror), (9) any intentional unsafe with the reason. The template at the top of this guide covers all of these.