首页 › tutorials › Claude Code Rust全栈:Axum后端入门实战

Claude Code Rust全栈:Axum后端入门实战

概述

本教程面向零Rust基础但熟悉Web开发概念的开发者,手把手教你用Claude Code从零搭建一个基于Axum框架的REST API服务。你将学会配置Rust专属的CLAUDE.md、利用Claude Code克服所有权和借用检查器的学习曲线、实现CRUD接口、集成SQLite数据库,并编写完整的集成测试。学完后你能独立用Claude Code开发生产级Rust后端服务。

适用人群:有Python/Node.js/Go后端经验、想用Claude Code快速上手Rust Web开发的工程师


前置条件

工具 最低版本 安装方式
Node.js 18+ curl -fsSL https://deb.nodesource.com/setup_20.x \| sudo -E bash - && sudo apt install -y nodejs
Claude Code CLI 最新 npm install -g @anthropic-ai/claude-code
Rust toolchain 1.75+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \| sh
cargo-nextest 最新 cargo install cargo-nextest(可选,更好的测试体验)
SQLite3 3.x sudo apt install sqlite3 libsqlite3-dev

验证安装:

# 确认工具链就绪
rustc --version    # 期待: rustc 1.75+
cargo --version    # 期待: cargo 1.75+
claude --version   # 期待: claude-code 最新版
node --version     # 期待: v18+

第一步:创建项目并配置CLAUDE.md

Rust项目的成功与否,很大程度上取决于Claude Code是否理解你的项目结构和约束。一份精心编写的CLAUDE.md能让Claude写出地道的Rust代码,而不是"C++风格的Rust"。

1.1 用Cargo初始化项目

mkdir rust-todo-api && cd rust-todo-api
cargo init

1.2 添加核心依赖

编辑 Cargo.toml:

[package]
name = "rust-todo-api"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tower-http = { version = "0.6", features = ["cors", "trace"] }
rusqlite = { version = "0.32", features = ["bundled"] }
thiserror = "2"
anyhow = "1"

[dev-dependencies]
axum-test = "16"

在终端运行 cargo check 验证依赖能正确编译:

cargo check

预期输出:

    Checking rust-todo-api v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 12s

1.3 创建Rust专属CLAUDE.md

这是整个教程最关键的一步。在项目根目录创建 CLAUDE.md:

# Rust Todo API Project

## Build & Run Commands
- Build: `cargo build`
- Run: `cargo run`
- Test: `cargo test`
- Check (fast): `cargo check`
- Lint: `cargo clippy -- -D warnings`
- Format: `cargo fmt`

## Architecture
- `src/main.rs` — 入口、服务器启动、路由注册
- `src/routes/` — API路由处理器(每个资源一个文件)
- `src/models/` — 数据模型和序列化
- `src/db.rs` — 数据库连接池和初始化
- `src/errors.rs` — 统一错误处理

## Rust Style Rules
- 使用 `thiserror` 定义库错误,`anyhow` 处理应用层错误
- 公共API使用owned类型(String而非&str),避免生命周期标注扩散
- 异步函数使用 `async fn`,不要手动实现 Future
- 使用 `axum::extract::State` 共享状态,不用 `Mutex<Arc<...>>`
- 错误返回 `Result<T, AppError>`,不要 panic
- 数据库操作使用 `r2d2` 连接池模式
- import按标准库/外部crate/本地模块分组,组间空行
- 不要在公共结构体中使用 `pub crate` 可见性
- 使用 `#[derive(Debug, Clone, Serialize, Deserialize)]` 标准派生

## Database
- SQLite with rusqlite(bundled模式)
- Schema放在 `migrations/` 目录
- 连接池使用自定义简单池(本项目规模不需要r2d2)

## API Conventions
- RESTful: GET/POST/PUT/DELETE
- JSON请求/响应
- 路径参数用 Path<Id>,查询参数用 Query<Params>
- 统一响应格式: `{ "data": ..., "message": "..." }`

为什么这份CLAUDE.md如此重要?

Claude Code在面对Rust项目时最大的挑战是所有权系统。如果没有明确的规则,它会倾向于写出过度使用 Arc<Mutex<>> 或到处标注生命周期的代码。上面这份文件告诉Claude Code:使用owned类型、用State共享状态、不要滥用锁——这些都是Rust社区的最佳实践。

