Rust

Claude Code for Rust Developers

Complete guide to Cargo workspaces, ownership & lifetimes, async Tokio, Axum/Actix-web, error handling, Clippy, and WebAssembly — with 40+ copy-paste prompts.

Contents

  1. Project Setup & CLAUDE.md
  2. Ownership, Lifetimes & Borrowing
  3. Cargo & Workspace Management
  4. Async Rust & Tokio
  5. Web Frameworks (Axum, Actix-web)
  6. Error Handling (thiserror, anyhow)
  7. Testing & Benchmarks
  8. CLI Tools with Clap
  9. WebAssembly (WASM)
  10. Unsafe Rust & FFI
  11. Performance & Profiling
  12. FAQ

Project Setup & CLAUDE.md

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.

CLAUDE.md Template — Rust Project

# 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
Setup
"Create a Cargo workspace with three crates: core (lib), api (lib using Axum), and cli (binary). Set up shared dev-dependencies and a workspace-level Cargo.toml with edition 2021."
CLAUDE.md
"Generate a CLAUDE.md for my Rust Axum project with Tokio, sqlx Postgres, and thiserror. Include test, lint, and format commands."
Cargo features
"Add a 'mock-db' feature flag to crates/db that swaps the Postgres implementation for an in-memory HashMap, keeping the same trait interface."

Ownership, Lifetimes & Borrowing

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.

Fixing Borrow Checker Errors

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.

Ownership
"Here is a borrow checker error: [paste error]. Explain exactly why this violates Rust's ownership rules, then show the minimal fix without unnecessary clones."
Lifetimes
"Add the minimum lifetime annotations needed to make this struct hold a reference to the input string, then explain each annotation."
Smart pointers
"This type needs to be shared across threads. Decide between Arc>, Arc>, and a channel-based approach, and implement the best fit with an explanation."
Interior mutability
"Refactor this struct to use interior mutability with Cell or RefCell so callers don't need &mut self for these getter methods. Explain the tradeoffs."

Common Patterns Claude Handles Well

Split borrows
Entry API
Cow<T>
// 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())
    }
}

Cargo & Workspace Management

Claude understands Cargo.toml deeply — features, optional dependencies, workspace inheritance, and build scripts.

Dependencies
"Audit Cargo.toml for duplicate transitive dependencies and suggest deduplication using workspace.dependencies with unified versions."
Build script
"Write a build.rs that compiles a C library in vendor/ and links it statically. Use cc crate and set the correct rerun-if-changed directives."
Proc macro
"Create a derive macro #[derive(Builder)] that generates a builder struct for any struct with named fields, following the builder pattern."
Workspace deps
"Migrate all crates in this workspace to use workspace.dependencies so versions are defined once and inherited with { workspace = true }."

Async Rust & Tokio

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.

Key Async Patterns

select!
JoinSet
Channels
Blocking I/O
// 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
Concurrency
"Refactor this sequential loop to make 50 concurrent HTTP requests, limit to 10 at a time with a semaphore, and return all errors collected — not just the first."
Cancellation
"Add graceful shutdown to this Tokio service: listen for SIGTERM/SIGINT, stop accepting new requests, and finish in-flight ones within 30 seconds."
Async trait
"Define a trait Repository with async methods and implement it for both a Postgres backend and an in-memory mock, using async-trait or the 2024 edition syntax."
Rate limiting
"Add rate limiting to this Tokio HTTP client: max 100 req/s using governor crate, with per-host buckets and a backpressure mechanism."

Web Frameworks: Axum & Actix-web

Claude supports both Axum and Actix-web idioms — including state injection, middleware, structured errors, and integration testing.

Axum
Actix-web
// 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))
Middleware
"Add request tracing middleware to my Axum app that logs method, path, status, and duration using tower-http's TraceLayer."
Auth
"Write a JWT extractor for Axum that validates a Bearer token, extracts claims into a Claims struct, and returns 401 with a JSON error on invalid tokens."
Integration test
"Write an integration test for the POST /api/items endpoint using axum::test, seeding a real Postgres test database with sqlx fixtures."
Structured errors
"Create an AppError enum with thiserror that covers NotFound, Validation, Database, and Unauthorized cases, and implement IntoResponse for Axum."

