Desarrollo Rust con Claude Code: Cargo, ownership, tests y refactorización CLI
Guía práctica de Rust con Claude Code: Cargo, ownership, tests, fmt, clippy, errores y un CLI pequeño listo para copiar.
Empieza con una tarea Rust pequeña y verificable
Rust suele intimidar al principio por ownership, borrowing, lifetimes y manejo de errores con Result. En español conviene traducir esos términos mentalmente: ownership es “quién posee el dato”, borrowing es “quién lo toma prestado para leerlo o modificarlo”, y lifetime es “durante cuánto tiempo ese préstamo es válido”. Claude Code ayuda mucho si lo usas como compañero de revisión: lee errores del compilador, explica decisiones, modifica archivos concretos y ejecuta checks.
En esta guía construiremos un CLI pequeño llamado tasknote. Lee un archivo tasks.txt con líneas [ ] task y [x] task, muestra un resumen y también puede listar solo tareas abiertas. Las fuentes oficiales que conviene mantener abiertas son Rust Book sobre ownership, Cargo Book para crear proyectos, cargo test, rustfmt, Clippy y Claude Code overview.
Si todavía estás aprendiendo Claude Code, empieza por la guía inicial. Para equipos, documenta comandos y reglas en buenas prácticas de CLAUDE.md y revisa la guía de permisos.
flowchart LR
Prompt["Objetivo y límites"]
Cargo["Proyecto Cargo pequeño"]
Compiler["Leer errores de ownership"]
Tests["Fijar conducta con tests"]
Quality["fmt y clippy"]
Refactor["Refactor con diff pequeño"]
Prompt --> Cargo --> Compiler --> Tests --> Quality --> Refactor
Crear el proyecto con Cargo
Cargo es la herramienta estándar de Rust para crear paquetes, compilar, ejecutar, probar y gestionar dependencias. Antes de pedir a Claude Code que escriba la aplicación, define los comandos de verificación. Así el agente sabe cuándo terminó la tarea.
cargo new tasknote --bin
cd tasknote
cargo run
El proyecto generado incluye Cargo.toml y src/main.rs. Los proyectos recientes pueden usar edition = "2024", así que pide a Claude Code que lea el manifiesto real en vez de copiar configuraciones antiguas. Para este ejemplo usamos solo clap para argumentos CLI y anyhow para añadir contexto a errores.
[package]
name = "tasknote"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
Un buen primer prompt no pide código todavía:
En este proyecto Cargo quiero crear un CLI `tasknote`.
Debe leer líneas `[ ] task` y `[x] task` desde `tasks.txt`.
Primero devuelve solo el diseño de `Cargo.toml`, `src/lib.rs` y `src/main.rs`.
No edites archivos todavía. Explica las decisiones de ownership y manejo de errores.
Esa pausa evita muchos problemas. En Rust, decidir si una función posee un dato, lo toma prestado o devuelve un nuevo valor cambia la forma de todo el programa.
Preguntar bien sobre ownership y borrowing
En este CLI, el contenido del archivo se lee como String; parse_tasks lo recibe como &str porque solo necesita leerlo; el resultado se convierte en Vec<Task> porque cada tarea debe poseer su título; summarize recibe &[Task] porque solo recorre la lista.
Explica el ownership de `parse_tasks(input: &str) -> Vec<Task>` y `summarize(tasks: &[Task]) -> String`.
Usa solo este CLI como ejemplo.
Explica `String`, `&str`, `Vec<Task>` y `&[Task]` con lenguaje de principiante.
No añadas `clone()` innecesarios.
El error más común es pedir “arregla el borrow checker” y aceptar una solución llena de clone(). Clonar no es malo, pero debe ser intencional. Si una función solo lee datos, borrowing suele expresar mejor el diseño.
Ejemplo CLI listo para copiar
Pon la lógica en src/lib.rs. Así puedes probarla sin ejecutar el CLI y el main.rs queda fácil de revisar.
// src/lib.rs
use anyhow::{Context, Result};
use std::{fs, path::Path};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Task {
pub title: String,
pub done: bool,
}
pub fn parse_tasks(input: &str) -> Vec<Task> {
input.lines().filter_map(parse_task_line).collect()
}
fn parse_task_line(line: &str) -> Option<Task> {
let trimmed = line.trim();
if let Some(title) = trimmed
.strip_prefix("[x] ")
.or_else(|| trimmed.strip_prefix("[X] "))
{
return Some(Task {
title: title.trim().to_string(),
done: true,
});
}
if let Some(title) = trimmed.strip_prefix("[ ] ") {
return Some(Task {
title: title.trim().to_string(),
done: false,
});
}
None
}
pub fn summarize(tasks: &[Task]) -> String {
let total = tasks.len();
let done = tasks.iter().filter(|task| task.done).count();
let open = total.saturating_sub(done);
format!("{total} tasks: {done} done, {open} open")
}
pub fn read_tasks(path: impl AsRef<Path>) -> Result<Vec<Task>> {
let path = path.as_ref();
let content = fs::read_to_string(path)
.with_context(|| format!("failed to read {}", path.display()))?;
Ok(parse_tasks(&content))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_markdown_style_tasks() {
let tasks = parse_tasks("[ ] write parser\n[x] add tests\n[X] run clippy\n");
assert_eq!(
tasks,
vec![
Task {
title: "write parser".to_string(),
done: false,
},
Task {
title: "add tests".to_string(),
done: true,
},
Task {
title: "run clippy".to_string(),
done: true,
},
]
);
}
#[test]
fn ignores_unrecognized_lines() {
let tasks = parse_tasks("# Sprint notes\n- plain bullet\n[ ] keep this\n");
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].title, "keep this");
}
#[test]
fn summarizes_counts() {
let tasks = parse_tasks("[ ] one\n[x] two\n[ ] three\n");
assert_eq!(summarize(&tasks), "3 tasks: 1 done, 2 open");
}
}
El punto de entrada queda pequeño.
// src/main.rs
use anyhow::Result;
use clap::Parser;
use std::path::PathBuf;
use tasknote::{read_tasks, summarize};
#[derive(Parser, Debug)]
#[command(name = "tasknote", about = "Summarize simple task files")]
struct Cli {
#[arg(short, long, default_value = "tasks.txt")]
file: PathBuf,
#[arg(long)]
only_open: bool,
}
fn main() -> Result<()> {
let args = Cli::parse();
let tasks = read_tasks(&args.file)?;
if args.only_open {
for task in tasks.iter().filter(|task| !task.done) {
println!("- {}", task.title);
}
} else {
println!("{}", summarize(&tasks));
}
Ok(())
}
Ejemplo de tasks.txt:
[ ] write parser
[x] add unit tests
[ ] run clippy
Comandos de verificación:
cargo fmt
cargo test
cargo clippy --all-targets -- -D warnings
cargo run -- --file tasks.txt
cargo run -- --file tasks.txt --only-open
main devuelve Result<()>, así que un archivo faltante puede mostrar contexto útil. Para un CLI, una ruta incorrecta es un error recuperable; no conviene esconderlo detrás de un unwrap().
Incluir tests, fmt y Clippy en la tarea
Claude Code debe terminar con evidencia, no solo con código. Pide los checks dentro del prompt.
Implementa `src/lib.rs` y `src/main.rs`.
Después ejecuta `cargo fmt`, `cargo test` y `cargo clippy --all-targets -- -D warnings`.
Si algo falla, resume el error y corrige con el menor diff razonable.
No uses `unwrap()` fuera de tests.
Claude Code puede leer el repositorio, editar archivos y ejecutar comandos, como explica su documentación oficial. Aun así, revisa el git diff, confirma que no tocó archivos fuera del alcance y repite los comandos críticos cuando el cambio afecte producción.
Casos de uso reales
Primer caso: añadir una opción pequeña a un CLI existente. Si necesitas --json, pide mantener summarize estable y crear una función separada de salida JSON.
Segundo caso: aprender de errores de ownership. Pega el error completo y pide que explique quién posee el valor, quién lo toma prestado y qué referencia vive demasiado.
Tercer caso: corregir bugs con tests primero. Escribe tests para líneas vacías, [X] y líneas inválidas antes de tocar el parser.
Cuarto caso: refactorizar sin romper. Cuando main.rs crezca, separa parsing, I/O y presentación, manteniendo las firmas públicas.
Trampas frecuentes
No conviertas cada problema de borrowing en clone(). Pregunta si el dato realmente necesita nuevo dueño.
No dejes unwrap() en rutas productivas. Archivos, configuración y entrada de usuario fallan; usa Result con contexto.
No refactorices sin tests. Claude Code puede cambiar mucho rápido, pero los diffs grandes son difíciles de revisar.
No ejecutes formateo global en un workspace compartido sin aclarar alcance. Define crate, archivos y comandos permitidos.
Prompt seguro para refactorizar
Refactoriza con seguridad el parser de `tasknote`.
Condiciones:
- Mantener el significado de `[ ] task` y `[x] task`
- No cambiar las firmas públicas de `parse_tasks`, `summarize` ni `read_tasks`
- Tocar solo `src/lib.rs` y sus tests
- No añadir `clone()` innecesarios
- Primero proponer un plan de 3 líneas y esperar aprobación
Después:
- Ejecutar `cargo fmt`
- Ejecutar `cargo test`
- Ejecutar `cargo clippy --all-targets -- -D warnings`
- Resumir el diff y explicar decisiones de ownership
Este prompt da al agente un arnés de trabajo: límites, checks y reporte. Rust encaja bien con este estilo porque el compilador, los tests y Clippy actúan como revisores adicionales.
CTA y próximos pasos
Como práctica personal, amplía el CLI con salida JSON, CSV, escaneo de directorios o un formato con serde. Para equipos, deja en CLAUDE.md la edición de Rust, comandos obligatorios, política sobre unwrap(), crates permitidos y checklist de revisión.
ClaudeCodeLab ofrece prompts, plantillas, guías de configuración y formación para equipos que quieren usar Claude Code con seguridad. Empieza por la chuleta gratuita, revisa productos y plantillas o usa formación y consultoría para adaptarlo a tu repositorio. También ayudan TDD con Claude Code y el checklist de revisión.
Nota de prueba: este flujo funciona mejor cuando la lógica vive en src/lib.rs, los comportamientos están fijados con cargo test y Claude Code explica ownership antes de editar. En mi experiencia, eso reduce clones innecesarios y deja diffs más pequeños que pedir “crea todo el CLI” en un solo prompt.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.