常见问题:

问题 原因 解决
cargo check 报链接错误 缺少C编译器(rusqlite bundled需要) sudo apt install build-essential
依赖下载慢 crates.io网络问题 配置 ~/.cargo/config.toml 使用镜像
CLAUDE.md没生效 文件不在项目根目录 确保在 cargo init 的同一目录

第二步:数据模型与错误处理

让Claude Code帮你生成Rust结构体比手写更可靠——它不会忘记derive宏和可见性修饰符。

在终端中启动Claude Code:

claude

然后输入以下提示词:

基于CLAUDE.md中的架构设计,创建以下文件:

1. src/models/todo.rs — Todo模型,包含id(uuid)、title(String)、completed(bool)、created_at(chrono::DateTime)
2. src/models/mod.rs — 模块导出
3. src/models/create_todo.rs — CreateTodoRequest(title必填,completed默认false)
4. src/errors.rs — AppError枚举,实现IntoResponse,包含NotFound、BadRequest、Internal variants

要求:
- 所有结构体 derive Debug, Clone, Serialize, Deserialize
- AppError 的 HTTP 状态码:NotFound=404, BadRequest=400, Internal=500
- 使用 thiserror derive Error

Claude Code会生成以下核心文件。

src/errors.rs:

use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde_json::json;

#[derive(Debug, thiserror::Error)]
pub enum AppError {
    #[error("Resource not found: {0}")]
    NotFound(String),

    #[error("Bad request: {0}")]
    BadRequest(String),

    #[error("Internal server error: {0}")]
    Internal(String),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match &self {
            AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
            AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
            AppError::Internal(msg) => {
                tracing::error!("Internal error: {}", msg);
                ("Internal server error".to_string())
            }
        };

        (status, Json(json!({ "error": message }))).into_response()
    }
}

impl From<rusqlite::Error> for AppError {
    fn from(err: rusqlite::Error) -> Self {
        AppError::Internal(err.to_string())
    }
}

src/models/todo.rs:

