<!-- title: 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` |

验证安装：

```bash
# 确认工具链就绪
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初始化项目

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

### 1.2 添加核心依赖

编辑 `Cargo.toml`：

```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` 验证依赖能正确编译：

```bash
cargo check
```

**预期输出**：

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

### 1.3 创建Rust专属CLAUDE.md

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

```markdown
# 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：

```bash
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`**：

```rust
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`**：

```rust
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` 验证编译：

```bash
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`：

```rust
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`）：

```rust
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`：

```rust
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`**：

```rust
pub mod todo;
```

**`src/routes/mod.rs`**：

```rust
pub mod todos;
```

编译并运行：

```bash
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测试：

```bash
# 创建一个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）**：

```json
{
  "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`：

```rust
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` 使用它：

```rust
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`：

```rust
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);
}
```

运行测试：

```bash
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个用例）
```

### 完整开发工作流

```mermaid
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）**

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

Claude Code会自动重构为：

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

**错误2：生命周期不匹配（E0597）**

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

Claude Code会改为：

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

**错误3：async trait未实现Send（E0277）**

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

```rust
// 错误：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）并编写完整测试 |

---

## 延伸阅读

- [Anthropic官方文档：Using CLAUDE.md files](https://claude.com/blog/using-claude-md-files) — 项目级指令配置最佳实践
- [7 CLAUDE.md Rules That Make AI Write Idiomatic Rust](https://dev.to/olivia_craft/7-claudemd-rules-that-make-ai-write-idiomatic-rust-not-c-in-disguise-5h0e) — 让AI写出地道Rust代码的7条规则
- [Coding Rust With Claude Code and Codex](https://hackernoon.com/coding-rust-with-claude-code-and-codex) — 实战经验分享
- [Axum官方文档](https://github.com/tokio-rs/axum) — Axum框架完整参考
- [Rust Ownership & Lifetimes Skill](https://mcpmarket.com/tools/skills/rust-ownership-lifetimes-expert-1) — Claude Code专用Rust所有权Skill

---

## 参考来源

1. [Claude Code 2.1.139: Agent View + /goal Command (May 2026)](https://authorityaitools.com/blog/claude-code-2-1-139-agent-view-goal-command-may-2026) — Claude Code最新版本特性
2. [7 CLAUDE.md Rules That Make AI Write Idiomatic Rust](https://dev.to/olivia_craft/7-claudemd-rules-that-make-ai-write-idiomatic-rust-not-c-in-disguise-5h0e) — Rust项目CLAUDE.md配置最佳实践
3. [Building a Full-Stack Rust Web App with Claude Opus 4.5](https://www.shuttle.dev/blog/2025/11/26/build-rust-app-claude-opus-4.5) — Shuttle平台Rust全栈实战案例
4. [Claude Code Cheat Sheet 2026](https://computingforgeeks.com/claude-code-cheat-sheet/) — Claude Code命令速查手册
5. [Claude Code for Rust Developers Complete Guide](https://claude-skills-browser-ashen.vercel.app/claude-code-for-rust-developers/) — 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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
