<!-- title: Claude Code后端开发全链路实战 -->

# Claude Code后端开发全链路：从数据建模到API部署

## 概述

本教程面向有基础编程经验、希望用 Claude Code 构建完整后端服务的开发者。你将从零开始，用 Claude Code 完成数据建模、ORM Schema 定义、REST API 开发、认证鉴权、数据库迁移、测试编写和容器化部署的完整闭环。学完本教程，你将掌握用 Claude Code 独立交付生产级后端服务的端到端工作流。

**适用人群**：有 Node.js 或 Python 基础、了解 REST 概念的后端开发者、全栈开发者。

---

## 前置条件

| 工具 | 版本要求 | 安装方式 |
|------|---------|---------|
| Claude Code | 最新版 | `npm install -g @anthropic-ai/claude-code` |
| Node.js | ≥ 20.x | `nvm install 20` |
| PostgreSQL | ≥ 15 | `docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:16` |
| Docker | ≥ 24.x | 系统包管理器安装 |
| git | ≥ 2.40 | 系统自带 |

确保 `claude` 命令在终端可用，已完成 API Key 认证。

---

## 第一步：项目脚手架与CLAUDE.md配置

### 1.1 初始化项目

```bash
mkdir taskflow-api && cd taskflow-api
git init
claude
```

进入 Claude Code 交互界面后，首先创建项目骨架：

```
请用 Node.js + TypeScript + Fastify 搭建一个后端项目脚手架。
要求：
1. 使用 pnpm 作为包管理器
2. TypeScript 严格模式
3. 目录结构：src/routes, src/services, src/models, src/middleware, src/config
4. 包含 eslint + prettier 配置
5. 添加 dotenv 用于环境变量管理
6. 添加热重载（tsx watch）
```

Claude Code 会自动执行 `pnpm init`、安装依赖、创建配置文件和目录结构。

### 1.2 编写 CLAUDE.md

CLAUDE.md 是 Claude Code 理解项目上下文的核心文件。针对后端项目，需要写入架构约定：

```markdown
# TaskFlow API - Project Context

## Tech Stack
- Runtime: Node.js 20+ with TypeScript 5.x strict mode
- Framework: Fastify 5.x
- ORM: Drizzle ORM with PostgreSQL driver
- Auth: JWT (jsonwebtoken) + bcrypt
- Validation: Zod schemas
- Testing: Vitest + Supertest

## Architecture Rules
- Each feature module goes in src/routes/[feature]/ with its own route, service, and schema files
- Database models live in src/models/schema.ts (single source of truth)
- All API responses follow { success: boolean, data?: T, error?: string } format
- Use Drizzle query builder, never raw SQL unless explicitly approved
- All environment variables must be declared in src/config/env.ts with Zod validation
- Every route must have input validation (Zod) and error handling
- HTTP status codes: 200 (OK), 201 (Created), 400 (Validation), 401 (Unauthorized), 403 (Forbidden), 404 (Not Found), 500 (Server Error)

## Database Conventions
- Table names: snake_case, plural (e.g., user_tasks)
- Column names: snake_case
- All tables have: id (uuid), created_at, updated_at
- Use pgcrypto for UUID generation: gen_random_uuid()
- Soft delete: use deleted_at column, never hard delete

## Naming Conventions
- Files: kebab-case (user-task.service.ts)
- Types/Interfaces: PascalCase with suffix (UserTask, UserTaskService)
- Functions: camelCase
- Route files export a Fastify plugin function
```

将此内容保存为 `CLAUDE.md` 后，Claude Code 在后续所有操作中都会自动遵循这些约定。

### 1.3 预期结果

```
taskflow-api/
├── CLAUDE.md
├── package.json
├── tsconfig.json
├── .env.example
├── .eslintrc.json
├── .prettierrc
├── drizzle.config.ts
├── src/
│   ├── config/
│   │   └── env.ts
│   ├── routes/
│   ├── services/
│   ├── models/
│   │   └── schema.ts
│   ├── middleware/
│   └── index.ts
└── test/
```

### 1.4 常见问题

| 问题 | 解决方案 |
|------|---------|
| pnpm 未安装 | `npm install -g pnpm` |
| TypeScript 报错 ts-node 未找到 | 用 `tsx` 替代 ts-node，更稳定 |
| drizzle.config.ts 引用报错 | 确保 `drizzle-kit` 已安装为 devDependency |

---

## 第二步：数据建模与Drizzle ORM Schema

### 2.1 设计数据模型

在 Claude Code 中描述业务需求，让其自动生成 Schema：

```
请为 TaskFlow 项目设计完整的数据库 Schema。这是一个任务管理 API，
需要支持以下功能：
1. 用户注册/登录（email + password）
2. 项目管理（每个用户可以创建多个项目）
3. 任务管理（任务属于项目，有状态：todo/in_progress/done/cancelled）
4. 任务标签（多对多关系）
5. 任务评论

请按照 CLAUDE.md 中的数据库约定生成 Drizzle ORM Schema，
放在 src/models/schema.ts 中。
```

