第 37 章:项目启动 —— SmartTodo
三十六章走过来,我们从入门到精通到方法论,最后在前沿探索中展望了 Agentic Coding 的未来。但从这一章开始,我们不再"讲"——我们做。
接下来的四章,我们将用 Claude Code 从零构建一个完整的全栈应用:SmartTodo,一个智能任务管理应用。这个贯穿项目不是"玩具示例"——它将具备用户认证、数据持久化、前端交互、测试覆盖和 CI/CD 部署,是你在面试中敢拿出来展示的实战项目。更重要的是,整个过程由 Claude Code 深度参与:从项目初始化到代码生成,从测试编写到部署脚本,你将亲身体验 Agentic Coding 在真实项目中的全部威力。
第八篇目标:用四章的篇幅,与 Claude Code 协作完成一个可部署上线的全栈项目。第四十章结束时,你将拥有一个完整的 SmartTodo 仓库——前后端分离、测试覆盖、CI/CD 自动部署。
本章目标:理解 SmartTodo 的功能需求和技术选型,用 Claude Code 完成项目初始化和依赖安装,编写项目的 CLAUDE.md 文件,建立 TDD + Feature Branch 的开发工作流。
37.1 项目介绍
什么是 SmartTodo
SmartTodo 是一个智能任务管理应用。说它"智能",不是指 AI 帮你写任务(虽然开发过程确实是 AI 帮的),而是指功能设计上比简单的 Todo 列表更完整、更贴合日常使用场景。
核心功能:
- 用户注册与登录:JWT 认证,支持邮箱注册和登录
- 任务的完整 CRUD:创建、查看、编辑、删除任务
- 任务分类与标签:每个任务可以归属一个分类,打上多个标签
- 截止日期与提醒:设置截止日期,到期前显示提醒
- 任务统计面板:展示完成率、分类分布、近期趋势
这些功能覆盖了 Web 开发中的核心场景:认证(auth)、CRUD、关联查询、状态管理、数据可视化。做完这个项目,你就掌握了全栈开发的基本功。
技术栈选择
在真实项目中,技术选型是第一步。我们的选择原则是:成熟稳定、生态活跃、适合与 AI 协作。具体来说,选择"AI 训练数据覆盖广"的技术栈,能让 Claude Code 生成更准确的代码。
| 层级 | 技术 | 选型理由 |
|---|---|---|
| 前端框架 | React 18 + TypeScript | 生态最成熟,Claude 训练数据最丰富 |
| 构建工具 | Vite | 开发体验极佳,零配置启动 |
| 样式方案 | Tailwind CSS | 原子化 CSS,快速出界面 |
| 前端路由 | React Router v6 | React 生态标准方案 |
| 状态管理 | Zustand | 轻量,比 Redux 更适合中型项目 |
| 后端框架 | Express + TypeScript | Node.js 生态标准,Claude 训练数据极丰富 |
| ORM | Prisma | 类型安全,自动生成迁移,与 TypeScript 深度集成 |
| 数据库 | PostgreSQL | 功能最完善的开源关系型数据库 |
| 认证 | JWT (jsonwebtoken) + bcrypt | 经典方案,实现简单可理解 |
| 测试(前端) | Vitest + Testing Library | Vite 原生测试框架,速度快 |
| 测试(后端) | Vitest + Supertest | 统一测试框架,端到端 HTTP 测试 |
| 容器化 | Docker + docker-compose | 标准化环境,一键启动 |
| CI/CD | GitHub Actions | 与 GitHub 仓库深度集成 |
为什么不用 Next.js? Next.js 是优秀框架,但它的 SSR/RSC 引入的复杂性对初步体验"AI 协作全栈"的读者不够友好。选择 React + Express 的显式分离架构,能让你更清晰地看到前后端的边界,也更容易理解和调试。
项目结构设计
在动手之前,先看一下项目的完整目录结构。这个结构是 AI 友好项目设计(第 29 章)的实践:
smarttodo/
├── CLAUDE.md # 项目 AI 协作指南
├── package.json # 根 monorepo 脚本
├── docker-compose.yml # 本地开发环境
├── .github/
│ └── workflows/
│ └── ci.yml # CI/CD 流水线
├── .claude/
│ └── settings.json # Claude Code 项目级配置
├── server/ # Express 后端
│ ├── package.json
│ ├── tsconfig.json
│ ├── prisma/
│ │ └── schema.prisma # 数据库模型定义
│ ├── src/
│ │ ├── index.ts # 入口
│ │ ├── app.ts # Express app 配置
│ │ ├── config.ts # 环境变量配置
│ │ ├── routes/ # 路由层
│ │ ├── controllers/ # 控制器层
│ │ ├── services/ # 业务逻辑层
│ │ ├── middleware/ # 中间件
│ │ └── utils/ # 工具函数
│ └── tests/ # 后端测试
├── client/ # React 前端
│ ├── package.json
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── tailwind.config.js
│ ├── index.html
│ └── src/
│ ├── main.tsx # 入口
│ ├── App.tsx # 根组件
│ ├── pages/ # 页面组件
│ ├── components/ # 通用组件
│ ├── hooks/ # 自定义 Hooks
│ ├── stores/ # Zustand stores
│ ├── api/ # API 请求封装
│ └── types/ # TypeScript 类型
└── docs/ # 项目文档
└── api.md # API 文档这个结构遵循了几个原则:
- server/ 和 client/ 分治:前后端在独立的目录中,各自有自己的
package.json和tsconfig.json。根目录使用concurrently或pnpm workspace统一管理。 - server 内部使用三层架构:
routes → controllers → services。路由层定义 URL 和方法;控制器层处理请求/响应;服务层封装业务逻辑。每一层的职责单一,测试独立。 - Prisma schema 作为单一数据源:所有数据库表结构定义在
prisma/schema.prisma中,通过prisma migrate自动生成 SQL 和 TypeScript 类型。 - client 模块化:页面、组件、Hook、Store、API 调用各自归位,一个文件不应该同时做两件事。
小贴士:这个目录结构看起来"过度设计"了一个 Todo 应用。但如果你将来要扩展功能(评论系统、团队协作、文件附件),这个结构不需要重构——你只需要在对应的目录下添加新文件。
37.2 用 Claude Code 初始化项目
理论说完了,现在动手。你将看到一段完整的 Claude Code 对话实录——从零创建 SmartTodo 项目。
37.2.1 对话实录
下面是我的实际对话。括号中的 【说明】 是我的旁注,帮助你理解每一步的意图。
【对话开始】
用户:
帮我初始化一个全栈项目 SmartTodo,一个智能任务管理应用。
技术栈要求:
- 前端:React 18 + TypeScript + Vite + Tailwind CSS + React Router v6 + Zustand
- 后端:Express + TypeScript + Prisma ORM + PostgreSQL
- 测试:Vitest + Testing Library (前端) + Supertest (后端)
- 包管理:pnpm
- 项目结构:monorepo 风格,server/ 和 client/ 分开
请执行以下步骤:
1. 创建完整的项目目录结构
2. 初始化各子包的 package.json
3. 配置 TypeScript(server 和 client 各一套)
4. 安装所有依赖
5. 配置 Tailwind CSS
6. 配置 Vite
7. 初始化 Prisma(创建 schema.prisma 和 Docker Compose 文件)
8. 创建入口文件(server/src/index.ts、client/src/main.tsx)
9. 确认 dev 命令能正常启动
一步一步来,先确认我的需求再动手。【Claude 的回复】
Claude Code:
好的,让我先确认需求再动手。
**确认需求清单**:
| 项目 | 你的要求 | 我补充的建议 |
|------|---------|-------------|
| 前端框架 | React 18 + TS | Vite 5(最新稳定版) |
| 样式 | Tailwind CSS | Tailwind v3.x,最稳定的版本 |
| 路由 | React Router v6 | v6.28+ |
| 状态管理 | Zustand | v4.x |
| 后端 | Express + TS | Express 4.x,ts-node 或 tsx 运行 |
| ORM | Prisma | Prisma v5.x |
| 数据库 | PostgreSQL | PostgreSQL 16 |
| 测试 | Vitest | 统一使用 Vitest(前后端) |
| 包管理 | pnpm | ✅ |
| 结构 | server/client 分离 | Monorepo(根级脚本串联) |
有两点确认:
1. 开发时后端用 `tsx watch` 热重载,可以吗?还是想用 `nodemon`?
2. 数据库先用 Docker Compose 在本地跑一个 PostgreSQL,还是你已经有运行中的数据库?
确认后我就开始创建项目。
用户:
1. 用 tsx watch
2. 用 Docker Compose,把 docker-compose.yml 写好
开始吧。【Claude Code 开始执行】
# ===== 步骤 1:创建目录结构 =====
mkdir -p smarttodo/server/src/{routes,controllers,services,middleware,utils}
mkdir -p smarttodo/server/tests
mkdir -p smarttodo/server/prisma
mkdir -p smarttodo/client/src/{pages,components,hooks,stores,api,types}
mkdir -p smarttodo/client/public
mkdir -p smarttodo/.github/workflows
mkdir -p smarttodo/.claude
mkdir -p smarttodo/docs
【说明】Claude Code 通过 Bash 工具逐条执行这些命令。这里是一气呵成的。
# ===== 步骤 2:创建根 package.json =====
# 创建 smarttodo/package.json
{
"name": "smarttodo",
"private": true,
"scripts": {
"dev": "concurrently \"pnpm --filter server dev\" \"pnpm --filter client dev\"",
"dev:server": "pnpm --filter server dev",
"dev:client": "pnpm --filter client dev",
"build": "pnpm --filter server build && pnpm --filter client build",
"test": "pnpm --filter server test && pnpm --filter client test",
"db:migrate": "pnpm --filter server db:migrate",
"db:seed": "pnpm --filter server db:seed"
},
"devDependencies": {
"concurrently": "^8.2.2"
}
}
【说明】pnpm 的 --filter 让你在根目录就能操作子包。concurrently 同时启动前后端。
# ===== 步骤 3:创建 server 子包 =====
# smarttodo/server/package.json
{
"name": "@smarttodo/server",
"private": true,
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "vitest run",
"test:watch": "vitest",
"db:migrate": "prisma migrate dev",
"db:seed": "tsx prisma/seed.ts"
},
"dependencies": {
"express": "^4.21.0",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"dotenv": "^16.4.5",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"@prisma/client": "^5.18.0",
"zod": "^3.23.8"
},
"devDependencies": {
"typescript": "^5.5.4",
"@types/express": "^4.17.21",
"@types/cors": "^2.8.17",
"@types/jsonwebtoken": "^9.0.6",
"@types/bcryptjs": "^2.4.6",
"@types/node": "^22.5.0",
"tsx": "^4.19.0",
"prisma": "^5.18.0",
"vitest": "^2.0.5",
"supertest": "^6.3.4",
"@types/supertest": "^6.0.2"
}
}
# smarttodo/client/package.json
{
"name": "@smarttodo/client",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0",
"zustand": "^4.5.5",
"axios": "^1.7.7",
"date-fns": "^3.6.0"
},
"devDependencies": {
"typescript": "^5.5.4",
"vite": "^5.4.3",
"@vitejs/plugin-react": "^4.3.1",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"tailwindcss": "^3.4.10",
"postcss": "^8.4.41",
"autoprefixer": "^10.4.20",
"vitest": "^2.0.5",
"@testing-library/react": "^16.0.1",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/user-event": "^14.5.2",
"jsdom": "^25.0.0"
}
}
# ===== 步骤 4:创建 TypeScript 配置 =====
# smarttodo/server/tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}
# smarttodo/client/tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
# smarttodo/client/tsconfig.node.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
# ===== 步骤 5:Vite 配置 =====
# smarttodo/client/vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/tests/setup.ts',
},
})
# ===== 步骤 6:Tailwind CSS 配置 =====
# smarttodo/client/tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
# smarttodo/client/postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
# ===== 步骤 7:Prisma Schema =====
# smarttodo/server/prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
password String
name String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tasks Task[]
tags Tag[]
@@map("users")
}
model Task {
id String @id @default(uuid())
title String
description String?
completed Boolean @default(false)
dueDate DateTime? @map("due_date")
priority Priority @default(MEDIUM)
userId String @map("user_id")
categoryId String? @map("category_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
category Category? @relation(fields: [categoryId], references: [id])
tags TaskTag[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("tasks")
}
model Category {
id String @id @default(uuid())
name String @unique
color String @default("#6366f1")
tasks Task[]
@@map("categories")
}
model Tag {
id String @id @default(uuid())
name String
color String @default("#10b981")
userId String @map("user_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tasks TaskTag[]
@@map("tags")
}
model TaskTag {
taskId String @map("task_id")
tagId String @map("tag_id")
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([taskId, tagId])
@@map("task_tags")
}
enum Priority {
HIGH
MEDIUM
LOW
}
# ===== 步骤 8:创建 .env 文件 =====
# smarttodo/server/.env
DATABASE_URL="postgresql://smarttodo:smarttodo@localhost:5432/smarttodo?schema=public"
JWT_SECRET="change-me-in-production-use-a-long-random-string"
JWT_REFRESH_SECRET="change-me-in-production-use-another-long-random-string"
PORT=3000
# smarttodo/client/.env
VITE_API_BASE_URL=http://localhost:3000/api
# ===== 步骤 9:Docker Compose =====
# smarttodo/docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: smarttodo-db
restart: unless-stopped
environment:
POSTGRES_USER: smarttodo
POSTGRES_PASSWORD: smarttodo
POSTGRES_DB: smarttodo
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
# ===== 步骤 10:创建入口文件 =====
# smarttodo/server/src/index.ts
import { app } from './app'
import { config } from './config'
const port = config.port
app.listen(port, () => {
console.log(`🚀 SmartTodo API server running on http://localhost:${port}`)
})
# smarttodo/server/src/app.ts
import express from 'express'
import cors from 'cors'
import helmet from 'helmet'
export const app = express()
app.use(helmet())
app.use(cors())
app.use(express.json())
// Health check
app.get('/api/health', (_req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() })
})
# smarttodo/server/src/config.ts
import dotenv from 'dotenv'
dotenv.config()
export const config = {
port: parseInt(process.env.PORT || '3000', 10),
jwtSecret: process.env.JWT_SECRET || 'dev-secret',
jwtRefreshSecret: process.env.JWT_REFRESH_SECRET || 'dev-refresh-secret',
databaseUrl: process.env.DATABASE_URL || '',
}
# smarttodo/client/index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SmartTodo</title>
</head>
<body class="bg-gray-50 text-gray-900">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
# smarttodo/client/src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { App } from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
# smarttodo/client/src/App.tsx
export function App() {
return (
<div className="min-h-screen flex items-center justify-center">
<h1 className="text-4xl font-bold text-indigo-600">SmartTodo</h1>
</div>
)
}
# smarttodo/client/src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
# smarttodo/client/src/tests/setup.ts
import '@testing-library/jest-dom'
# ===== 步骤 11:安装依赖 =====
# 在 smarttodo 根目录执行:
pnpm install
【说明】安装过程输出略。这是耗时最长的一步,Claude Code 会等待完成。
# ===== 步骤 12:创建 .gitignore =====
# smarttodo/.gitignore
node_modules/
dist/
.env
*.log
.prisma/
!.env.example
# ===== 步骤 13:初始化 Git 仓库 =====
cd smarttodo
git init
git add .
git commit -m "chore: 初始化 SmartTodo 项目"
【对话结束】以上就是完整的项目初始化对话。整个过程流畅、紧凑,Claude Code 按步骤完成了所有创建和配置工作。
37.2.2 步骤拆解与讲评
上面的对话用了约 15 分钟完成整个项目骨架的搭建。如果纯手动做,一个熟练的开发者大概需要 1-2 小时(翻阅文档、复制粘贴配置、调试路径问题)。让我们拆解每一步中 Claude Code 的要点,以及你作为开发者应该检察的验证点。
步骤 1:目录创建。Claude Code 通过 Bash 工具执行的是一组 mkdir -p 命令。它知道需要一次性创建所有目录,而不是让你来回确认。你要验证的是:目录结构是否和你预期一致——如果某个目录位置不对,立刻纠正,不要"以后再说"。
步骤 2:根 package.json。Claude Code 理解 monorepo 模式,自动创建了 concurrently 依赖和串联脚本。注意它的脚本命名约定:dev 同时启动前后端,dev:server 和 dev:client 分别单独启动。这是常见的 monorepo 实践。
步骤 3:子包 package.json。Claude Code 选择的依赖版本全部是最新稳定版(写作时 2025-2026 的生态状况),并且自动处理了 @types/* 的对应关系——这是人类开发者经常遗漏的事。注意后端选择了 bcryptjs(纯 JS 实现)而非 bcrypt(需要原生编译),这是跨平台兼容的优先选择。
步骤 4-6:TypeScript / Vite / Tailwind 配置。这三步是配置的"脏活"。Claude Code 生成的三套 tsconfig 完全符合官方的最佳实践:server 使用 CommonJS 模块 + ES2022 target(因为 Node.js 原生支持);client 使用 ESNext 模块 + bundler 解析(因为 Vite 处理)。Vite 配置中已经包含了 /api 的代理转发——这说明 Claude Code 理解了前后端分离架构的网络拓扑。
步骤 7:Prisma Schema。这是整个项目中蕴含最多"设计决策"的文件。Claude Code 的数据模型设计覆盖了任务管理的核心需求:
User↔Task:一对多,级联删除Task↔Category:多对一(可选),无级联(删除分类不影响任务)Task↔Tag:多对多,通过TaskTag中间表Priority枚举:HIGH / MEDIUM / LOW- 时间戳自动管理:
createdAt/updatedAt
这里有一个设计细节值得注意:Tag 属于 User 而非全局。这意味着每个用户有自己的标签体系,而不是共享一套标签——这在多用户场景中更合理。Claude Code 做出了这个判断,没有把它设计成全局标签表。
步骤 8-9:环境配置与 Docker。.env 文件的注释明确提醒"change-me-in-production"——这是安全意识的基本体现。docker-compose.yml 使用 postgres:16-alpine(最小化镜像),正确设置了 volume 持久化数据。
步骤 10:入口文件。前端入口文件包含了 Tailwind 的 @apply 指令,以及一个简单的 "SmartTodo" 占位页面。后端的 app.ts 已经包含了 helmet(安全头)和 cors(跨域)——这些是生产环境必备的中间件。健康检查端点 /api/health 让你能立即验证服务器是否正常启动。
步骤 11-13:安装与提交。pnpm install 需要几分钟。安装完成后 Claude Code 通过 git status 检查了状态,确认没有错误后创建了初始 commit。
37.2.3 验证一切正常
# 终端 1:启动数据库
docker compose up -d
# 终端 2:初始化数据库(创建表)
cd server && npx prisma migrate dev --name init
# 终端 3:启动开发服务器
pnpm dev
# 浏览器访问:
# http://localhost:5173 → 看到 "SmartTodo" 大字
# http://localhost:3000/api/health → 看到 { "status": "ok", ... }如果这三个验证都通过,说明项目骨架搭建成功。如果某个步骤失败,把错误信息丢给 Claude Code,它会帮你分析并修复——这就是 Agentic Coding 的日常节奏。
37.3 编写 CLAUDE.md
项目初始化完成后的第一件事不是开始写功能代码,而是编写 CLAUDE.md。在第 12 章中我们详细讲解了 CLAUDE.md 的写法和原理,现在把它落地到这个真实项目中。
37.3.1 为什么 CLAUDE.md 必须第一个写
在 SmartTodo 的开发过程中,你可能会和 Claude Code 进行几十甚至上百轮对话。如果每一轮对话你都要向 Claude 解释"我们的项目结构是怎样的""我们用什么技术栈""我们的代码约定是什么",那每一轮都是在浪费时间。更糟的是,不同轮次的 Claude 对项目的理解可能不一致,导致代码风格不统一。
CLAUDE.md 解决的就是这个问题:让 Claude Code 在每一轮对话开始时,就拥有对项目的完整理解。它不是"锦上添花的文档",而是与 AI 高效协作的基础设施。
具体到这个项目:
- 项目分为
server/和client/两个独立子包,共享一个 git 仓库 - 前后端有各自独立的 TypeScript 配置和构建流程
- 数据模型定义了 Entity 之间的关系(级联删除、多对多关联)
- 代码有命名约定和分层架构
- 开发和部署有固定的命令序列
如果不写 CLAUDE.md,每一轮对话你都需要解释这些。写了,Claude Code 自己读取,你只需要说"给 Task 加一个搜索 API"——它就知道该在 server/src/routes/ 下创建路由,在 server/src/services/ 下写业务逻辑,同时更新前端 API 调用和类型定义。
37.3.2 让 Claude Code 生成初稿
最省力的方式不是自己手写 CLAUDE.md,而是让 Claude Code 读一遍项目,然后生成初稿。以下是对话记录:
用户:
请阅读整个 SmartTodo 项目的目录结构和关键配置文件,生成一份 CLAUDE.md 初稿。
CLAUDE.md 应该包含:
- 项目简介
- 技术栈
- 目录结构说明
- 常用命令
- 代码约定
写好了存到 CLAUDE.md,我来审查。Claude Code:
我已阅读了所有项目文件。下面是生成的 CLAUDE.md 初稿,存为 CLAUDE.md。
【说明】它生成了初稿——通常质量已经很不错了。你审查完后提出修改意见。
---
# SmartTodo - CLAUDE.md
## 项目简介
SmartTodo 是一个智能任务管理全栈应用。前端 React 18 + TypeScript + Vite + Tailwind CSS,
后端 Express + TypeScript + Prisma + PostgreSQL。前后端分离,monorepo 结构。
## 技术栈
| 层级 | 技术 |
|------|------|
| 前端框架 | React 18 + TypeScript |
| 构建工具 | Vite 5 |
| 样式 | Tailwind CSS 3 |
| 路由 | React Router v6 |
| 状态管理 | Zustand |
| HTTP 客户端 | axios |
| 日期处理 | date-fns |
| 后端框架 | Express 4 + TypeScript |
| 运行时 | tsx |
| ORM | Prisma 5 |
| 数据库 | PostgreSQL 16 |
| 认证 | JWT + bcryptjs |
| 验证 | Zod |
| 测试 | Vitest + Testing Library + Supertest |
| 包管理 | pnpm |
## 目录结构smarttodo/ ├── CLAUDE.md ├── package.json # 根 monorepo 脚本 ├── docker-compose.yml # PostgreSQL 本地开发环境 ├── .claude/ │ └── settings.json ├── .github/workflows/ │ └── ci.yml ├── server/ # Express 后端 │ ├── package.json │ ├── tsconfig.json │ ├── .env │ ├── prisma/ │ │ └── schema.prisma # 数据库模型(唯一数据源) │ ├── src/ │ │ ├── index.ts # 入口:启动 Express 服务器 │ │ ├── app.ts # Express app 配置(中间件注册) │ │ ├── config.ts # 环境变量集中管理 │ │ ├── routes/ # 路由层:URL + HTTP 方法 → 控制器 │ │ ├── controllers/ # 控制器层:处理请求/响应 │ │ ├── services/ # 服务层:业务逻辑 │ │ ├── middleware/ # 中间件(auth、validation 等) │ │ └── utils/ # 工具函数 │ └── tests/ └── client/ # React 前端 ├── package.json ├── tsconfig.json ├── vite.config.ts ├── tailwind.config.js ├── index.html └── src/ ├── main.tsx # 入口 ├── App.tsx # 根组件(路由配置) ├── index.css # Tailwind 指令 ├── pages/ # 页面组件(每个路由对应一个页面) ├── components/ # 通用组件(可跨页面复用) ├── hooks/ # 自定义 Hooks ├── stores/ # Zustand stores ├── api/ # axios 实例 + API 调用函数 └── types/ # 通用 TypeScript 类型
## 常用命令
```bash
# 启动开发环境(需要先 docker compose up -d 启动数据库)
pnpm dev # 同时启动前后端
# 单独启动
pnpm dev:server # 后端 http://localhost:3000
pnpm dev:client # 前端 http://localhost:5173
# 数据库
docker compose up -d # 启动 PostgreSQL
docker compose down # 停止 PostgreSQL
pnpm db:migrate # 运行 Prisma 数据库迁移
pnpm db:seed # 填充测试数据
prisma studio # 打开 Prisma 数据库浏览器(在 server/ 目录下执行)
# 测试
pnpm test # 运行所有测试
pnpm --filter server test:watch # 后端测试监视模式
pnpm --filter client test:watch # 前端测试监视模式
# 构建与部署
pnpm build # 构建前后端代码约定
总体原则
- 所有新功能优先编写测试(TDD)
- 后端遵循 routes → controllers → services 三层架构
- 前端每个页面独立一个文件,提取可复用的 components
- TypeScript strict 模式,不要使用
any - 使用 Zod 进行输入验证(后端),不信任客户端传入的数据
后端约定
- 路由文件:
<resource>.routes.ts,定义路径和 HTTP 方法 - 控制器文件:
<resource>.controller.ts,处理 req/res - 服务文件:
<resource>.service.ts,业务逻辑 - 中间件文件:
<feature>.middleware.ts - API 路径前缀统一
/api/ - 错误响应格式:
{ error: string, details?: any } - 成功响应格式:
{ data: any }或直接返回对象
前端约定
- 页面组件放在
pages/,文件名用 PascalCase:LoginPage.tsx - 通用组件放在
components/:TaskCard.tsx、Navbar.tsx - 自定义 Hook 以
use开头:useAuth.ts、useTasks.ts - Zustand store 以
Store后缀:useAuthStore.ts - API 函数按资源分文件:
tasks.ts、auth.ts - 样式优先使用 Tailwind 类,避免自定义 CSS(除非必要)
- 表单状态管理优先使用 React 状态 + Zod 验证
Git 约定
- 分支命名:
feature/<name>/fix/<name>/chore/<name> - Commit 信息使用中文,格式:
<类型>: <简述>- 类型:feat(新功能)/ fix(修复)/ chore(杂项)/ docs(文档)/ refactor(重构)/ test(测试)
- 例:
feat: 添加用户注册接口/fix: 修复任务完成状态切换 Bug
数据库约定
- 表名使用复数 snake_case:
users、task_tags - 字段名使用 camelCase,通过 @map 映射为 snake_case
- 主键使用 UUID 字符串
- 外键格式:
<关联表名单数>_id,例:user_id、category_id - 时间戳字段:
created_at、updated_at,使用 Prisma 自动管理
初稿生成完毕。请审查,我可以根据你的反馈修改。
### 37.3.3 审查并完善
Claude Code 生成的初稿已经很完整了。但在真实项目中,你需要以**项目维护者**的身份审查它,确保没有遗漏和错误。以下是这次审查中发现的问题和改进:用户: 初稿整体不错。以下修改:
- 数据库部分补充 Prisma schema 中定义的模型关系(User → Task → Category/Tag),方便 Claude 理解数据模型
- "常用命令"中补充 npx prisma studio 的使用方法
- 代码约定中明确:前后端 TypeScript 的 target 不同(server ES2022, client ES2020),新增代码注意兼容性
- 补充:环境变量在 server/.env 和 client/.env 中,新增变量需要更新 .env.example
- 补充:前端 Vite 已配置 /api 代理到 localhost:3000,开发时前端的 API 请求 base URL 留空即可
Claude Code 会根据这些反馈更新 CLAUDE.md。更新完成后,把 CLAUDE.md 提交到 Git:
```bash
git add CLAUDE.md
git commit -m "docs: 添加项目 CLAUDE.md,定义技术栈、目录结构、命令和代码约定"现在,无论是一小时后、一天后还是一周后,当你重新打开这个项目与 Claude Code 对话时,它都会以"理解项目全貌"的状态开始工作。
关键洞察:CLAUDE.md 的价值在项目初期不明显(因为你还记得所有细节),但当你一个月后回来加功能、或者邀请另一个开发者加入项目时,它的价值会立刻显现。在项目第一天花 20 分钟写 CLAUDE.md,省下的是后续几十轮对话反复解释的时间。
37.4 建立开发工作流
项目骨架搭好了,CLAUDE.md 写好了。在开始写功能代码之前,最后一步是建立一套可持续的开发工作流。工作流的意义在于:它不是限制你的创造力,而是让你不用每次都想"下一步该做什么"——把认知资源省下来,用到真正需要思考的事情上。
37.4.1 Feature Branch 工作流
SmartTodo 使用 Feature Branch 工作流。主干 main 始终保持可部署状态,所有开发在特性分支上进行。
# 每开始一个新功能,从 main 切出分支
git checkout -b feature/user-auth # 用户认证
git checkout -b feature/task-crud # 任务 CRUD
git checkout -b feature/task-stats # 统计面板
git checkout -b fix/login-error # Bug 修复分支命名遵循 CLAUDE.md 中约定的规范。每完成一个功能,合并回 main 并删除分支。
与 Claude Code 的协作节奏:
- 对 Claude Code 描述当前分支要实现的功能
- Claude Code 在 Plan 模式下给出方案
- 审查方案,确认后开始实现
- 每完成一个独立的文件/模块,让 Claude 运行测试
- 整个功能完成后,发起 PR 并用 Claude Code 进行 Code Review
- 合并到 main,切换到下一个分支
37.4.2 Plan Mode 先行
在第 21 章中我们强调过:拿到需求后先让 Claude 做分析,不要直接开始写代码。在 SmartTodo 项目中,这个原则要贯彻到每一次功能开发中。
每次开发新功能的流程:
【Step 1】在 Claude Code 中切换为 Plan 模式
↓
【Step 2】描述需求 → Claude 输出设计方案
↓
【Step 3】审查方案 → 提出修改意见 → Claude 更新方案
↓
【Step 4】方案确认 → 切换到实现阶段
↓
【Step 5】"按照刚才确认的方案,开始实现,一次一个文件"
↓
【Step 6】每完成一个文件 → 查看变更 → 运行测试 → 确认这里有一个容易被忽视的细节:每次只实现一个文件。不要对 Claude 说"把整个功能写完",因为:
- 单个文件的变更容易审查(你一眼就能看出问题)
- 万一方向跑偏,浪费的时间少
- 每完成一个文件就运行测试,问题能早发现
37.4.3 测试驱动开发
在 SmartTodo 中,我们采用 TDD(测试驱动开发)循环:
写测试(红)→ 写实现(绿)→ 重构(保持绿)在与 Claude Code 协作时,TDD 的循环略有不同——你不需要让 Claude 每一步都给出完美答案,而是先让它写测试,你审查测试用例是否正确;然后再让它写实现;最后让它重构。
用户:
为 TaskService 的 createTask 方法写测试。先只写测试,不写实现。
【Claude 写出测试用例】
用户:
测试用例看起来不错,但缺少一个场景:当 categoryId 不存在时应该返回错误。
请补充这个测试用例。
【Claude 补充】
用户:
现在测试覆盖完整了。请实现 createTask,让测试通过。这种"测试先行"的方式有双重好处:既保证了代码质量,又让你通过审查测试用例来验证你对该功能的理解是否完整——如果 Claude 写的测试遗漏了某个场景,说明你的需求描述可能也不够清晰。
37.4.4 配置 .claude/settings.json
为 SmartTodo 项目创建 Claude Code 的项目级配置,可以定制 Claude 在这个项目中的行为。
在项目初始化阶段,我们配置以下内容:
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(pnpm *)",
"Bash(npx *)",
"Bash(docker *)",
"Bash(git *)"
],
"deny": []
},
"model": "deepseek-v4-flash"
}配置说明:
permissions.allow:预先授权常用开发命令(pnpm、docker、git)。这样每次执行这些命令时不会弹出权限确认框——既安全(这些命令只在项目内操作),又高效(不打断开发流)。model:项目默认使用 DeepSeek V4 Flash(日常开发的主力模型)。遇到复杂任务时可以通过cc-switch切换到 Opus 或其他更强模型。
动态切换:记住,这不是一成不变的。日常 CRUD 用 Flash 即可(快且便宜),遇到复杂的业务逻辑设计或 Bug 排查时切到 Opus 或 Sonnet。Settings 中的
model只是一个默认值。
37.4.5 配置 cc-switch
在第 9 章中我们介绍了 cc-switch(模型切换工具)。为 SmartTodo 项目配置一个快速切换方案:
{
"cc-switch": {
"daily": {
"model": "deepseek-v4-flash",
"note": "日常开发:CRUD、测试、简单重构"
},
"complex": {
"model": "claude-opus-4-20250514",
"thinking": true,
"note": "复杂任务:架构设计、疑难 Bug、大范围重构"
},
"review": {
"model": "claude-sonnet-4-20250514",
"note": "Code Review、代码分析"
}
}
}这个配置的意义:
- 大部分时间用 Flash——响应快,能覆盖 80% 的开发任务
- 遇到复杂问题切到 Opus(开启 thinking mode)——推理能力强,能处理多层次的逻辑问题
- Code Review 用 Sonnet——分析能力强且成本适中
实际使用中,你可能会发现一个规律:写代码用 Flash,想方案用 Opus,审代码用 Sonnet。这不是教条,只是经验。你的实际体验才是最终的判断标准。
37.4.6 CI/CD 骨架
在开发真正开始之前,先把 CI/CD 的骨架搭好。这遵循了"尽早集成"的原则——你不会希望在项目快完成的时候才发现 CI 配不通。
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: smarttodo
POSTGRES_PASSWORD: smarttodo
POSTGRES_DB: smarttodo_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Run database migration
working-directory: server
run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://smarttodo:smarttodo@localhost:5432/smarttodo_test
- name: Run tests
run: pnpm test
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Type check
run: |
pnpm --filter server exec tsc --noEmit
pnpm --filter client exec tsc --noEmit这个 CI 骨架做了两件事:
- test job:启动 PostgreSQL service 容器 → 安装依赖 → 运行迁移 → 执行所有测试
- lint job:运行前后端的 TypeScript 类型检查
每次 push 到 main 或创建 PR 时自动触发。虽然现在还没有任何业务测试(测试文件都是空的),但 CI 管道本身是通的——以后每写完一个功能,对应的测试就会在这条管道中运行。
37.4.7 工作流小结
把上述工作流串联起来,SmartTodo 的日常开发节奏是:
早上打开项目
↓
git checkout -b feature/xxx
↓
docker compose up -d # 启动数据库
pnpm dev # 启动开发服务器
↓
打开 Claude Code → Plan 模式 → 描述需求
↓
审查方案 → 确认
↓
写测试(让 Claude 写,你审查)
↓
写实现(让 Claude 写,一个文件一个文件来)
↓
运行测试 → 确保通过
↓
git commit → git push → 创建 PR
↓
Claude Code 预审 PR → 你最终 Review → 合并这个节奏在接下来的三章中将反复使用。它不是理论,而是你在第 38、39、40 章中会实际执行的流程。
37.5 本章小结
这一章,我们完成了 SmartTodo 项目的启动。回顾一下我们做了什么:
项目规划:定义了 SmartTodo 的功能范围、技术栈选择和目录结构。这个规划不是"一次性决定后永不改变",而是为开发提供方向——后续实践中可以根据实际体验调整。
项目初始化:用 Claude Code 完成了完整的项目骨架搭建——目录结构、依赖安装、TypeScript/Vite/Tailwind/Prisma 的配置、Docker Compose 文件、入口文件、Git 初始化。整个过程通过一段紧凑的对话完成。
编写 CLAUDE.md:在写功能代码之前,先建立项目与 Claude Code 之间的"共享知识库"。让 Claude 生成初稿,你审查并完善。这是 AI 友好项目的基础设施。
建立工作流:定义了 Feature Branch + Plan Mode + TDD + Code Review 的开发节奏。配置了
.claude/settings.json和cc-switch,搭好了 CI/CD 骨架。
此刻,SmartTodo 项目有以下文件:
smarttodo/
├── CLAUDE.md ✅ 项目 AI 协作指南
├── package.json ✅ 根 monorepo 脚本
├── pnpm-lock.yaml ✅ 锁定依赖版本
├── docker-compose.yml ✅ PostgreSQL 本地环境
├── .gitignore ✅
├── .github/workflows/ci.yml ✅ CI 骨架
├── .claude/settings.json ✅ 项目级配置
├── server/ ✅ Express 后端
│ ├── package.json
│ ├── tsconfig.json
│ ├── .env
│ ├── prisma/schema.prisma
│ └── src/
│ ├── index.ts
│ ├── app.ts
│ └── config.ts
└── client/ ✅ React 前端
├── package.json
├── tsconfig.json
├── vite.config.ts
├── tailwind.config.js
├── index.html
└── src/
├── main.tsx
├── App.tsx
└── index.css还没有写的(后面三章的活儿):路由、控制器、服务、中间件、前端页面、组件、测试用例、部署配置。
在下一章,我们将从"项目骨架"进入"血肉填充"——从后端开始,用 Claude Code 实现用户认证、任务 CRUD、数据验证和完整的测试覆盖。准备好了吗?让我们继续。
进入下一章之前,确认三件事:
docker compose up -d能启动数据库pnpm dev能同时启动前后端curl http://localhost:3000/api/health返回{"status":"ok"}如果以上都通过了,你的 SmartTodo 项目已经有了一个坚实的起点。