本教程面向零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+
Rust项目的成功与否,很大程度上取决于Claude Code是否理解你的项目结构和约束。一份精心编写的CLAUDE.md能让Claude写出地道的Rust代码,而不是"C++风格的Rust"。
mkdir rust-todo-api && cd rust-todo-api
cargo init
编辑 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
这是整个教程最关键的一步。在项目根目录创建 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>>。
这是核心部分。在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, ?2),而非字符串拼接Uuid 类型,无效UUID会自动返回400? 操作符自动将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
打开另一个终端窗口,用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
让我们把以上所有步骤串联起来,看完整的项目结构和工作流。
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方式 | 提速 |
|---|---|---|---|
| 学习所有权 | 读The Book 2天 | 对话式提问+即时代码示例 | 5x |
| 修复borrow checker错误 | 搜索错误码+翻RFC | 粘贴错误→自动修复 | 10x |
| 添加新的API端点 | 手写handler+路由+模型 | 描述需求→一键生成 | 3x |
| 编写测试 | 手动构造每个case | 描述测试矩阵→批量生成 | 4x |
| 异步代码重构 | 理解Pin/Future/Send | Claude直接输出正确async代码 | 8x |
你在开发过程中几乎一定会遇到这些错误。遇到时,直接把完整错误信息粘贴给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;
}
给Todo列表接口添加分页功能:
1. 添加 Query<PaginationParams> 参数,包含 page和 per_page,默认值分别为1和20
2. 响应中添加 total_count 和 page 信息
3. SQL使用 LIMIT 和 OFFSET
4. 写一个新的测试验证分页
把数据库从内存SQLite改为文件SQLite:
1. 修改 db.rs,Connection::open("data.db") 替代 open_in_memory()
2. 添加环境变量支持 DATABASE_URL
3. 考虑用 sqlx 替换 rusqlite 以获得编译时SQL检查
给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)并编写完整测试 |
[!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 等场景全覆盖。