### 2.2 生成结果示例

Claude Code 会生成类似以下的 Drizzle Schema：

```typescript
// src/models/schema.ts
import { pgTable, uuid, varchar, text, timestamp, boolean, pgEnum }
  from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';

// 枚举
export const taskStatusEnum = pgEnum('task_status', [
  'todo', 'in_progress', 'done', 'cancelled'
]);

// 用户表
export const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  passwordHash: varchar('password_hash', { length: 255 }).notNull(),
  displayName: varchar('display_name', { length: 100 }),
  avatarUrl: text('avatar_url'),
  isActive: boolean('is_active').notNull().default(true),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
  deletedAt: timestamp('deleted_at'),
});

// 项目表
export const projects = pgTable('projects', {
  id: uuid('id').primaryKey().defaultRandom(),
  name: varchar('name', { length: 200 }).notNull(),
  description: text('description'),
  ownerId: uuid('owner_id').notNull().references(() => users.id),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
  deletedAt: timestamp('deleted_at'),
});

// 任务表
export const tasks = pgTable('tasks', {
  id: uuid('id').primaryKey().defaultRandom(),
  title: varchar('title', { length: 300 }).notNull(),
  description: text('description'),
  status: taskStatusEnum('status').notNull().default('todo'),
  priority: integer('priority').notNull().default(0),
  dueDate: timestamp('due_date'),
  projectId: uuid('project_id').notNull().references(() => projects.id),
  assigneeId: uuid('assignee_id').references(() => users.id),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
  deletedAt: timestamp('deleted_at'),
});

// 标签表
export const tags = pgTable('tags', {
  id: uuid('id').primaryKey().defaultRandom(),
  name: varchar('name', { length: 50 }).notNull(),
  color: varchar('color', { length: 7),  // hex color
  projectId: uuid('project_id').notNull().references(() => projects.id),
  createdAt: timestamp('created_at').notNull().defaultNow(),
});

// 任务-标签关联表（多对多）
export const taskTags = pgTable('task_tags', {
  taskId: uuid('task_id').notNull().references(() => tasks.id),
  tagId: uuid('tag_id').notNull().references(() => tags.id),
});

// 评论表
export const comments = pgTable('comments', {
  id: uuid('id').primaryKey().defaultRandom(),
  content: text('content').notNull(),
  taskId: uuid('task_id').notNull().references(() => tasks.id),
  authorId: uuid('author_id').notNull().references(() => users.id),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
  deletedAt: timestamp('deleted_at'),
});

// Relations
export const usersRelations = relations(users, ({ many }) => ({
  projects: many(projects),
  assignedTasks: many(tasks, { relationName: 'assignee' }),
  comments: many(comments),
}));

export const projectsRelations = relations(projects, ({ one, many }) => ({
  owner: one(users, { fields: [projects.ownerId], references: [users.id] }),
  tasks: many(tasks),
  tags: many(tags),
}));

export const tasksRelations = relations(tasks, ({ one, many }) => ({
  project: one(projects, { fields: [tasks.projectId], references: [projects.id] }),
  assignee: one(users, { fields: [tasks.assigneeId], references: [users.id], relationName: 'assignee' }),
  comments: many(comments),
  tags: many(taskTags),
}));
```

### 2.3 配置 Drizzle Kit

Claude Code 生成 `drizzle.config.ts`：

```typescript
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/models/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});
```

在 `package.json` 中添加脚本：

```json
{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:push": "drizzle-kit push",
    "db:studio": "drizzle-kit studio"
  }
}
```

### 2.4 执行迁移

```
请执行数据库迁移，先生成 migration 文件再应用。
同时检查生成的 SQL 是否符合预期。
```

Claude Code 会依次运行：

```bash
pnpm db:generate   # 生成 drizzle/0000_initial_migration.sql
pnpm db:migrate    # 应用到数据库
```

**预期输出**：

```
✅ Generated drizzle/0000_initial_migration.sql
✅ Applied migration 0000_initial_migration
✅ 6 tables created: users, projects, tasks, tags, task_tags, comments
```

### 2.5 常见问题

| 问题 | 原因 | 解决 |
|------|------|------|
| `defaultRandom()` 报错 | pgcrypto 扩展未启用 | 在 migration 前执行 `CREATE EXTENSION IF NOT EXISTS pgcrypto;` |
| 外键约束失败 | 迁移顺序问题 | 确保被引用的表先创建 |
| relationName 冲突 | 同一表多个外键指向同一表 | 使用 `relationName` 参数区分 |

---

## 第三步：数据库连接与服务层

### 3.1 创建数据库连接

```
请创建 src/config/database.ts，使用 Drizzle 的 node-postgres 连接池。
要求：
1. 连接池大小根据环境变量配置
2. 连接失败时有重试机制（最多3次）
3. 开发环境开启查询日志
4. 优雅关闭处理
```

