首页 › tutorials › 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 初始化项目

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 理解项目上下文的核心文件。针对后端项目,需要写入架构约定:

# 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:

// 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:

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 中添加脚本:

{
  "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 会依次运行:

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 生成:

// 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 生成的核心服务:

// 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)
// 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 输入验证和完整的错误处理。

认证路由示例:

// 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 } },
    });
  });
}

任务路由示例:

// 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 主入口组装路由

// 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 生成:

// 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 生成的测试:

// 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: '[email protected]',
        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);
  });
});

运行测试:

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 供确认:
-- 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;
  1. 确认后运行 pnpm db:migrate

6.2 迁移回滚策略

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

# 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 启动

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 的完整工作流:

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测试完整流程

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

# 1. 注册用户
curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","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"}

架构全景图

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 — Claude Code 构建 REST API 的完整流程
  2. How to Use Claude Code for Database Design and Migration — 数据库设计与迁移实战指南
  3. Claude Code with Drizzle ORM: Schema, Migrations, Queries — Drizzle ORM 与 Claude Code 的集成方法
  4. Beyond One-Shot Prompts: 5 Claude Code Workflow Patterns Explained — Anthropic 官方推荐的五种工作流模式
  5. 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学通信知识星球海报-扫码加入