Error Handling: thiserror & anyhow

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")
}
Error design
"Design the error hierarchy for a payment processing library — use thiserror with variants for validation, network, and idempotency failures."
Context chains
"Refactor these Result returns to add context strings at each layer so error messages show the full operation chain, not just the low-level I/O failure."
From impls
"Add #[from] conversions to my error enum so sqlx::Error, reqwest::Error, and serde_json::Error all convert automatically with ? operator."

Testing & Benchmarks

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.

Unit tests
"Write comprehensive unit tests for the parse_duration function covering happy paths, edge cases, and every error variant. Include doc tests for the public API."
Integration tests
"Create an integration test in tests/ that spins up a real Axum server with a test database, exercises the full request lifecycle, and tears down cleanly."
Mocking
"Create a mock implementation of the EmailSender trait using mockall that can expect specific calls and return controlled results in tests."
Benchmarks
"Add Criterion benchmarks for the three serialization approaches we discussed: serde_json, bincode, and postcard. Include warm-up and comparison groups."
Property tests
"Write proptest property-based tests for the round-trip invariant: serializing then deserializing any valid Config always produces the identical value."
Async tests
"Write async tests using #[tokio::test] for the repository layer. Use sqlx::test attribute to get an isolated transaction per test that auto-rolls back."

Tip: Add cargo test --doc to your CLAUDE.md test command — doc tests catch documentation examples that drift from the actual API.

CLI Tools with Clap

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 },
}
Subcommands
"Build a CLI with three subcommands (init, run, status), env-var fallbacks for all flags, and colored output using owo-colors."
Completions
"Add a 'completions' subcommand that generates shell completions for bash, zsh, and fish using clap_complete and prints them to stdout."
Progress
"Add a progress bar to the download loop using indicatif with accurate byte counts, ETA, and a spinner for indeterminate phases."

WebAssembly (WASM)

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)
}
WASM export
"Export our Markdown parser to WASM with wasm-bindgen. The JS API should accept a string and return parsed tokens as a JSON array."
WASM build
"Set up wasm-pack build for web target with optimized release flags (opt-level='z', lto=true). Output to pkg/ and show how to import in a Vite project."
WASM size
"Audit the WASM binary size using twiggy. Find the top contributors and suggest which can be removed with feature flags or alternative crates."

Unsafe Rust & FFI

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.

FFI binding
"Write an FFI binding for libsodium's crypto_secretbox_easy function. Include the C extern declaration, a safe Rust wrapper, and a test."
Safety audit
"Audit all unsafe blocks in src/buffer.rs. For each one, verify or add a SAFETY comment, and refactor any that can become safe without major performance loss."
Bindgen
"Run bindgen on include/mylib.h and generate Rust bindings. Then wrap the raw bindings in a safe Rust API that owns allocations and prevents use-after-free."

Performance & Profiling

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.

Allocation
"Profile this parser for allocations using cargo-instruments or valgrind/massif. Identify the hottest allocation sites and suggest arena or small-string optimizations."
SIMD
"Replace this byte-scanning loop with SIMD using the memchr crate or std::simd. Show both a portable version and an x86 AVX2 version with a runtime fallback."
Rayon
"Parallelize this CPU-bound map operation using rayon's par_iter(). Add a benchmark comparing sequential vs parallel at 10K, 100K, and 1M items."
Flamegraph
"Set up cargo-flamegraph for this binary and document the sampling command in CLAUDE.md. Then interpret the flamegraph output and suggest the top optimization."

Frequently Asked Questions

Can Claude Code understand Rust's ownership and borrow checker?

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.

How does Claude Code work with Cargo workspaces?

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.

Does Claude Code understand async Rust and Tokio?

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.

How does Claude Code handle error handling with thiserror and anyhow?

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.

Can Claude Code help fix Clippy warnings?

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.

How does Claude Code work with Axum and Actix-web?

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.

Can Claude Code help with WebAssembly in Rust?

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.

What should I put in CLAUDE.md for a Rust project?

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.

Related Guides

More Claude Code Tools

⚡ Using Claude Code? 30 power prompts that 2× your output · £5 £3 first 10Get PDF £3 →