Claude Code 生成：

```typescript
// src/config/database.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from '../models/schema.js';
import { env } from './env.js';

const pool = new Pool({
  connectionString: env.DATABASE_URL,
  max: env.DB_POOL_SIZE ?? 10,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 5000,
});

// 重试连接
let retries = 3;
while (retries > 0) {
  try {
    const client = await pool.connect();
    console.log('✅ Database connected');
    client.release();
    break;
  } catch (err) {
    retries--;
    console.error(`DB connection failed, retries left: ${retries}`, err);
    if (retries === 0) throw err;
    await new Promise(r => setTimeout(r, 2000));
  }
}

export const db = drizzle(pool, {
  schema,
  logger: env.NODE_ENV === 'development',
});

// 优雅关闭
process.on('SIGTERM', async () => {
  await pool.end();
  console.log('Database pool closed');
});
```

### 3.2 构建服务层

```
请为 tasks 模块创建完整的服务层 src/services/task.service.ts。
要求：
1. CRUD 全覆盖
2. 支持过滤（status, assigneeId）和分页
3. 使用 Drizzle query builder
4. 返回类型安全的结果
5. 软删除（设置 deleted_at）
```

Claude Code 生成的核心服务：

```typescript
// src/services/task.service.ts
import { eq, and, isNull, desc, sql, ilike } from 'drizzle-orm';
import { db } from '../config/database.js';
import { tasks, taskTags, tags, comments } from '../models/schema.js';
import type { InferSelectModel, InferInsertModel } from 'drizzle-orm';

export type Task = InferSelectModel<typeof tasks>;
export type NewTask = InferInsertModel<typeof tasks>;

interface ListTasksOptions {
  projectId: string;
  status?: 'todo' | 'in_progress' | 'done' | 'cancelled';
  assigneeId?: string;
  page?: number;
  limit?: number;
}

export async function listTasks(opts: ListTasksOptions) {
  const { projectId, status, assigneeId, page = 1, limit = 20 } = opts;
  const offset = (page - 1) * limit;

  const conditions = [
    eq(tasks.projectId, projectId),
    isNull(tasks.deletedAt),
  ];
  if (status) conditions.push(eq(tasks.status, status));
  if (assigneeId) conditions.push(eq(tasks.assigneeId, assigneeId));

  const [items, countResult] = await Promise.all([
    db.select().from(tasks)
      .where(and(...conditions))
      .orderBy(desc(tasks.createdAt))
      .limit(limit).offset(offset),
    db.select({ count: sql<number>`count(*)` })
      .from(tasks).where(and(...conditions)),
  ]);

  return {
    items,
    total: Number(countResult[0].count),
    page,
    totalPages: Math.ceil(Number(countResult[0].count) / limit),
  };
}

export async function createTask(data: NewTask) {
  const [task] = await db.insert(tasks).values(data).returning();
  return task;
}

export async function getTaskById(id: string) {
  const [task] = await db.select().from(tasks)
    .where(and(eq(tasks.id, id), isNull(tasks.deletedAt)));
  return task ?? null;
}

export async function updateTask(id: string, data: Partial<NewTask>) {
  const [task] = await db.update(tasks)
    .set({ ...data, updatedAt: new Date() })
    .where(and(eq(tasks.id, id), isNull(tasks.deletedAt)))
    .returning();
  return task ?? null;
}

export async function softDeleteTask(id: string) {
  const [task] = await db.update(tasks)
    .set({ deletedAt: new Date() })
    .where(eq(tasks.id, id))
    .returning();
  return task ?? null;
}
```

---

## 第四步：REST API路由与认证中间件

### 4.1 JWT认证中间件

```
请创建 JWT 认证中间件 src/middleware/auth.ts。
要求：
1. 使用 Fastify 的 onRequest 钩子
2. 从 Authorization: Bearer <token> 提取 token
3. 验证 JWT 并将用户信息挂载到 request.user
4. 未认证返回 401
5. 提供 optionalAuth 中间件（不强制但解析 token）
```

```typescript
// src/middleware/auth.ts
import { FastifyRequest, FastifyReply } from 'fastify';
import jwt from 'jsonwebtoken';
import { env } from '../config/env.js';

interface TokenPayload {
  userId: string;
  email: string;
}

declare module 'fastify' {
  interface FastifyRequest {
    user?: TokenPayload;
  }
}

export async function authMiddleware(
  request: FastifyRequest,
  reply: FastifyReply
) {
  const authHeader = request.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return reply.status(401).send({
      success: false,
      error: 'Missing or invalid authorization header',
    });
  }

  try {
    const token = authHeader.slice(7);
    const payload = jwt.verify(token, env.JWT_SECRET) as TokenPayload;
    request.user = payload;
  } catch {
    return reply.status(401).send({
      success: false,
      error: 'Invalid or expired token',
    });
  }
}

export async function optionalAuth(
  request: FastifyRequest,
  _reply: FastifyReply
) {
  const authHeader = request.headers.authorization;
  if (authHeader?.startsWith('Bearer ')) {
    try {
      const token = authHeader.slice(7);
      request.user = jwt.verify(token, env.JWT_SECRET) as TokenPayload;
    } catch {
      // Silently ignore invalid tokens
    }
  }
}
```

