前端联调接口文档生成器(团队通用)
这个 skill 解决什么
大模块开发完,前端(通常也是 AI)要联调。Swagger 虽然有,但对 AI 不够友好:要点开点去、看不到全局响应包装、不知道哪些接口要带 token、枚举和字段语义散落。这个 skill 把一个模块整理成一份结构固定、自包含、机器可读的 Markdown,让前端 AI 一次读懂、照着就能调。
它解决的不是"提醒你读代码"——一个像样的 agent 读代码、核对约定是本能。它解决的是两件 agent 自己推不出来的事:
- 结构一致:每次产出同一套小标题,前端 AI 能稳定解析,而不是每次换一种排版。
- 团队工作流约定:本次改动从哪来、产物文件名/位置、多模块怎么合并——这些是约定,不在代码里。
读者是前端 AI,写作目标:结构规整、字段类型明确、示例是真实线上格式(含包装层)、不依赖外部上下文。宁可啰嗦写全,也不要让读者去猜。
触发与产出
- 手动触发,针对一次开发涉及的接口(通常一个大模块,如
user、order)。 - 每次调用都优先生成新文件,不覆盖旧文档——文件名带年月日 + 时分秒,便于区分每次产出:
- 文件名
docs/<module>-<YYYYMMDD-HHmmss>.api.md(如docs/order-20260624-143052.api.md)。 - 确认
docs/已在.gitignore;若没有,提示用户加上——它是联调用的临时产物,源头永远是代码。
- 文件名
- 涉及多个模块时,仍只生成一个文件,在文件内按模块拆分章节(每个模块一个二级标题段落),不要拆成多个文件:
- 文件名
docs/<module1>-<module2>-<YYYYMMDD-HHmmss>.api.md,或模块多时用docs/api-<YYYYMMDD-HHmmss>.api.md。
- 文件名
本次改动从哪来(新文件不靠对比旧文档)
每次都是新文件、不覆盖,所以别拿旧文档做 diff。"本次改动"来源按优先级:
- 本次开发的真实代码改动——
git diff(工作区 / 本分支 vs 主开发分支,团队一般是develop)里命中该模块 controller / DTO / 返回映射文件(如*.select.ts/ mapper)/ Prisma schema 的部分,就是这次改了什么。这是最准的来源。 - 当前对话上下文——如果这次产出紧接着一段开发,直接用刚做的改动归纳。
- 两者都没有(纯整理一个老模块)→ 写"全量整理",不要硬凑改动。
想跟上次产出对比,可以人工看
docs/下同模块上一个时间戳的文件,但文档本身不依赖它。
怎么提取信息(数据来源是代码,不是脑补)
按模块读这些文件,拼出每个接口的全貌:
| 要素 | 来源 |
|---|---|
| 路径前缀 | @Controller({ path }) / @Controller('x') → 完整路径见下方"全局约定核对"得到的真实前缀 |
| 方法 + 子路径 | 方法上的 @Get/@Post/...('sub') |
| 是否要鉴权 | 看该项目的鉴权模型:有免登录装饰器(如 @Public())→ 免登录;否则按全局 Guard 要求带 token |
| 入参(body) | @Body() dto: XxxDto → 读该 DTO 的 class-validator 装饰器(必填/长度/正则/类型)+ @ApiProperty(描述/示例) |
| 入参(query/param) | @Query() / @Param(),注意 ParseIntPipe、@Type() 等是否存在——决定 handler 收到的真实类型(见校验行为) |
| 返回值 | service 方法的返回类型 / Prisma model 字段;敏感/内部字段确认是否被 select 排除,别写进返回 |
| 字段语义 / 枚举 | DTO 注释、Prisma schema 的 // 注释、枚举取值 |
| 业务说明 | service 里的关键逻辑(如"手机号唯一""密码哈希不返回") |
拿不准返回结构时,读 service 实现和对应 Prisma model,别编。确实无法确定的字段,标
// TODO: 待确认而不是猜一个。
全局约定核对(核心:读代码,不照抄死值)
前端 AI 最容易错的就是全局约定——而每个项目可能不同(有的前缀 api、有的 v1;包装可能是 {code,message,data,timestamp} 也可能是别的;有的项目关掉了 implicit conversion)。所以这一段必须从目标项目代码实测得到,团队 dl-nestjs-starter 的默认值只作"没读到时的 fallback / 对照基线"。
落笔前按这张清单读代码确认,并把实测结果写进文档头:
| 约定 | 去哪读 | starter 默认值(仅 fallback) |
|---|---|---|
| 路由前缀 | main.ts 的 setGlobalPrefix(...)(含 exclude) |
api(health/docs 除外) |
| 响应包装 | 响应拦截器(*.interceptor.ts)的 wrapper 结构 + 跳过包装的装饰器 |
{ code, message, data, timestamp };@SkipResponseTransform() |
| 鉴权 | 全局 Guard + 免登录装饰器(@Public() 等) |
全局 JwtAuthGuard,@Public() 免登录,token 来自登录接口 |
| 参数校验 | main.ts 的 ValidationPipe 配置 |
whitelist+forbidNonWhitelisted+transform |
⚠️ 发现项目偏离 starter 默认值时,以代码为准,并在文档头明确标注"本项目与常见模板不同"(例:前缀是
v1不是api;包装是payload不是data;enableImplicitConversion关着)。这是这份文档最该帮前端避开的坑。
把实测到的这四条写进文档头,写法参考:
路径拼装
{BASE_URL}/<实测前缀>/{controller-path}/{route}
例(前缀实测为 api 时):@Controller({ path: 'user' }) + @Get('me') → GET /api/user/me
统一响应包装(重要!前端拿到的不是裸数据)
所有响应都被包了一层,真正的业务数据在包装的数据字段里(字段名以实测为准,starter 是 data):
// 成功(以 starter 结构为例,实测可能不同)
{ "code": 200, "message": "success", "data": <业务数据>, "timestamp": "2026-06-04T..." }
// 失败(异常过滤器封装)
{ "code": 400, "message": "错误信息", "data": null, "timestamp": "...", "path": "/..." }
- 例外:标了跳过包装的装饰器(starter 是
@SkipResponseTransform())的接口返回原始数据(文件下载/SSE 等),文档里要特别标注。 - 因此每个接口文档的"返回值"描述的是包装内数据字段的内容,并至少给一个完整包装的示例。
鉴权
- 按实测的全局 Guard 写默认要求;带免登录装饰器的除外。
- token 从登录接口拿。
参数校验行为(前端常踩)
- 若开了
whitelist+forbidNonWhitelisted:传了 DTO 里没声明的多余字段 → 直接 400。前端别乱传字段。 - 留意
transform与enableImplicitConversion:若未开enableImplicitConversion,query 里的数字/布尔字符串不会自动转,全靠 DTO 字段上的@Type(() => Number)/@Transform(...)或ParseIntPipe显式转换。写入参类型时以装饰器/管道为准——没标显式转换的 query 参数到 handler 里仍是 string。
文档模板
文件名见上方"触发与产出"(带年月日+时分秒),按这个结构写(保持每个接口同一套小标题,方便 AI 解析)。多模块时,在 ## 全局约定 之后按模块各起一段(# <模块名> 接口文档 → 接口列表 → 各接口),全局约定只写一次:
# <模块名> 接口文档(前端联调)
> 生成时间:<YYYY-MM-DD HH:mm:ss> | 对应代码:src/modules/<module>/
> 本文件由 dl-api-doc-for-frontend skill 生成,不进版本管理;以代码为准。
## 本次改动
- 新增 `POST /<前缀>/.../xxx`:……
- 字段变更:`Order.status` 新增枚举值 `REFUNDED`
- 行为变更:……
(来源见上方"本次改动从哪来";实在没有改动语境就写"全量整理"。)
## 全局约定
<把上面"全局约定核对"实测到的整段写进来:路径拼装 / 响应包装 / 鉴权 / 校验行为。若与 starter 默认不同,开头一句标注"本项目与常见模板不同:……"。>
## 接口列表
| 方法 | 路径 | 鉴权 | 说明 |
| ---- | --------------- | ---- | -------- |
| POST | /<前缀>/auth/login | 否 | 登录 |
| GET | /<前缀>/user/me | 是 | 当前用户 |
---
## POST /<前缀>/auth/login — 登录
- **鉴权**:否(免登录装饰器)
- **Content-Type**:application/json
- **业务说明**:手机号 + 密码登录,成功返回 accessToken。
### 入参(body)
| 字段 | 类型 | 必填 | 约束 | 说明 |
| -------- | ------ | ---- | --------------- | ------ |
| phone | string | 是 | `^1[3-9]\d{9}$` | 手机号 |
| password | string | 是 | 非空 | 密码 |
### 返回值(包装内数据字段的内容)
| 字段 | 类型 | 说明 |
| ------------- | ------ | ------------------------------------- |
| accessToken | string | JWT,后续请求放 Authorization: Bearer |
| user.userId | number | 用户 ID |
| user.username | string | 用户名 |
### 示例
请求:
```json
POST /<前缀>/auth/login
{ "phone": "13800138000", "password": "P@ssw0rd" }
```
响应(完整包装,以实测包装结构为准):
```json
{
"code": 200,
"message": "success",
"data": {
"accessToken": "eyJhbGci...",
"user": { "userId": 1, "username": "zhangsan" }
},
"timestamp": "2026-06-04T08:00:00.000Z"
}
```
### 错误
| code | 场景 |
| ---- | --------------------------- |
| 400 | 参数校验失败 / 传了多余字段 |
| 401 | 手机号或密码错误 |
写作要点(针对 AI 读者)
- 类型写具体:
number/string/boolean/string[]/ 对象展开到字段;枚举列全部取值('PENDING' | 'PAID' | ...)。 - 必填、约束、可空都标清:前端 AI 据此生成校验和 TS 类型。
- 示例用真实数据:手机号像手机号、时间按实测格式(ISO 串还是时间戳);响应示例一定是带包装的完整结构。
- 敏感/内部字段不出现在返回:password / 哈希 / 风控分 / 创建人等内部字段,确认是否已被 service 的
select排除。若发现接口实际会下发本不该给前端的字段(如findUnique没加select),按"当前真实输出"如实记录,并单列一条风险提示让后端确认修复——别假装它被排除了。 - 不可破坏性变更要标红:删字段、改类型、改路径,在"本次改动"里明确写"⚠️ 破坏性",让前端 AI 知道要改调用。
- 一次产出一份文件:单模块就一个模块段落,多模块在同一文件内按模块拆分章节,不要生成多个文件。
产出给用户
- 已写入的文件路径(带年月日+时分秒的新文件)。
- 一句话说明覆盖了哪些接口、本次改动重点。
- 若该项目全局约定与团队 starter 默认不同,明确点出(前缀/包装/校验差异),这是最容易坑前端的地方。
- 若有字段语义/返回结构无法从代码确定、或发现疑似数据泄露/坏味道,列出来让用户确认(不要猜着写死)。