--- name: api-development description: FastGPT API 开发规范。重点强调使用 zod schema 定义入参和出参,在 API 文档中声明路由信息,编写对应的 OpenAPI 文档,以及在 API 路由中使用 schema.parse 进行验证。 --- # FastGPT API 开发规范 > FastGPT 项目 API 路由开发的标准化指南,确保 API 的一致性、类型安全和文档完整性。 ## 何时使用此技能 - 开发新的 Next.js API 路由 - 修改现有 API 的入参或出参 - 需要 API 类型定义和文档 - 审查 API 相关代码 ## 核心原则 ### 🔴 必须遵守的规则 1. **API 中实际存在的业务入参和业务出参必须使用 zod schema 定义;无入参或空成功响应不创建空 Schema** 2. **已定义的 schema 必须导出对应的 TypeScript 类型** 3. **必须在 schema 文件头部声明 API 信息(路由、方法、描述、标签),一次性管理员升级/清洗能力除外** 4. **实际存在的入参必须使用 `parseApiInput` 验证;完全无入参时不做空 Query/Body Schema 校验** 5. **实际存在业务数据的函数返回值必须使用 schema.parse() 验证;空成功响应直接返回 `undefined`,不做空 Schema 校验** 6. **必须编写完整的 OpenAPI 文档,一次性管理员升级/清洗能力除外** ### 管理员升级与清洗能力的文档豁免 仅供系统管理员执行、用于一次性升级、迁移、修复或数据清洗的内部接口和脚本,不属于产品 API,不要求: - 在 `packages/global/openapi/` 声明接口文档; - 注册 OpenAPI Path; - 编写 API 头部路由、方法、描述和标签信息。 豁免只针对文档,不豁免安全和校验要求: - 管理员接口必须使用 `authSystemAdmin` 鉴权; - 实际存在的 API 入参必须使用 Zod Schema 和 `parseApiInput`,完全无入参时不创建空 Schema; - 实际存在业务数据的返回值必须使用 Zod Schema 校验,空成功响应直接返回 `undefined`; - 数据清洗默认使用 dry-run,显式确认后才能写入,并输出成功、跳过和失败统计; - 清洗逻辑应可重复执行,无法安全修复的数据必须跳过并报告,不得静默填入猜测值。 常规管理员产品接口(例如模型配置 CRUD、用户管理和系统配置)不因仅管理员可用而获得豁免,仍需按标准 API 流程维护文档。 ## 开发流程 ### 步骤 1: 定义 Zod Schema 并声明 API **文件位置**: `packages/global/openapi/[module]/[api].ts` **文件头部必须声明 API 信息**: ```typescript import { z } from 'zod'; /* ============================================================================ * API: 获取应用对话日志列表 * Route: POST /api/core/app/logs/list * Method: POST * Description: 获取指定应用的对话日志列表,支持分页和多种筛选条件 * Tags: ['App', 'Log', 'Read'] * ============================================================================ */ // 入参 Schema export const GetAppChatLogsBodySchema = PaginationSchema.extend({ appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }), dateStart: z.union([z.string(), z.date()]).meta({ example: '2024-01-01T00:00:00.000Z', description: '开始时间' }), dateEnd: z.union([z.string(), z.date()]).meta({ example: '2024-12-31T23:59:59.999Z', description: '结束时间' }), sources: z.array(z.nativeEnum(ChatSourceEnum)).optional().meta({ example: [ChatSourceEnum.api, ChatSourceEnum.online], description: '对话来源筛选' }) }); // 导出入参类型 export type getAppChatLogsBody = z.infer; // 出参 Schema export const GetAppChatLogsResponseSchema = z.object({ total: z.number().meta({ example: 100, description: '总记录数' }), list: z.array(ChatLogItemSchema) }); // 导出出参类型 export type getAppChatLogsResponseType = z.infer; ``` **API 声明规范**: ```typescript /** * 每个 API 文件必须在文件头部声明以下信息: * * 1. API 名称 (API): 简短的功能描述 * 2. 路由 (Route): 完整的 API 路径 * 3. 方法 (Method): HTTP 方法 (GET/POST/PUT/DELETE) * 4. 描述 (Description): API 的详细功能说明 * 5. 标签 (Tags): API 的分类标签数组 * * 标签示例: * - 'App': 应用相关 API * - 'User': 用户相关 API * - 'Log': 日志相关 API * - 'Read': 只读操作 * - 'Write': 写入操作 * - 'Delete': 删除操作 */ ``` **OpenAPI Tag 归属规则**: - 如果接口能力属于通用模块 A,但会被业务模块 B 使用,则该接口必须同时声明 A 模块 tag 和 B 模块 tag。 - 如果同一个通用接口也被业务模块 C 使用,则继续追加 C 模块 tag。 - 通用模块 tag 表示接口能力和实现抽象归属;业务模块 tag 表示该接口应出现在对应业务文档视角里。 - 如果接口只是业务模块自己的状态查询或状态操作,不属于通用模块能力,则只声明业务模块 tag,不要为了实现位置或相邻目录误加通用模块 tag。 - 示例:`协作者管理` 是通用权限能力,应用协作者接口需要同时声明 `协作者管理` 和应用侧 `权限管理`;`获取应用权限`、`恢复应用继承权限` 是应用自身权限状态接口,只声明应用侧 `权限管理`。 **Schema 定义规范**: #### ✅ 空入参与空成功响应 - 请求没有 query、body 或 params 时,不要为了形式完整而声明 `z.object({})`,路由中也不需要调用 `parseApiInput`。 - 成功响应没有业务数据时,不要声明 `z.undefined()`、`z.null()` 或 `z.object({})` 作为占位 Schema,也不需要调用 `Schema.parse(undefined)`;handler 直接不返回值或返回 `undefined`。 - 只有请求或响应实际携带业务字段时,才定义对应 Schema、导出类型并在 API 边界执行校验。 - OpenAPI 中无入参接口省略 `requestParams`/`requestBody`;空成功响应只保留状态码和说明,不声明占位 schema。 #### ✅ 字段定义规范 ```typescript // ✅ 好的实践: 完整的 meta 信息 export const GetUserSchema = z.object({ userId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '用户 ID' }), email: z.string().email().meta({ example: 'user@example.com', description: '用户邮箱' }), age: z.number().int().positive().meta({ example: 25, description: '用户年龄' }), status: z.enum(['active', 'inactive']).meta({ example: 'active', description: '用户状态' }) }); // ❌ 不好的实践: 缺少 meta 信息 export const GetUserSchemaBad = z.object({ userId: z.string(), email: z.string(), age: z.number(), status: z.string() }); ``` #### ✅ 嵌套对象定义 ```typescript // 嵌套对象应该定义为独立的 Schema export const AddressSchema = z.object({ street: z.string().meta({ description: '街道地址' }), city: z.string().meta({ description: '城市' }), country: z.string().meta({ description: '国家' }) }); export const CreateUserSchema = z.object({ name: z.string().meta({ description: '用户名' }), address: AddressSchema.meta({ description: '地址信息' }) }); ``` #### ✅ 数组定义 ```typescript export const GetUserListResponseSchema = z.object({ total: z.number().meta({ example: 100, description: '总数' }), list: z.array( z.object({ id: z.string().meta({ description: '用户 ID' }), name: z.string().meta({ description: '用户名' }) }) ).meta({ description: '用户列表' }) }); ``` #### ✅ 可选字段 ```typescript export const UpdateUserSchema = z.object({ userId: z.string().meta({ description: '用户 ID' }), // 可选字段使用 .optional() name: z.string().optional().meta({ description: '用户名' }), // 或使用 .nullish() 允许 null 和 undefined email: z.string().email().nullish().meta({ description: '用户邮箱' }) }); ``` #### ✅ 分页 Schema ```typescript import { PaginationSchema } from '@fastgpt/global/openapi/api'; // 继承分页 Schema export const GetUserListSchema = PaginationSchema.extend({ // 添加额外的筛选字段 keyword: z.string().optional().meta({ description: '搜索关键词' }), status: z.enum(['active', 'inactive']).optional().meta({ description: '状态筛选' }) }); ``` 分页请求参数必须优先复用 `@fastgpt/global/openapi/api` 导出的 `PaginationSchema`,不要在业务域或单个接口中重复声明同构的分页 Schema(例如再次定义 `UserPaginationBodySchema`)。如果接口确实需要额外筛选条件,使用 `PaginationSchema.extend(...)`;只有存在明确的兼容性或协议差异时,才允许定义专用 wrapper,并在附近说明原因。分页响应优先复用 `PaginationResponseSchema`。 #### ✅ 多个 API 的 Schema 文件 ```typescript /* ============================================================================ * API: 获取日志键 * Route: GET /api/core/app/logs/keys * Method: GET * Description: 获取应用的日志配置键列表 * Tags: ['App', 'Log', 'Read'] * ============================================================================ */ export const GetLogKeysQuerySchema = z.object({ appId: z.string().meta({ description: '应用 ID' }) }); export const GetLogKeysResponseSchema = z.object({ logKeys: z.array(AppLogKeysSchema).meta({ description: '日志键列表' }) }); /* ============================================================================ * API: 更新日志键 * Route: POST /api/core/app/logs/keys * Method: POST * Description: 更新应用的日志配置键 * Tags: ['App', 'Log', 'Write'] * ============================================================================ */ export const UpdateLogKeysBodySchema = z.object({ appId: z.string().meta({ description: '应用 ID' }), logKeys: z.array(AppLogKeysSchema).meta({ description: '日志键列表' }) }); ``` ### 步骤 2: 实现 API 路由 **文件位置**: `projects/app/src/pages/api/[path]/[route].ts` **标准实现模板**: ```typescript import type { NextApiResponse } from 'next'; import { NextAPI } from '@/service/middleware/entry'; import type { ApiRequestProps } from '@fastgpt/service/types/next'; import { GetAppChatLogsBodySchema, GetAppChatLogsResponseSchema, type getAppChatLogsResponseType } from '@fastgpt/global/openapi/...'; async function handler( req: ApiRequestProps, _res: NextApiResponse ): Promise { // 🔴 步骤 1: 使用 schema.parse() 验证入参 const { appId, dateStart, dateEnd, sources } = GetAppChatLogsBodySchema.parse(req.body); // 或对于 query 参数 // const { param1, param2 } = YourAPIQuerySchema.parse(req.query); // 🔴 步骤 2: 业务逻辑处理 const result = await yourBusinessLogic({ appId, dateStart, dateEnd, sources }); // 🔴 步骤 3: 使用 schema.parse() 验证出参 return GetAppChatLogsResponseSchema.parse({ list: result.list, total: result.total }); } export default NextAPI(handler); ``` **完整示例**: ```typescript import type { NextApiResponse } from 'next'; import type { ApiRequestProps } from '@fastgpt/service/types/next'; import { NextAPI } from '@/service/middleware/entry'; import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { GetAppChatLogsBodySchema, GetAppChatLogsResponseSchema, type getAppChatLogsResponseType } from '@fastgpt/global/openapi/core/app/log/api'; async function handler( req: ApiRequestProps, _res: NextApiResponse ): Promise { // 🔴 1. 验证入参 const { appId, dateStart, dateEnd, sources } = GetAppChatLogsBodySchema.parse(req.body); // 2. 权限验证 (如果需要) await authApp({ req, authToken: true, appId, per: AppReadChatLogPerVal }); // 3. 业务逻辑 const { list, total } = await getChatLogsFromDB({ appId, dateStart, dateEnd, sources }); // 🔴 4. 验证出参 return GetAppChatLogsResponseSchema.parse({ list, total }); } export default NextAPI(handler); ``` ### 步骤 3: 权限验证 (如需要) **使用 `authApp` 或其他权限验证函数**: ```typescript import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { AppWritePerVal } from '@fastgpt/global/support/permission/app/constant'; async function handler(req: ApiRequestProps, res: NextApiResponse) { const { appId } = YourAPIBodySchema.parse(req.body); // 权限验证 await authApp({ req, authToken: true, appId, per: AppWritePerVal // 权限常量 }); // 继续处理... } ``` ### 步骤 4: 错误处理 **使用统一的错误处理**: ```typescript import { APIError } from '@fastgpt/service/core/error/controller'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; async function handler(req: ApiRequestProps, res: NextApiResponse) { try { const { appId } = YourAPIBodySchema.parse(req.body); if (!appId) { return Promise.reject(CommonErrEnum.missingParams); } // 业务逻辑... } catch (error) { // 统一错误处理 return APIError(error)(req, res); } } ``` ## 完整开发示例 ### 场景: 创建用户 API **1. 定义 Schema** (`packages/global/openapi/core/user/api.ts`): ```typescript import { z } from 'zod'; /* ============================================================================ * API: 创建用户 * Route: POST /api/core/user/create * Method: POST * Description: 创建新用户,返回创建的用户信息 * Tags: ['User', 'Write'] * ============================================================================ */ // 入参 export const CreateUserBodySchema = z.object({ name: z.string().min(2).max(50).meta({ example: 'Alice', description: '用户名 (2-50 字符)' }), email: z.string().email().meta({ example: 'alice@example.com', description: '用户邮箱' }), age: z.number().int().positive().optional().meta({ example: 25, description: '用户年龄' }), avatar: z.string().url().optional().meta({ example: 'https://example.com/avatar.jpg', description: '头像 URL' }) }); export type createUserBodyType = z.infer; // 出参 export const CreateUserResponseSchema = z.object({ userId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '用户 ID' }), name: z.string().meta({ example: 'Alice', description: '用户名' }), email: z.string().meta({ example: 'alice@example.com', description: '用户邮箱' }), createdAt: z.date().meta({ example: '2024-01-01T00:00:00.000Z', description: '创建时间' }) }); export type createUserResponseType = z.infer; ``` **2. 实现 API** (`projects/app/src/pages/api/core/user/create.ts`): ```typescript import type { NextApiResponse } from 'next'; import { NextAPI } from '@/service/middleware/entry'; import type { ApiRequestProps } from '@fastgpt/service/types/next'; import { MongoUser } from '@fastgpt/service/core/user/schema'; import { CreateUserBodySchema, CreateUserResponseSchema, type createUserResponseType } from '@fastgpt/global/openapi/core/user/api'; async function handler( req: ApiRequestProps, _res: NextApiResponse ): Promise { // 🔴 验证入参 const { name, email, age, avatar } = CreateUserBodySchema.parse(req.body); // 检查邮箱是否已存在 const existingUser = await MongoUser.findOne({ email }); if (existingUser) { return Promise.reject('Email already exists'); } // 创建用户 const user = await MongoUser.create({ name, email, age, avatar, createdAt: new Date() }); // 🔴 验证出参 return CreateUserResponseSchema.parse({ userId: user._id.toString(), name: user.name, email: user.email, createdAt: user.createdAt }); } export default NextAPI(handler); ``` ## 审查检查清单 ### 🔴 必须检查项 (阻塞性) **Schema 文件** (`packages/global/openapi/.../api.ts`): - [ ] **文档豁免判断**: 仅一次性管理员升级/清洗能力可跳过 OpenAPI 文档 - [ ] **API 声明**: 文件头部有 API 信息(路由、方法、描述、标签) - [ ] **Schema 定义**: 实际存在的业务入参和业务出参使用 zod 定义;没有业务值时不创建空 Schema - [ ] **类型导出**: 为已定义的 Schema 导出 `z.infer` 类型 - [ ] **Meta 信息**: 所有字段都有 `description` 和 `example` **API 路由文件** (`projects/app/src/pages/api/.../route.ts`): - [ ] **入参验证**: 存在实际入参时使用 `parseApiInput`;完全无入参时不做空 Schema 校验 - [ ] **出参验证**: 返回实际业务数据时使用 `Schema.parse(responseData)`;空成功响应无需 Schema 和 parse - [ ] **函数返回类型**: 函数返回值声明为导出的类型 - [ ] **权限验证**: API 路由有相应的权限检查 (如需要) ### 🟡 推荐检查项 (建议性) - [ ] **错误处理**: 使用 `APIError` 统一错误处理 - [ ] **字段验证**: 使用 zod 的验证方法 (.min(), .max(), .email() 等) - [ ] **可空字段**: 正确使用 `.optional()` 或 `.nullish()` - [ ] **复用 Schema**: 相同结构抽取为独立 Schema - [ ] **分页支持**: 列表 API 继承 `PaginationSchema` - [ ] **Schema 复用**: 分页及其他通用结构优先复用全局 Schema,未重复声明同构的业务专用 Schema ### 🟢 可选检查项 (优化性) - [ ] **字段顺序**: 字段按重要性排序 - [ ] **Schema 复用**: 复用现有 Schema 减少重复 - [ ] **注释**: 复杂逻辑添加注释 ## 常见问题和解决方案 ### 问题 1: 缺少 API 声明 **错误示例**: ```typescript // ❌ 错误: 缺少 API 声明 import { z } from 'zod'; export const GetUserSchema = z.object({ id: z.string() }); ``` **正确做法**: ```typescript // ✅ 正确: 包含完整的 API 声明 import { z } from 'zod'; /* ============================================================================ * API: 获取用户信息 * Route: GET /api/core/user/detail * Method: GET * Description: 根据 userId 获取用户详细信息 * Tags: ['User', 'Read'] * ============================================================================ */ export const GetUserSchema = z.object({ id: z.string().meta({ example: '68ad85a7463006c963799a05', description: '用户 ID' }) }); ``` ### 问题 2: 类型不匹配 **错误示例**: ```typescript // ❌ 错误: 函数返回类型未声明 async function handler(req: ApiRequestProps, res: NextApiResponse) { const data = YourAPIBodySchema.parse(req.body); return { success: true, data }; // 类型未声明 } ``` **正确做法**: ```typescript // ✅ 正确: 声明返回类型 async function handler( req: ApiRequestProps, _res: NextApiResponse ): Promise { const data = YourAPIBodySchema.parse(req.body); return YourAPIResponseSchema.parse({ success: true, data }); } ``` ### 问题 3: 缺少 Meta 信息 **错误示例**: ```typescript // ❌ 错误: 缺少 meta 信息 export const UserSchema = z.object({ id: z.string(), name: z.string(), email: z.string() }); ``` **正确做法**: ```typescript // ✅ 正确: 完整的 meta 信息 export const UserSchema = z.object({ id: z.string().meta({ example: '68ad85a7463006c963799a05', description: '用户 ID' }), name: z.string().meta({ example: 'Alice', description: '用户名' }), email: z.string().email().meta({ example: 'alice@example.com', description: '用户邮箱' }) }); ``` ### 问题 4: 未验证出参 **错误示例**: ```typescript // ❌ 错误: 直接返回数据 async function handler(req: ApiRequestProps, res: NextApiResponse) { const { appId } = YourAPIBodySchema.parse(req.body); const result = await getData(appId); return result; // 未验证出参结构 } ``` **正确做法**: ```typescript // ✅ 正确: 验证出参 async function handler(req: ApiRequestProps, res: NextApiResponse) { const { appId } = YourAPIBodySchema.parse(req.body); const result = await getData(appId); return YourAPIResponseSchema.parse(result); } ``` ### 问题 5: Schema 复用不当 **不好做法**: ```typescript // ❌ 重复定义相同的结构 export const Schema1 = z.object({ id: z.string(), name: z.string(), email: z.string() }); export const Schema2 = z.object({ id: z.string(), name: z.string(), email: z.string() }); ``` **正确做法**: ```typescript // ✅ 抽取公共 Schema export const BaseUserSchema = z.object({ id: z.string().meta({ description: 'ID' }), name: z.string().meta({ description: '名称' }), email: z.string().email().meta({ description: '邮箱' }) }); export const Schema1 = z.object({ user: BaseUserSchema }); export const Schema2 = z.object({ users: z.array(BaseUserSchema) }); ``` ## 快速参考 ### API 声明模板 ```typescript /* ============================================================================ * API: [简短功能描述] * Route: [HTTP 方法] [完整路由路径] * Method: [GET/POST/PUT/DELETE] * Description: [详细功能说明] * Tags: [['模块', '子模块', '操作类型']] * ============================================================================ */ ``` ### 常用标签 - **模块标签**: `App`, `User`, `Chat`, `Workflow`, `Dataset` - **操作类型**: `Read`, `Write`, `Delete`, `Update` - **其他**: `Admin`, `Public`, `Internal` ### 常用 Zod 验证方法 ```typescript // 字符串 z.string() // 字符串 .min(2) // 最小长度 .max(50) // 最大长度 .email() // 邮箱格式 .url() // URL 格式 .uuid() // UUID 格式 // 数字 z.number() // 数字 .int() // 整数 .positive() // 正数 .min(0) // 最小值 .max(100) // 最大值 // 布尔 z.boolean() // 布尔值 // 日期 z.date() // 日期对象 .or(z.string()) // 或日期字符串 // 枚举 z.enum(['active', 'inactive']) // 枚举值 z.nativeEnum(MyEnum) // TypeScript 枚举 // 数组 z.array(z.string()) // 字符串数组 .min(1) // 最小长度 .max(10) // 最大长度 // 可选 z.string().optional() // 可选 (undefined) z.string().nullish() // 可空 (undefined | null) // 对象 z.object({ // 对象 name: z.string(), age: z.number() }) // 继承 PaginationSchema.extend({ // 扩展 keyword: z.string() }) // 联合类型 z.union([z.string(), z.number()]) // 字符串或数字 z.discriminator('type', { // 判别联合 type1: Type1Schema, type2: Type2Schema }) ``` ### Meta 字段说明 ```typescript z.string().meta({ example: 'value', // 示例值 (必填) description: '字段说明' // 字段描述 (必填) }) ``` ### TypeScript 类型导出 ```typescript // Schema 定义 export const UserSchema = z.object({ id: z.string(), name: z.string() }); // 导出类型 (命名规范: camelCase) export type userType = z.infer; // 或使用 PascalCase export type UserType = z.infer; ``` ## 参考资源 ### 项目内示例 - **API Schema 示例**: `/Volumes/code/fastgpt-pro/FastGPT/packages/global/openapi/core/app/log/api.ts` - **API 实现示例**: `/Volumes/code/fastgpt-pro/FastGPT/projects/app/src/pages/api/core/app/logs/list.ts` - **分页 Schema**: `packages/global/openapi/api.ts` ### 相关文档 - **Zod 官方文档**: https://zod.dev/ - **FastGPT API 规范**: `.claude/skills/pr-review/fastgpt-style-guide.md` - **PR Review 审查维度**: `.claude/skills/pr-review/code-quality-standards.md` --- **Version**: 1.0 **Last Updated**: 2026-01-27 **Maintainer**: FastGPT Development Team