### 4.2 路由定义

```
请创建以下路由文件，遵循 CLAUDE.md 的约定：
1. src/routes/auth/auth.routes.ts - 注册/登录
2. src/routes/tasks/task.routes.ts - 任务CRUD
3. src/routes/projects/project.routes.ts - 项目管理
每个路由都要有 Zod 输入验证和完整的错误处理。
```

**认证路由示例**：

```typescript
// src/routes/auth/auth.routes.ts
import { FastifyInstance } from 'fastify';
import { z } from 'zod';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { db } from '../../config/database.js';
import { users } from '../../models/schema.js';
import { eq, isNull } from 'drizzle-orm';
import { env } from '../../config/env.js';

const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8).max(128),
  displayName: z.string().min(1).max(100).optional(),
});

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string(),
});

export async function authRoutes(app: FastifyInstance) {
  app.post('/auth/register', async (request, reply) => {
    const body = registerSchema.parse(request.body);

    // 检查邮箱是否已注册
    const existing = await db.select().from(users)
      .where(eq(users.email, body.email));
    if (existing.length > 0) {
      return reply.status(400).send({
        success: false,
        error: 'Email already registered',
      });
    }

    const passwordHash = await bcrypt.hash(body.password, 12);
    const [user] = await db.insert(users).values({
      email: body.email,
      passwordHash,
      displayName: body.displayName,
    }).returning();

    const token = jwt.sign(
      { userId: user.id, email: user.email },
      env.JWT_SECRET,
      { expiresIn: '7d' }
    );

    return reply.status(201).send({
      success: true,
      data: { token, user: { id: user.id, email: user.email, displayName: user.displayName } },
    });
  });

  app.post('/auth/login', async (request, reply) => {
    const body = loginSchema.parse(request.body);

    const [user] = await db.select().from(users)
      .where(and(eq(users.email, body.email), isNull(users.deletedAt)));

    if (!user || !(await bcrypt.compare(body.password, user.passwordHash))) {
      return reply.status(401).send({
        success: false,
        error: 'Invalid email or password',
      });
    }

    const token = jwt.sign(
      { userId: user.id, email: user.email },
      env.JWT_SECRET,
      { expiresIn: '7d' }
    );

    return reply.send({
      success: true,
      data: { token, user: { id: user.id, email: user.email, displayName: user.displayName } },
    });
  });
}
```

**任务路由示例**：

```typescript
// src/routes/tasks/task.routes.ts
import { FastifyInstance } from 'fastify';
import { z } from 'zod';
import { authMiddleware } from '../../middleware/auth.js';
import * as taskService from '../../services/task.service.js';

const createTaskSchema = z.object({
  title: z.string().min(1).max(300),
  description: z.string().optional(),
  status: z.enum(['todo', 'in_progress', 'done', 'cancelled']).default('todo'),
  priority: z.number().int().min(0).max(10).default(0),
  dueDate: z.string().datetime().optional(),
  assigneeId: z.string().uuid().optional(),
});

const listTasksQuery = z.object({
  status: z.enum(['todo', 'in_progress', 'done', 'cancelled']).optional(),
  assigneeId: z.string().uuid().optional(),
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
});

export async function taskRoutes(app: FastifyInstance) {
  // 所有任务路由需要认证
  app.addHook('onRequest', authMiddleware);

  // GET /tasks/:projectId - 列出项目下的任务
  app.get('/tasks/:projectId', async (request, reply) => {
    const { projectId } = request.params as { projectId: string };
    const query = listTasksQuery.parse(request.query);

    const result = await taskService.listTasks({ ...query, projectId });
    return reply.send({ success: true, data: result });
  });

  // POST /tasks - 创建任务
  app.post('/tasks', async (request, reply) => {
    const body = createTaskSchema.parse(request.body);
    const task = await taskService.createTask({
      ...body,
      dueDate: body.dueDate ? new Date(body.dueDate) : undefined,
    });
    return reply.status(201).send({ success: true, data: task });
  });

  // GET /task/:id - 获取单个任务
  app.get('/task/:id', async (request, reply) => {
    const { id } = request.params as { id: string };
    const task = await taskService.getTaskById(id);
    if (!task) {
      return reply.status(404).send({ success: false, error: 'Task not found' });
    }
    return reply.send({ success: true, data: task });
  });

  // PATCH /task/:id - 更新任务
  app.patch('/task/:id', async (request, reply) => {
    const { id } = request.params as { id: string };
    const body = createTaskSchema.partial().parse(request.body);
    const task = await taskService.updateTask(id, body);
    if (!task) {
      return reply.status(404).send({ success: false, error: 'Task not found' });
    }
    return reply.send({ success: true, data: task });
  });

  // DELETE /task/:id - 软删除任务
  app.delete('/task/:id', async (request, reply) => {
    const { id } = request.params as { id: string };
    const task = await taskService.softDeleteTask(id);
    if (!task) {
      return reply.status(404).send({ success: false, error: 'Task not found' });
    }
    return reply.send({ success: true, data: null });
  });
}
```