use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Todo {
    pub id: Uuid,
    pub title: String,
    pub completed: bool,
    pub created_at: NaiveDateTime,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateTodoRequest {
    pub title: String,
    #[serde(default)]
    pub completed: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateTodoRequest {
    pub title: Option<String>,
    pub completed: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TodoResponse {
    pub data: Todo,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TodoListResponse {
    pub data: Vec<Todo>,
}

运行 cargo check 验证编译:

cargo check

Claude Code使用技巧:当遇到编译错误时,直接把错误信息粘贴给Claude Code。它能精确理解Rust编译器的诊断信息并给出修复方案。比你自己翻文档快10倍。


第三步:数据库层

在Claude Code中输入:

创建 src/db.rs,实现以下功能:
1. DbPool 类型别名 = Vec<Mutex<rusqlite::Connection>> 的简单封装(用 RefCell 或直接用 ConnectionManager)
2. 实际上用最简单的方式:直接传 Connection,用 Mutex<Connection> 包装
3. init_db() 函数:创建 SQLite 内存数据库,执行建表SQL,返回 Mutex<Connection>
4. 建表SQL:
   CREATE TABLE IF NOT EXISTS todos (
     id TEXT PRIMARY KEY,
     title TEXT NOT NULL,
     completed BOOLEAN NOT NULL DEFAULT 0,
     created_at TEXT NOT NULL
   );

参考CLAUDE.md中的规则,保持简洁。

Claude Code生成的 src/db.rs:

use std::sync::{Arc, Mutex};
use rusqlite::Connection;
use crate::errors::AppError;

pub type DbPool = Arc<Mutex<Connection>>;

pub fn init_db() -> Result<DbPool, AppError> {
    let conn = Connection::open_in_memory()
        .map_err(|e| AppError::Internal(e.to_string()))?;

    conn.execute_batch(
        "CREATE TABLE IF NOT EXISTS todos (
            id TEXT PRIMARY KEY,
            title TEXT NOT NULL,
            completed BOOLEAN NOT NULL DEFAULT 0,
            created_at TEXT NOT NULL
        )"
    ).map_err(|e| AppError::Internal(e.to_string()))?;

    Ok(Arc::new(Mutex::new(conn)))
}

这里Claude Code做出了一个关键选择:Arc<Mutex<Connection>> 对于单连接场景足够简单。虽然生产环境应该用连接池,但对于入门教程和演示项目,这是最清晰的方式。CLAUDE.md中的"不要用Mutex<Arc<>>"规则在此处不矛盾——我们用的是 Arc<Mutex<Connection>>(Arc在外层),而非 Mutex<Arc<Connection>>。


第四步:实现CRUD路由

这是核心部分。在Claude Code中分步骤输入:

创建 src/routes/todos.rs,实现完整的Todo CRUD:

GET    /todos          — 列出所有todo
GET    /todos/:id      — 获取单个todo  
POST   /todos          — 创建todo
PUT    /todos/:id      — 更新todo
DELETE /todos/:id      — 删除todo

要求:
- 使用 axum::extract::State<DbPool> 获取数据库连接
- 使用 axum::extract::Path 获取路径参数
- 路径参数 id 类型为 uuid::Uuid
- 错误处理使用 AppError
- SQL查询使用参数绑定(防注入)
- 时间用 chrono::Utc::now().naive_utc()

同时创建 src/routes/mod.rs 导出路由。

Claude Code生成的核心路由代码(src/routes/todos.rs):

use axum::{
    extract::{Path, State},
    routing::{get, post, delete, put},
    Json, Router,
};
use chrono::Utc;
use uuid::Uuid;

use crate::db::DbPool;
use crate::errors::AppError;
use crate::models::todo::*;
use crate::models::todo::{TodoListResponse, TodoResponse};

pub fn todo_router() -> Router<DbPool> {
    Router::new()
        .route("/", get(list_todos).post(create_todo))
        .route("/{id}", get(get_todo).put(update_todo).delete(delete_todo))
}

async fn list_todos(
    State(pool): State<DbPool>,
) -> Result<Json<TodoListResponse>, AppError> {
    let conn = pool.lock().map_err(|e| AppError::Internal(e.to_string()))?;
    let mut stmt = conn.prepare(
        "SELECT id, title, completed, created_at FROM todos ORDER BY created_at DESC"
    )?;

    let todos = stmt.query_map([], |row| {
        Ok(Todo {
            id: row.get(0)?,
            title: row.get(1)?,
            completed: row.get(2)?,
            created_at: row.get(3)?,
        })
    })?
    .collect::<Result<Vec<_>, _>>()?;

    Ok(Json(TodoListResponse { data: todos }))
}

async fn create_todo(
    State(pool): State<DbPool>,
    Json(body): Json<CreateTodoRequest>,
) -> Result<Json<TodoResponse>, AppError> {
    let id = Uuid::new_v4();
    let created_at = Utc::now().naive_utc().to_string();

    let conn = pool.lock().map_err(|e| AppError::Internal(e.to_string()))?;
    conn.execute(
        "INSERT INTO todos (id, title, completed, created_at) VALUES (?1, ?2, ?3, ?4)",
        (id.to_string(), &body.title, body.completed, &created_at),
    )?;

    let todo = Todo {
        id,
        title: body.title,
        completed: body.completed,
        created_at: Utc::now().naive_utc(),
    };

    Ok(Json(TodoResponse { data: todo }))
}

async fn get_todo(
    State(pool): State<DbPool>,
    Path(id): Path<Uuid>,
) -> Result<Json<TodoResponse>, AppError> {
    let conn = pool.lock().map_err(|e| AppError::Internal(e.to_string()))?;
    let todo = conn.query_row(
        "SELECT id, title, completed, created_at FROM todos WHERE id = ?1",
        [id.to_string()],
        |row| {
            Ok(Todo {
                id: row.get(0)?,
                title: row.get(1)?,
                completed: row.get(2)?,
                created_at: row.get(3)?,
            })
        },
    ).map_err(|_| AppError::NotFound(format!("Todo {} not found", id)))?;

    Ok(Json(TodoResponse { data: todo }))
}

async fn update_todo(
    State(pool): State<DbPool>,
    Path(id): Path<Uuid>,
    Json(body): Json<UpdateTodoRequest>,
) -> Result<Json<TodoResponse>, AppError> {
    let conn = pool.lock().map_err(|e| AppError::Internal(e.to_string()))?;

    // 先获取现有记录
    let existing = conn.query_row(
        "SELECT id, title, completed, created_at FROM todos WHERE id = ?1",
        [id.to_string()],
        |row| {
            Ok(Todo {
                id: row.get(0)?,
                title: row.get(1)?,
                completed: row.get(2)?,
                created_at: row.get(3)?,
            })
        },
    ).map_err(|_| AppError::NotFound(format!("Todo {} not found", id)))?;

    let updated = Todo {
        id: existing.id,
        title: body.title.unwrap_or(existing.title),
        completed: body.completed.unwrap_or(existing.completed),
        created_at: existing.created_at,
    };

    conn.execute(
        "UPDATE todos SET title = ?1, completed = ?2 WHERE id = ?3",
        (&updated.title, updated.completed, id.to_string()),
    )?;

    Ok(Json(TodoResponse { data: updated }))
}

async fn delete_todo(
    State(pool): State<DbPool>,
    Path(id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, AppError> {
    let conn = pool.lock().map_err(|e| AppError::Internal(e.to_string()))?;
    let rows = conn.execute(
        "DELETE FROM todos WHERE id = ?1",
        [id.to_string()],
    )?;

    if rows == 0 {
        return Err(AppError::NotFound(format!("Todo {} not found", id)));
    }

    Ok(Json(serde_json::json!({ "message": "Deleted successfully" })))
}

Claude Code帮Rust开发者避开的坑:

  1. SQL注入:Claude Code默认使用参数绑定(?1, ?2),而非字符串拼接
  2. 类型安全:路径参数直接解析为 Uuid 类型,无效UUID会自动返回400
  3. 错误传播:每个 ? 操作符自动将rusqlite错误转换为AppError

第五步:组装服务器入口

在Claude Code中:

创建 src/main.rs,组装整个服务:
1. 初始化数据库
2. 创建 axum Router,注册 todo_router
3. 添加 CORS 中间件(允许所有来源,开发用)
4. 添加 tracing 日志
5. 绑定 0.0.0.0:3000
6. 打印启动日志

确保所有模块在 src/lib.rs 中正确导出(如果有lib.rs的话),或者直接在 main.rs 中声明模块。

生成的 src/main.rs:

mod db;
mod errors;
mod models;
mod routes;

use axum::Router;
use tower_http::cors::{CorsLayer, Any};
use tower_http::trace::TraceLayer;
use tracing_subscriber::EnvFilter;

use db::init_db;
use routes::todos::todo_router;

#[tokio::main]
async fn main() {
    // 初始化日志
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env().add_directive("debug".parse().unwrap()))
        .init();

    // 初始化数据库
    let pool = init_db().expect("Failed to initialize database");
    tracing::info!("Database initialized");

    // 构建路由
    let app = Router::new()
        .nest("/api/todos", todo_router())
        .layer(CorsLayer::new()
            .allow_origin(Any)
            .allow_methods(Any)
            .allow_headers(Any))
        .layer(TraceLayer::new_for_http())
        .with_state(pool);

    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .expect("Failed to bind to port 3000");

    tracing::info!("🚀 Server running on http://localhost:3000");
    tracing::info!("📚 API endpoints:");
    tracing::info!("  GET    /api/todos");
    tracing::info!("  POST   /api/todos");
    tracing::info!("  GET    /api/todos/:id");
    tracing::info!("  PUT    /api/todos/:id");
    tracing::info!("  DELETE /api/todos/:id");

    axum::serve(listener, app)
        .await
        .expect("Server error");
}

确保 src/models/mod.rs 和 src/routes/mod.rs 正确导出:

src/models/mod.rs:

pub mod todo;

src/routes/mod.rs:

pub mod todos;

编译并运行:

cargo build
RUST_LOG=debug cargo run

预期输出:

2026-05-27T10:00:00.000Z  INFO  rust_todo_api: Database initialized
2026-05-27T10:00:00.000Z  INFO  rust_todo_api: 🚀 Server running on http://localhost:3000
2026-05-27T10:00:00.000Z  INFO  rust_todo_api: 📚 API endpoints:
2026-05-27T10:00:00.000Z  INFO  rust_todo_api:   GET    /api/todos
2026-05-27T10:00:00.000Z  INFO  rust_todo_api:   POST   /api/todos

第六步:测试API

打开另一个终端窗口,用curl测试:

# 创建一个Todo
curl -s -X POST http://localhost:3000/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "Learn Rust with Claude Code", "completed": false}' | jq

# 列出所有Todos
curl -s http://localhost:3000/api/todos | jq

# 获取单个Todo(替换UUID)
curl -s http://localhost:3000/api/todos/<UUID> | jq

# 更新Todo
curl -s -X PUT http://localhost:3000/api/todos/<UUID> \
  -H "Content-Type: application/json" \
  -d '{"completed": true}' | jq

# 删除Todo
curl -s -X DELETE http://localhost:3000/api/todos/<UUID> | jq

# 测试404
curl -s http://localhost:3000/api/todos/00000000-0000-0000-0000-000000000000 | jq

预期输出(创建Todo):

{
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "title": "Learn Rust with Claude Code",
    "completed": false,
    "created_at": "2026-05-27T10:00:00"
  }
}

第七步:编写集成测试

让Claude Code帮你写测试——这是它的强项。在Claude Code中:

创建 tests/api_tests.rs,为所有CRUD接口编写集成测试:
1. 使用 axum-test crate
2. 每个测试独立初始化数据库(用内存SQLite)
3. 测试用例:
   - test_create_todo:创建成功返回201
   - test_list_todos:返回数组
   - test_get_todo:返回单个todo
   - test_get_todo_not_found:返回404
   - test_update_todo:更新title和completed
   - test_delete_todo:删除成功
   - test_delete_nonexistent:返回404

需要在 src/lib.rs 中暴露 create_app() 函数以便测试复用。

为了让测试能独立创建app,需要重构一下。先让Claude Code创建 src/app.rs:

use axum::Router;
use tower_http::cors::{CorsLayer, Any};
use tower_http::trace::TraceLayer;

use crate::db::{init_db, DbPool};
use crate::routes::todos::todo_router;

pub fn create_app() -> Router {
    let pool = init_db().expect("Failed to initialize database");
    Router::new()
        .nest("/api/todos", todo_router())
        .layer(CorsLayer::new()
            .allow_origin(Any)
            .allow_methods(Any)
            .allow_headers(Any))
        .layer(TraceLayer::new_for_http())
        .with_state(pool)
}

更新 src/main.rs 使用它:

mod app;
mod db;
mod errors;
mod models;
mod routes;

use app::create_app;

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
                .add_directive("debug".parse().unwrap()))
        .init();

    let app = create_app();
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .expect("Failed to bind");

    tracing::info!("🚀 Server running on http://localhost:3000");
    axum::serve(listener, app).await.expect("Server error");
}

集成测试文件 tests/api_tests.rs:

use axum_test::TestServer;
use rust_todo_api::app::create_app;

fn setup() -> TestServer {
    TestServer::new(create_app()).unwrap()
}

#[tokio::test]
async fn test_create_todo() {
    let server = setup();

    let response = server
        .post("/api/todos")
        .json(&serde_json::json!({
            "title": "Test todo",
            "completed": false
        }))
        .await;

    assert!(response.status_code().is_success());
    let body: serde_json::Value = response.json();
    assert_eq!(body["data"]["title"], "Test todo");
    assert_eq!(body["data"]["completed"], false);
}

#[tokio::test]
async fn test_list_todos() {
    let server = setup();

    // 先创建两个
    server.post("/api/todos")
        .json(&serde_json::json!({"title": "Todo 1"}))
        .await;
    server.post("/api/todos")
        .json(&serde_json::json!({"title": "Todo 2"}))
        .await;

    let response = server.get("/api/todos").await;
    assert!(response.status_code().is_success());
    let body: serde_json::Value = response.json();
    assert_eq!(body["data"].as_array().unwrap().len(), 2);
}

#[tokio::test]
async fn test_get_todo() {
    let server = setup();

    let create_resp = server
        .post("/api/todos")
        .json(&serde_json::json!({"title": "Find me"}))
        .await;
    let created: serde_json::Value = create_resp.json();
    let id = created["data"]["id"].as_str().unwrap();

    let response = server.get(&format!("/api/todos/{}", id)).await;
    assert!(response.status_code().is_success());
    let body: serde_json::Value = response.json();
    assert_eq!(body["data"]["title"], "Find me");
}

#[tokio::test]
async fn test_get_todo_not_found() {
    let server = setup();

    let response = server
        .get("/api/todos/00000000-0000-0000-0000-000000000000")
        .await;

    assert_eq!(response.status_code(), 404);
}

#[tokio::test]
async fn test_update_todo() {
    let server = setup();

    let create_resp = server
        .post("/api/todos")
        .json(&serde_json::json!({"title": "Original"}))
        .await;
    let created: serde_json::Value = create_resp.json();
    let id = created["data"]["id"].as_str().unwrap();

    let response = server
        .put(&format!("/api/todos/{}", id))
        .json(&serde_json::json!({"title": "Updated", "completed": true}))
        .await;

    assert!(response.status_code().is_success());
    let body: serde_json::Value = response.json();
    assert_eq!(body["data"]["title"], "Updated");
    assert_eq!(body["data"]["completed"], true);
}

#[tokio::test]
async fn test_delete_todo() {
    let server = setup();

    let create_resp = server
        .post("/api/todos")
        .json(&serde_json::json!({"title": "Delete me"}))
        .await;
    let created: serde_json::Value = create_resp.json();
    let id = created["data"]["id"].as_str().unwrap();

    let response = server
        .delete(&format!("/api/todos/{}", id))
        .await;
    assert!(response.status_code().is_success());

    // 验证已删除
    let get_resp = server.get(&format!("/api/todos/{}", id)).await;
    assert_eq!(get_resp.status_code(), 404);
}

#[tokio::test]
async fn test_delete_nonexistent() {
    let server = setup();

    let response = server
        .delete("/api/todos/00000000-0000-0000-0000-000000000000")
        .await;

    assert_eq!(response.status_code(), 404);
}

运行测试:

cargo test

预期输出:

running 7 tests
test test_create_todo ... ok
test test_list_todos ... ok
test test_get_todo ... ok
test test_get_todo_not_found ... ok
test test_update_todo ... ok
test test_delete_todo ... ok
test test_delete_nonexistent ... ok

test result: ok. 7 passed; 0 failed; 0 ignored

实战案例:完整的Todo API项目

让我们把以上所有步骤串联起来,看完整的项目结构和工作流。

项目结构

rust-todo-api/
├── CLAUDE.md              # Claude Code配置
├── Cargo.toml             # 依赖声明
├── src/
│   ├── main.rs            # 入口(5行)
│   ├── app.rs             # 应用组装
│   ├── db.rs              # 数据库初始化
│   ├── errors.rs          # 错误类型
│   ├── models/
│   │   ├── mod.rs
│   │   └── todo.rs        # 数据模型
│   └── routes/
│       ├── mod.rs
│       └── todos.rs       # CRUD处理器
└── tests/
    └── api_tests.rs       # 集成测试(7个用例)

完整开发工作流

flowchart TD
    A[cargo init] --> B[编辑 Cargo.toml 添加依赖]
    B --> C[cargo check 验证编译]
    C --> D[编写 CLAUDE.md Rust规则]
    D --> E["Claude Code: 生成 models + errors"]
    E --> F["Claude Code: 生成 db.rs"]
    F --> G["Claude Code: 生成 routes/todos.rs"]
    G --> H["Claude Code: 生成 main.rs + app.rs"]
    H --> I[cargo build]
    I --> J{编译通过?}
    J -->|是| K[cargo run 启动服务]
    J -->|否| L["将错误粘贴给 Claude Code"]
    L --> M["Claude Code 修复错误"]
    M --> I
    K --> N[curl 测试 API]
    N --> O["Claude Code: 生成集成测试"]
    O --> P[cargo test]
    P --> Q{测试通过?}
    Q -->|是| R[项目完成 ✅]
    Q -->|否| S["将失败信息粘贴给 Claude Code"]
    S --> T["Claude Code 修复测试"]
    T --> P

    style A fill:#e1f5fe
    style D fill:#fff3e0
    style R fill:#e8f5e9
    style L fill:#fce4ec
    style S fill:#fce4ec

Claude Code × Rust 效率倍增场景

场景 传统方式 Claude Code方式 提速
学习所有权 读The Book 2天 对话式提问+即时代码示例 5x
修复borrow checker错误 搜索错误码+翻RFC 粘贴错误→自动修复 10x
添加新的API端点 手写handler+路由+模型 描述需求→一键生成 3x
编写测试 手动构造每个case 描述测试矩阵→批量生成 4x
异步代码重构 理解Pin/Future/Send Claude直接输出正确async代码 8x

常见Rust编译错误与Claude Code修复模式

你在开发过程中几乎一定会遇到这些错误。遇到时,直接把完整错误信息粘贴给Claude Code即可。

错误1:借用检查器(E0502)

// 错误代码
let conn = pool.lock().unwrap();
let todos = conn.query_row(...);  // 不可变借用
conn.execute(...);                // 错误!同时可变借用

Claude Code会自动重构为:

let conn = pool.lock().unwrap();
let todos = conn.query_row(...);  // 先读取
drop(conn);                       // 显式释放锁
let conn = pool.lock().unwrap();
conn.execute(...);                // 再写入

错误2:生命周期不匹配(E0597)

// 错误:临时值生命周期不够
let result = pool.lock().unwrap().query_row(...);

Claude Code会改为:

let conn = pool.lock().unwrap();  // 绑定到变量,延长生命周期
let result = conn.query_row(...);

错误3:async trait未实现Send(E0277)

当 Mutex 在 async 上下文中使用时,Claude Code知道要确保锁不跨 .await 点持有:

// 错误:MutexGuard跨await
async fn handler(State(pool): State<DbPool>) {
    let conn = pool.lock().unwrap();
    some_async_fn().await;  // conn仍然被持有!
}

// Claude Code修复:缩小锁的作用域
async fn handler(State(pool): State<DbPool>) {
    let result = {
        let conn = pool.lock().unwrap();
        conn.query_row(...)
    };  // 锁在此释放
    some_async_fn().await;
}

进阶:让Claude Code帮你做更多

用Claude Code添加分页功能

给Todo列表接口添加分页功能:
1. 添加 Query<PaginationParams> 参数,包含 page和 per_page,默认值分别为1和20
2. 响应中添加 total_count 和 page 信息
3. SQL使用 LIMIT 和 OFFSET
4. 写一个新的测试验证分页

用Claude Code迁移到持久化存储

把数据库从内存SQLite改为文件SQLite:
1. 修改 db.rs,Connection::open("data.db") 替代 open_in_memory()
2. 添加环境变量支持 DATABASE_URL
3. 考虑用 sqlx 替换 rusqlite 以获得编译时SQL检查

用Claude Code添加认证中间件

给API添加JWT认证:
1. 添加 axum-extra 的 CookieJar extract
2. 创建 auth middleware layer
3. 保护除 POST /api/todos(注册)之外的所有路由
4. 使用 jsonwebtoken crate

训练营适用性评估

评估维度 说明
适合训练营类型 后端开发训练营、Rust入门训练营、AI辅助编程工作坊
预计教学时长 3-4小时(含动手实践)
难度等级 中等偏上(Rust学习曲线 + API开发)
学员前置要求 熟悉HTTP/REST概念,有命令行经验,Rust零基础可接受
硬件要求 4GB内存以上,能运行cargo build
最佳分组方式 2人一组,一人负责提示词设计,一人负责代码审查
可扩展方向 添加WebSocket、迁移到PostgreSQL、部署到Shuttle/Render
评估方式 要求学员独立添加一个新资源(如/tags)并编写完整测试

延伸阅读


参考来源

  1. Claude Code 2.1.139: Agent View + /goal Command (May 2026) — Claude Code最新版本特性
  2. 7 CLAUDE.md Rules That Make AI Write Idiomatic Rust — Rust项目CLAUDE.md配置最佳实践
  3. Building a Full-Stack Rust Web App with Claude Opus 4.5 — Shuttle平台Rust全栈实战案例
  4. Claude Code Cheat Sheet 2026 — Claude Code命令速查手册
  5. Claude Code for Rust Developers Complete Guide — Rust开发者Claude Code使用指南

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入