Claude Code for Leptos Rust — Workflow Guide

Written by Michael Lip · Solo founder of Zovo · $400K+ on Upwork · 100% JSS Join 50+ builders · More at zovo.one

The Setup

You are building a full-stack web application with Leptos, the Rust framework for building reactive web UIs with fine-grained reactivity. Leptos compiles to WebAssembly for the client and native code for the server, enabling true isomorphic Rust. Claude Code can write Leptos components, but it generates React JSX or Yew syntax instead.

What Claude Code Gets Wrong By Default

  1. Writes React JSX syntax. Claude generates <div className="...">{items.map(i => <Item />)}</div>. Leptos uses RSX with view! { <div class="...">{items.iter().map(|i| view! { <Item/> }).collect_view()}</div> }.

  2. Uses React hooks for state. Claude writes const [count, setCount] = useState(0). Leptos uses signals: let (count, set_count) = signal(0) with fine-grained reactivity, not virtual DOM diffing.

  3. Creates separate client and server projects. Claude scaffolds separate frontend and backend repos. Leptos has built-in server functions with #[server] attribute — client and server code live in the same project.

  4. Uses JavaScript for client-side logic. Claude adds <script> tags for interactivity. Leptos compiles Rust to WebAssembly — all client logic is Rust. No JavaScript needed (though JS interop is available).

The CLAUDE.md Configuration

# Leptos Full-Stack Rust Project

## Framework
- UI: Leptos (Rust, fine-grained reactivity, WASM)
- Build: cargo-leptos (handles client + server builds)
- Server: Actix or Axum backend
- Styling: Tailwind CSS with trunk or cargo-leptos

## Leptos Rules
- Components: #[component] fn MyComponent() -> impl IntoView { view! { } }
- Signals: let (value, set_value) = signal(initial)
- Derived: let doubled = move || count() * 2
- Effects: Effect::new(move |_| { log!("{}", count()) })
- Server functions: #[server] async fn get_data() -> Result<T, ServerFnError>
- Events: on:click=move |_| set_count(count() + 1)
- Conditional: <Show when=move || count() > 5>...</Show>
- Lists: <For each=move || items() key=|i| i.id let:item>...</For>

## Conventions
- Components in src/components/ directory
- Server functions in src/server/ directory
- Shared types in src/models/ (used by both client and server)
- Use cargo-leptos for development: cargo leptos watch
- Tailwind: configured in Cargo.toml [package.metadata.leptos]
- No JavaScript — all logic in Rust compiled to WASM
- Error handling with Result and ServerFnError

Workflow Example

You want to create a todo list with server persistence. Prompt Claude Code:

“Create a Leptos todo list with add and toggle functionality. Use server functions to save todos to a database. Implement optimistic updates so the UI responds immediately while the server operation completes.”

Claude Code should create a #[component] with signal-based state, #[server] functions for CRUD operations, create_action() for optimistic updates, and view! macro RSX with <For> iteration and on:click event handlers.

Common Pitfalls

  1. Forgetting the move keyword on closures. Claude writes event handlers without move. Leptos closures that access signals need move to capture them: on:click=move |_| set_count.update(|c| *c += 1). Missing move causes borrow checker errors.

  2. Server function serialization issues. Claude uses complex Rust types in server function arguments. Server functions serialize arguments across the WASM-server boundary — types must implement Serialize + Deserialize. Keep server function signatures simple.

  3. WASM binary size. Claude does not configure release optimizations. Leptos WASM bundles can be large. Configure wasm-opt, opt-level = 'z', and lto = true in Cargo.toml release profile for production builds.