### 4.3 主入口组装路由

```typescript
// src/index.ts
import Fastify from 'fastify';
import cors from '@fastify/cors';
import { env } from './config/env.js';
import { db } from './config/database.js';
import { authRoutes } from './routes/auth/auth.routes.js';
import { taskRoutes } from './routes/tasks/task.routes.js';
import { projectRoutes } from './routes/projects/project.routes.js';

const app = Fastify({ logger: env.NODE_ENV === 'development' });

await app.register(cors, { origin: true });
await app.register(authRoutes);
await app.register(taskRoutes);
await app.register(projectRoutes);

// 健康检查
app.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() }));

try {
  await app.listen({ port: env.PORT, host: '0.0.0.0' });
  console.log(`🚀 Server running on port ${env.PORT}`);
} catch (err) {
  app.log.error(err);
  process.exit(1);
}
```

---

## 第五步：测试驱动开发

### 5.1 配置测试环境

```
请配置 Vitest 测试环境，创建 test/setup.ts 用于测试前：
1. 创建独立的测试数据库连接
2. 每个测试套件前清空所有表
3. 提供认证辅助函数（registerAndLogin）
```

Claude Code 生成：

```typescript
// test/setup.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from '../src/models/schema.js';
import { sql } from 'drizzle-orm';

const TEST_DB_URL = process.env.TEST_DATABASE_URL!;

let testDb: ReturnType<typeof drizzle>;
let testPool: Pool;

export async function setup() {
  testPool = new Pool({ connectionString: TEST_DB_URL, max: 5 });
  testDb = drizzle(testPool, { schema });

  // 清空所有表（按依赖顺序）
  const tables = ['task_tags', 'comments', 'tags', 'tasks', 'projects', 'users'];
  for (const table of tables) {
    await testDb.execute(sql`TRUNCATE TABLE ${sql.identifier(table)} CASCADE`);
  }

  return { db: testDb };
}

export async function teardown() {
  await testPool.end();
}

export function getTestDb() { return testDb; }
```

### 5.2 编写API集成测试

```
请为任务 API 编写完整的集成测试 test/routes/task.test.ts。
覆盖：
1. 创建任务（成功/验证失败）
2. 获取任务列表（分页/过滤）
3. 更新任务状态
4. 软删除任务
5. 未认证访问返回 401
```

Claude Code 生成的测试：

```typescript
// test/routes/task.test.ts
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { buildApp } from '../helpers/app.js';
import type { FastifyInstance } from 'fastify';

describe('Task API', () => {
  let app: FastifyInstance;
  let authToken: string;
  let projectId: string;

  beforeAll(async () => {
    app = await buildApp();
    // 注册并登录测试用户
    const registerRes = await app.inject({
      method: 'POST',
      url: '/auth/register',
      payload: {
        email: 'test@taskflow.dev',
        password: 'securepass123',
        displayName: 'Test User',
      },
    });
    authToken = registerRes.json().data.token;
  });

  beforeEach(async () => {
    // 每个测试前创建一个项目
    const res = await app.inject({
      method: 'POST',
      url: '/projects',
      headers: { authorization: `Bearer ${authToken}` },
      payload: { name: 'Test Project' },
    });
    projectId = res.json().data.id;
  });

  afterAll(async () => await app.close());

  it('should create a task', async () => {
    const res = await app.inject({
      method: 'POST',
      url: '/tasks',
      headers: { authorization: `Bearer ${authToken}` },
      payload: {
        title: 'Implement auth',
        description: 'Add JWT authentication',
        projectId,
      },
    });

    expect(res.statusCode).toBe(201);
    const body = res.json();
    expect(body.success).toBe(true);
    expect(body.data.title).toBe('Implement auth');
    expect(body.data.status).toBe('todo');
  });

  it('should reject task without title', async () => {
    const res = await app.inject({
      method: 'POST',
      url: '/tasks',
      headers: { authorization: `Bearer ${authToken}` },
      payload: { projectId },
    });

    expect(res.statusCode).toBe(400);
  });

  it('should list tasks with pagination', async () => {
    // 创建 3 个任务
    for (let i = 0; i < 3; i++) {
      await app.inject({
        method: 'POST',
        url: '/tasks',
        headers: { authorization: `Bearer ${authToken}` },
        payload: { title: `Task ${i}`, projectId },
      });
    }

    const res = await app.inject({
      method: 'GET',
      url: `/tasks/${projectId}?page=1&limit=2`,
      headers: { authorization: `Bearer ${authToken}` },
    });

    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.data.items).toHaveLength(2);
    expect(body.data.total).toBe(3);
    expect(body.data.totalPages).toBe(2);
  });

  it('should return 401 without auth', async () => {
    const res = await app.inject({
      method: 'GET',
      url: `/tasks/${projectId}`,
    });
    expect(res.statusCode).toBe(401);
  });

  it('should soft delete a task', async () => {
    const createRes = await app.inject({
      method: 'POST',
      url: '/tasks',
      headers: { authorization: `Bearer ${authToken}` },
      payload: { title: 'To delete', projectId },
    });
    const taskId = createRes.json().data.id;

    const deleteRes = await app.inject({
      method: 'DELETE',
      url: `/task/${taskId}`,
      headers: { authorization: `Bearer ${authToken}` },
    });
    expect(deleteRes.statusCode).toBe(200);

    // 验证 GET 404
    const getRes = await app.inject({
      method: 'GET',
      url: `/task/${taskId}`,
      headers: { authorization: `Bearer ${authToken}` },
    });
    expect(getRes.statusCode).toBe(404);
  });
});
```

