本教程面向有基础编程经验、希望用 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 认证。
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、安装依赖、创建配置文件和目录结构。
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 在后续所有操作中都会自动遵循这些约定。
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/
| 问题 | 解决方案 |
|---|---|
| pnpm 未安装 | npm install -g pnpm |
| TypeScript 报错 ts-node 未找到 | 用 tsx 替代 ts-node,更稳定 |
| drizzle.config.ts 引用报错 | 确保 drizzle-kit 已安装为 devDependency |
在 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 中。
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),
}));
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"
}
}
请执行数据库迁移,先生成 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
| 问题 | 原因 | 解决 |
|---|---|---|
defaultRandom() 报错 |
pgcrypto 扩展未启用 | 在 migration 前执行 CREATE EXTENSION IF NOT EXISTS pgcrypto; |
| 外键约束失败 | 迁移顺序问题 | 确保被引用的表先创建 |
| relationName 冲突 | 同一表多个外键指向同一表 | 使用 relationName 参数区分 |
请创建 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');
});
请为 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;
}
请创建 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
}
}
}
请创建以下路由文件,遵循 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 });
});
}
// 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);
}
请配置 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; }
请为任务 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
当业务需求变化时(如新增字段),使用 Claude Code 安全地管理迁移:
请为 tasks 表新增以下字段:
1. estimated_hours (integer, nullable) - 预估工时
2. actual_hours (integer, nullable) - 实际工时
3. story_points (integer, nullable) - 故事点
先生成 migration 文件,检查 SQL,然后应用。
注意:这是一个已有生产数据的表,必须确保迁移是安全的。
Claude Code 会:
src/models/schema.ts 添加字段pnpm db:generate 生成增量迁移-- 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;
pnpm db:migrate请为刚才的迁移创建一个回滚脚本 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";
请创建生产级 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"]
# 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:
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"}
以下是用 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
以下是在 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 小时。
在整个开发过程中,以下技巧能显著提升效率:
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 直接执行破坏性的数据库变更。
启动服务后,在终端运行以下命令验证整个链路:
# 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 |
[!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 等场景全覆盖。
