- 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<typeof GetAppChatLogsBodySchema>;
// 出参 Schema
export const GetAppChatLogsResponseSchema = z.object({
total: z.number().meta({ example: 100, description: '总记录数' }),
list: z.array(ChatLogItemSchema)
});
// 导出出参类型
export type getAppChatLogsResponseType = z.infer<typeof GetAppChatLogsResponseSchema>;
```
**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<getAppChatLogsResponseType> {
// 🔴 步骤 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
View on GitHub