运行测试：

```bash
pnpm test
```

**预期输出**：

```
 ✓ test/routes/task.test.ts (6 tests) 342ms
   ✓ Task API > should create a task
   ✓ Task API > should reject task without title
   ✓ Task API > should list tasks with pagination
   ✓ Task API > should return 401 without auth
   ✓ Task API > should soft delete a task
   ✓ Task API > should update task status

 Test Files  1 passed (1)
 Tests       6 passed (6)
 Duration    1.24s
```

---

## 第六步：数据库迁移管理

### 6.1 增量迁移工作流

当业务需求变化时（如新增字段），使用 Claude Code 安全地管理迁移：

```
请为 tasks 表新增以下字段：
1. estimated_hours (integer, nullable) - 预估工时
2. actual_hours (integer, nullable) - 实际工时
3. story_points (integer, nullable) - 故事点

先生成 migration 文件，检查 SQL，然后应用。
注意：这是一个已有生产数据的表，必须确保迁移是安全的。
```

Claude Code 会：

1. 更新 `src/models/schema.ts` 添加字段
2. 运行 `pnpm db:generate` 生成增量迁移
3. 展示生成的 SQL 供确认：

```sql
-- drizzle/0001_add_task_metrics.sql
ALTER TABLE "tasks" ADD COLUMN "estimated_hours" integer;
ALTER TABLE "tasks" ADD COLUMN "actual_hours" integer;
ALTER TABLE "tasks" ADD COLUMN "story_points" integer;
```

4. 确认后运行 `pnpm db:migrate`

### 6.2 迁移回滚策略

```
请为刚才的迁移创建一个回滚脚本 drizzle/0001_add_task_metrics_down.sql。
同时更新 task.service.ts 中的相关类型和查询。
```

```sql
-- drizzle/0001_add_task_metrics_down.sql
ALTER TABLE "tasks" DROP COLUMN IF EXISTS "estimated_hours";
ALTER TABLE "tasks" DROP COLUMN IF EXISTS "actual_hours";
ALTER TABLE "tasks" DROP COLUMN IF EXISTS "story_points";
```

---

## 第七步：容器化与生产部署

### 7.1 Dockerfile

```
请创建生产级 Dockerfile，要求：
1. 多阶段构建（build + production）
2. 基础镜像 node:20-alpine
3. 非 root 用户运行
4. 健康检查
5. 只包含运行时依赖，不包含 devDependencies
```

Claude Code 生成：

```dockerfile
# Dockerfile
FROM node:20-alpine AS builder

RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app

COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY tsconfig.json ./
COPY src/ ./src/
COPY drizzle/ ./drizzle/
RUN pnpm build

# ---

FROM node:20-alpine AS production

RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app

COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod

COPY --from=builder /app/dist ./dist
COPY drizzle/ ./drizzle/

RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -s /bin/sh -D appuser
USER appuser

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "dist/index.js"]
```

### 7.2 Docker Compose

```yaml
# docker-compose.yml
version: '3.9'

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/taskflow
      - JWT_SECRET=${JWT_SECRET}
      - PORT=3000
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: taskflow
      POSTGRES_PASSWORD: postgres
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  pgdata:
```

### 7.3 启动

```bash
docker compose up -d
docker compose logs -f api
```

**预期输出**：

```
api  | ✅ Database connected
api  | 🚀 Server running on port 3000
api  | {"level":30,"time":1747752000000,"msg":"Server listening at http://0.0.0.0:3000"}
```

---

## 实战案例：TaskFlow API 完整工作流

