Skip to content
Published at:

第 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 v6React 生态标准方案
状态管理Zustand轻量,比 Redux 更适合中型项目
后端框架Express + TypeScriptNode.js 生态标准,Claude 训练数据极丰富
ORMPrisma类型安全,自动生成迁移,与 TypeScript 深度集成
数据库PostgreSQL功能最完善的开源关系型数据库
认证JWT (jsonwebtoken) + bcrypt经典方案,实现简单可理解
测试(前端)Vitest + Testing LibraryVite 原生测试框架,速度快
测试(后端)Vitest + Supertest统一测试框架,端到端 HTTP 测试
容器化Docker + docker-compose标准化环境,一键启动
CI/CDGitHub 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 文档

这个结构遵循了几个原则:

  1. server/ 和 client/ 分治:前后端在独立的目录中,各自有自己的 package.jsontsconfig.json。根目录使用 concurrentlypnpm workspace 统一管理。
  2. server 内部使用三层架构routes → controllers → services。路由层定义 URL 和方法;控制器层处理请求/响应;服务层封装业务逻辑。每一层的职责单一,测试独立。
  3. Prisma schema 作为单一数据源:所有数据库表结构定义在 prisma/schema.prisma 中,通过 prisma migrate 自动生成 SQL 和 TypeScript 类型。
  4. 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:serverdev: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 的数据模型设计覆盖了任务管理的核心需求:

  • UserTask:一对多,级联删除
  • TaskCategory:多对一(可选),无级联(删除分类不影响任务)
  • TaskTag:多对多,通过 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.tsxNavbar.tsx
  • 自定义 Hook 以 use 开头:useAuth.tsuseTasks.ts
  • Zustand store 以 Store 后缀:useAuthStore.ts
  • API 函数按资源分文件:tasks.tsauth.ts
  • 样式优先使用 Tailwind 类,避免自定义 CSS(除非必要)
  • 表单状态管理优先使用 React 状态 + Zod 验证

Git 约定

  • 分支命名:feature/<name> / fix/<name> / chore/<name>
  • Commit 信息使用中文,格式:<类型>: <简述>
    • 类型:feat(新功能)/ fix(修复)/ chore(杂项)/ docs(文档)/ refactor(重构)/ test(测试)
    • 例:feat: 添加用户注册接口 / fix: 修复任务完成状态切换 Bug

数据库约定

  • 表名使用复数 snake_case:userstask_tags
  • 字段名使用 camelCase,通过 @map 映射为 snake_case
  • 主键使用 UUID 字符串
  • 外键格式:<关联表名单数>_id,例:user_idcategory_id
  • 时间戳字段:created_atupdated_at,使用 Prisma 自动管理

初稿生成完毕。请审查,我可以根据你的反馈修改。


### 37.3.3 审查并完善

Claude Code 生成的初稿已经很完整了。但在真实项目中,你需要以**项目维护者**的身份审查它,确保没有遗漏和错误。以下是这次审查中发现的问题和改进:

用户: 初稿整体不错。以下修改:

  1. 数据库部分补充 Prisma schema 中定义的模型关系(User → Task → Category/Tag),方便 Claude 理解数据模型
  2. "常用命令"中补充 npx prisma studio 的使用方法
  3. 代码约定中明确:前后端 TypeScript 的 target 不同(server ES2022, client ES2020),新增代码注意兼容性
  4. 补充:环境变量在 server/.env 和 client/.env 中,新增变量需要更新 .env.example
  5. 补充:前端 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 始终保持可部署状态,所有开发在特性分支上进行。

bash
# 每开始一个新功能,从 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 的协作节奏

  1. 对 Claude Code 描述当前分支要实现的功能
  2. Claude Code 在 Plan 模式下给出方案
  3. 审查方案,确认后开始实现
  4. 每完成一个独立的文件/模块,让 Claude 运行测试
  5. 整个功能完成后,发起 PR 并用 Claude Code 进行 Code Review
  6. 合并到 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 在这个项目中的行为。

在项目初始化阶段,我们配置以下内容:

json
{
  "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 项目配置一个快速切换方案:

json
{
  "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 配不通。

yaml
# .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 骨架做了两件事:

  1. test job:启动 PostgreSQL service 容器 → 安装依赖 → 运行迁移 → 执行所有测试
  2. 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 项目的启动。回顾一下我们做了什么:

  1. 项目规划:定义了 SmartTodo 的功能范围、技术栈选择和目录结构。这个规划不是"一次性决定后永不改变",而是为开发提供方向——后续实践中可以根据实际体验调整。

  2. 项目初始化:用 Claude Code 完成了完整的项目骨架搭建——目录结构、依赖安装、TypeScript/Vite/Tailwind/Prisma 的配置、Docker Compose 文件、入口文件、Git 初始化。整个过程通过一段紧凑的对话完成。

  3. 编写 CLAUDE.md:在写功能代码之前,先建立项目与 Claude Code 之间的"共享知识库"。让 Claude 生成初稿,你审查并完善。这是 AI 友好项目的基础设施。

  4. 建立工作流:定义了 Feature Branch + Plan Mode + TDD + Code Review 的开发节奏。配置了 .claude/settings.jsoncc-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、数据验证和完整的测试覆盖。准备好了吗?让我们继续。

进入下一章之前,确认三件事

  1. docker compose up -d 能启动数据库
  2. pnpm dev 能同时启动前后端
  3. curl http://localhost:3000/api/health 返回 {"status":"ok"}

如果以上都通过了,你的 SmartTodo 项目已经有了一个坚实的起点。