以下是用 Claude Code 从零构建 TaskFlow API 的完整工作流：

```mermaid
flowchart TD
    A[claude 启动 Claude Code] --> B[创建 CLAUDE.md 项目约定]
    B --> C[生成 Drizzle Schema 数据模型]
    C --> D[执行数据库迁移 db:generate + db:migrate]
    D --> E[构建服务层 CRUD + 查询]
    E --> F[创建认证中间件 JWT]
    F --> G[定义 REST 路由 Zod验证]
    G --> H[编写集成测试 Vitest]
    H --> I{测试通过?}
    I -->|否| J[修复错误]
    J --> H
    I -->|是| K[增量迁移 添加字段]
    K --> L[容器化 Dockerfile + Compose]
    L --> M[部署与验证]

    style A fill:#7c3aed,color:#fff
    style B fill:#7c3aed,color:#fff
    style C fill:#2563eb,color:#fff
    style D fill:#2563eb,color:#fff
    style E fill:#059669,color:#fff
    style F fill:#059669,color:#fff
    style G fill:#059669,color:#fff
    style H fill:#d97706,color:#fff
    style I fill:#d97706,color:#fff
    style J fill:#dc2626,color:#fff
    style K fill:#7c3aed,color:#fff
    style L fill:#2563eb,color:#fff
    style M fill:#059669,color:#fff
```

### 完整 Prompt 序列

以下是在 Claude Code 中按顺序执行的对话序列，每个 Prompt 建议独立发送，等上一步完成后再发下一步：

| 步骤 | Prompt 概要 | 预计耗时 |
|------|------------|---------|
| 1 | 搭建 Fastify + TypeScript 脚手架 | 30秒 |
| 2 | 编写 CLAUDE.md | 手动复制，10秒 |
| 3 | 设计数据库 Schema（6张表） | 45秒 |
| 4 | 配置 Drizzle Kit + 执行迁移 | 20秒 |
| 5 | 创建数据库连接池 | 20秒 |
| 6 | 构建用户/项目/任务服务层 | 60秒 |
| 7 | 实现认证中间件 + 注册登录 | 30秒 |
| 8 | 定义所有 REST 路由 + 验证 | 60秒 |
| 9 | 组装主入口 + 路由注册 | 15秒 |
| 10 | 配置测试环境 + 编写集成测试 | 60秒 |
| 11 | 运行测试 + 修复 | 30秒 |
| 12 | 增量迁移示例 | 20秒 |
| 13 | Dockerfile + Compose | 30秒 |

**总耗时约 7-8 分钟**（含 Claude Code 生成和执行时间），相比手动编写同等规模后端服务节省约 3-4 小时。

### Claude Code 效率优化技巧

在整个开发过程中，以下技巧能显著提升效率：

**1. 上下文感知 Prompt**

```
# 不要这样写（信息不足）
"写一个创建用户的 API"

# 要这样写（利用 CLAUDE.md 上下文）
"根据 CLAUDE.md 中的 Schema 约定，创建一个 POST /auth/register 路由，
使用已有的 users 表和 Zod 验证，密码用 bcrypt 加密。"
```

**2. 迭代修复而非重写**

当 Claude Code 生成的代码有问题时：

```
# 不要这样写
"重写整个任务服务"

# 要这样写
"src/services/task.service.ts 的 listTasks 函数在 status 为空时
返回了空结果。请修复条件构建逻辑，status 为 undefined 时不应添加到条件数组。"
```

**3. 利用 Plan Mode 做复杂设计**

对于多表关联或复杂业务逻辑，先进入 Plan Mode：

```
/plan 我需要给 TaskFlow 添加实时通知功能。
当任务状态变更时，通过 WebSocket 推送给项目的所有成员。
请分析需要修改哪些文件，新增哪些模块。
```

Claude Code 会给出详细的实施计划，确认后再逐步执行。

**4. 迁移前先检查**

```
请先展示将要执行的 migration SQL，我确认后再应用到数据库。
```

这避免了 Claude Code 直接执行破坏性的数据库变更。

---

## API验证：用curl测试完整流程

启动服务后，在终端运行以下命令验证整个链路：

```bash
# 1. 注册用户
curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"demo@taskflow.dev","password":"demo12345","displayName":"Demo User"}'

# 输出: {"success":true,"data":{"token":"eyJhbG...","user":{...}}}

# 2. 保存 token
TOKEN="eyJhbG..."  # 从上一步响应中复制

# 3. 创建项目
curl -X POST http://localhost:3000/projects \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"name":"My First Project","description":"A test project"}'

# 4. 创建任务
curl -X POST http://localhost:3000/tasks \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"title":"Setup CI/CD","projectId":"<project-id>","priority":3}'

# 5. 列出任务
curl http://localhost:3000/tasks/<project-id>?page=1&limit=10 \
  -H "Authorization: Bearer $TOKEN"

# 6. 更新任务状态
curl -X PATCH http://localhost:3000/task/<task-id> \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"status":"in_progress"}'

# 7. 健康检查
curl http://localhost:3000/health
# 输出: {"status":"ok","timestamp":"2026-05-20T12:00:00.000Z"}
```

---

## 架构全景图

```mermaid
graph TB
    subgraph Client["客户端"]
        WEB[Web Frontend]
        CLI[CLI Tool]
        MOBILE[Mobile App]
    end

    subgraph API["Fastify API Server"]
        AUTH_MW[Auth Middleware JWT]
        VALIDATOR[Zod Validator]
        ROUTER[Route Handler]
    end

    subgraph Services["Service Layer"]
        USER_SVC[User Service]
        PROJECT_SVC[Project Service]
        TASK_SVC[Task Service]
    end

    subgraph Data["Data Layer"]
        DRIZZLE[Drizzle ORM]
        POOL[Connection Pool]
    end

    subgraph Storage["PostgreSQL"]
        USERS_TBL[(users)]
        PROJECTS_TBL[(projects)]
        TASKS_TBL[(tasks)]
        TAGS_TBL[(tags / task_tags)]
        COMMENTS_TBL[(comments)]
    end

    WEB --> API
    CLI --> API
    MOBILE --> API
    AUTH_MW --> VALIDATOR --> ROUTER
    ROUTER --> USER_SVC
    ROUTER --> PROJECT_SVC
    ROUTER --> TASK_SVC
    USER_SVC --> DRIZZLE
    PROJECT_SVC --> DRIZZLE
    TASK_SVC --> DRIZZLE
    DRIZZLE --> POOL --> Storage
    POOL --> USERS_TBL
    POOL --> PROJECTS_TBL
    POOL --> TASKS_TBL
    POOL --> TAGS_TBL
    POOL --> COMMENTS_TBL

    style API fill:#7c3aed,color:#fff
    style Services fill:#059669,color:#fff
    style Data fill:#2563eb,color:#fff
    style Storage fill:#d97706,color:#fff
```

---

## 进阶扩展方向

完成本教程后，以下是用 Claude Code 继续扩展项目的方向：

| 方向 | Claude Code Prompt 示例 | 复杂度 |
|------|------------------------|--------|
| WebSocket 实时通知 | "根据 CLAUDE.md 架构，添加 @fastify/websocket 插件，实现任务状态变更时的实时推送" | 中 |
| GraphQL 接口 | "为现有 Schema 添加 Mercurius GraphQL 层，复用已有的 service 层" | 中 |
| 文件上传 | "添加 S3 文件上传功能，支持任务附件。使用 @aws-sdk/client-s3" | 中 |
| 全文搜索 | "为任务标题和描述添加 PostgreSQL 全文搜索（tsvector），支持中英文分词" | 高 |
| OpenAPI 文档 | "使用 @fastify/swagger 从路由定义自动生成 OpenAPI 3.0 文档" | 低 |
| 速率限制 | "添加 @fastify/rate-limit，按用户 ID 和 IP 进行请求限流" | 低 |

每个扩展只需在 Claude Code 中发送一句自然语言描述，即可自动完成代码生成、测试更新和迁移管理。

---

## 训练营适用性评估

| 维度 | 评估 |
|------|------|
| 适用训练营类型 | 全栈开发训练营、后端开发训练营、AI辅助开发训练营 |
| 学员基础要求 | JavaScript/TypeScript 基础、了解 REST 概念、会使用终端 |
| 预计教学时长 | 3-4 小时（含实操练习） |
| 实操占比 | 80%（跟随教程一步步操作） |
| 建议分组 | 2-3 人一组，每组完成独立功能模块 |
| 考核方式 | 要求学员用 Claude Code 独立完成一个新模块（如评论系统），并跑通测试 |
| 所需环境 | 每人一台有 Node.js + Docker 的开发机，安装好 Claude Code |

---

## 参考来源

1. [Claude Code for API Development: Build, Test, and Document REST APIs](https://scottmcauley.com/blog/claude-code-api-dev/) — Claude Code 构建 REST API 的完整流程
2. [How to Use Claude Code for Database Design and Migration](https://atticusli.com/blog/posts/how-to-use-claude-code-database-design-migration/) — 数据库设计与迁移实战指南
3. [Claude Code with Drizzle ORM: Schema, Migrations, Queries](https://claudify.tech/blog/claude-code-drizzle) — Drizzle ORM 与 Claude Code 的集成方法
4. [Beyond One-Shot Prompts: 5 Claude Code Workflow Patterns Explained](https://www.mindstudio.ai/blog/claude-code-agentic-workflow-patterns) — Anthropic 官方推荐的五种工作流模式
5. [Claude Code Full-Stack Configuration Guide](https://htdocs.dev/posts/claude-code-full-stack-configuration-guide/) — 全栈项目配置最佳实践

> [!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)
