如果你做过前后端分离或者多团队并行开发,大概率经历过这种场面:后端接口已经上线了,前端来问“你返回的不是说好的 status 吗,怎么变成 code 了”,测试拿着旧文档去对时又报出一堆差异。问题往往不在某个人身上,而是接口的“定义”环节就没被当成正经工程来做。Spec Kit 就是把这个环节补上的一套玩法——以 OpenAPI Specification 这类规范文件作为接口的唯一事实来源,再叠加校验、文档生成、Mock、契约测试等工具,让一份 YAML 从零散的描述文本变成整个项目真正在用的工程资产。
我打算在这篇里按从零到专家的路径,把 Spec Kit 的核心思想、YAML 写法、工具链、CI 集成和踩坑经验一次讲透。适合刚接触 API 规范的新手,也适合已经在用 Swagger / OpenAPI 但想往工程化方向走的老手。文中所有示例都是我在真实项目里验证过的,工具命令直接抄就行。
1. 别急着写 YAML,先搞清 Spec Kit 究竟能干什么
1.1 联调返工不是人的问题,而是“接口定义”缺位
我做过的接口项目里,返工最严重的一次不是代码 bug,而是文档和实现完全对不上。后端按自己的理解写接口,前端按产品给的字段名联调,两边都没错,但合在一起就是不通。后来排查发现,接口文档停在三个月前,中间改了五六轮,没有任何人同步过。
很多人觉得这是沟通问题,开会强调一下就好。但开会解决不了根源:接口本身没有一个可执行、可校验、可版本化的定义。Markdown 文档再怎么写,它只能给人看,不能被机器检查,不能驱动 Mock,不能生成客户端代码,也不能在 CI 里拦截破坏性变更。只要文档与代码分离,这种“各写各的”状态就一定会复发。
Spec Kit 解决的就是这个问题。它把接口描述从“文档”变成“代码”:用 OpenAPI 3.x 写一份结构化的 YAML 文件,描述所有路径、参数、请求体、响应体和数据类型。这份文件就是接口的唯一事实来源,所有下游工具都从它派生。
我第一次用这个思路是在一个 ToB 项目里。当时团队从零搭了 20 多个接口,前后端并行开发,我只花了半天把 spec 文件整理出来,前端直接照着 Mock 数据开发,后端照着 schema 实现。那一次,联调从原计划的 3 天压缩到了半天。从那以后,凡是需要前后端协作的接口项目,我都会先把 spec 文件立起来。
1.2 Spec Kit 不是某个软件,而是一套工作范式
需要说明一下“Spec Kit”这个说法。它不是某个必须安装的特定应用程序,而是围绕 OpenAPI Specification 建立起来的一整套工作方式,包括描述语言本身和它周边的一批开源工具。你完全可以把它理解成一组“规格套件”:一份 YAML 管定义,几个工具管消费。
这套范式里,典型的工具链包含六个环节:
- 编写:手写 YAML,或者用可视化编辑器的表单录入(但手写更可控);
- 校验:用 Spectral 检查格式错误、命名规范、安全规则;
- 文档:用 Swagger UI 或 Redoc 一键生成接口文档;
- Mock:用 Prism 等工具从 spec 起一个模拟服务;
- 生成:用 openapi-generator 产出客户端 SDK、服务端骨架、TS 类型;
- 测试:用 openapi-diff 检测版本间是否出现 breaking change。
我把这些工具配起来之后,最大的体感变化是:接口定义不再是“写完了就锁抽屉”的文档,而是贯穿整个研发流程的基础设施。
下面用一张表对比传统文档方式和 Spec Kit 工作流的差别:
| 对比项 | 传统 Markdown 文档 | Spec Kit 工作流 |
|---|---|---|
| 更新驱动 | 手动,靠人记 | 代码变更触发,靠规则约束 |
| 机器可读 | 否 | 是,YAML/JSON |
| 文档生成 | 手写排版 | 工具自动生成 |
| 前端联调 | 等后端接口完成 | Mock 先行 |
| 类型隐患 | 字段名靠肉眼 | 客户端类型自动生成 |
| 变更检查 | 无 | CI 里自动 diff |
| 验证成本 | 测试阶段暴露 | 代码提交前暴露 |
看这张表你就明白,Spec Kit 省的不是写文档那半小时,而是把整个联调链条上的不确定因素一个一个消灭掉。
1.3 用 30 行 YAML 搭起你的第一个 Spec 文件
理论讲再多,不如上手跑一遍。我先给一个最小但完整的 OpenAPI 3.0 示例,后面所有内容都围绕它展开。
yaml复制openapi: 3.0.3
info:
title: Demo API
version: 0.1.0
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
operationId: listUsers
summary: 获取用户列表
parameters:
- name: limit
in: query
schema:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
'400':
description: 参数错误
components:
schemas:
User:
type: object
required:
- id
- name
properties:
id:
type: integer
name:
type: string
逐行拆一下关键字段。openapi: 3.0.3 是版本标记,工具靠它决定解析规则。info 里面至少要有 title 和 version,这是文档生成器的基本信息来源。servers 定义环境地址,可以配多个,比如 dev、staging、prod。
paths 是整个文件的核心,按路径组织接口。每个路径下按 HTTP 方法分,operationId 是给操作起的唯一名字,生成客户端函数名时会用到。parameters 定义请求参数,这里定义了一个 query 参数 limit。responses 必须包含至少一个正常响应,我这里定义了 200 和 400。
最后是 components.schemas,专门放可复用的数据结构。User 被定义为 object,必填 id 和 name,属性分别是 integer 和 string。在 paths 里通过 $ref: '#/components/schemas/User' 引用它。这样当 User 结构变化时,所有引用它的接口自动跟着变,不用逐个改。
这份文件存成 openapi.yaml,就是 Spec Kit 的起点。先把这一行行看懂,后面玩工具时才不会觉得 YAML 是黑魔法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 手写 OpenAPI 的必修课:schema、$ref 与参数规则
2.1 三种最容易被旧习惯带偏的写法
很多从传统文档转过来的人,写 OpenAPI 时会把“描述性”的习惯带进来。最常见的有三种,我一个个说。
第一种是把响应体直接写成 type: object 然后不写 properties。比如 schema: { type: object }。这在语法上合法,但它等于什么都没告诉下游:前端不知道有哪些字段,Mock 不知道返回什么,生成工具也无能为力。OpenAPI 的价值恰恰在“精确定义”,一旦你用 additionalProperties: true 或者干脆空 object,你就退回到了 Markdown 时代。
第二种是只写 description 不写 schema。比如:
yaml复制responses:
'200':
description: 返回用户列表
这个描述确实写了,但字段结构完全缺失。Consumers 拿到这份 spec 只能当小说看,不能当契约用。我在评审时看到这种写法,基本会直接打回。
第三种是同样的对象在每个接口里都重复内联一遍。比如五个接口都要返回 User,就在五个地方各写一份 user 字段。下次 User 加一个字段,你打算改五处?一定有人漏改。正确做法是把 User 抽到 components.schemas 里统一管理。
这三点本质上是同一个原则:能结构化的不要用描述,能复用的不要内联,能机检的不要靠人记。
2.2 schema 类型、required 与 nullable 的正确边界
OpenAPI 里的 schema 遵循 JSON Schema 的子集,但有几个地方特别容易踩坑。
首先是 required。它是个数组,写在对象层级,不是写在属性上。比如上面的 User,id 和 name 必填,就要写 required: [id, name]。很多人写成 id: { required: true },这在 OpenAPI 3 里不生效。
然后是 nullable。默认情况下,schema 类型一旦声明为 string,就不允许值是 null。如果你想让某个字段既能是字符串又能是 null,需要显式加 nullable: true。这个坑在数据库字段上特别常见——数据库的 nullable 字段映射成 API 后,很容易被工具生成成不含 null 的类型,前端跑出 undefined 还不知道哪来的。
yaml复制User:
type: object
required: [id, name]
properties:
id:
type: integer
name:
type: string
nickname:
type: string
nullable: true
这么定义之后,nickname 的值可以是 "Tom" 也可以是 null,但不会是数字 123。类型边界的意义就在这里:它把“合法值”的空间画出来,超出边界的请求或响应都能在联调前被发现。
我在实际项目中还习惯给所有 object schema 显式声明 type: object,即使它有 properties。虽然不写类型也能被推断,但显式声明能让生成器更稳定,特别是后续要从 spec 生成 TS 类型时,少一个推断歧义就少一个坑。
2.3 $ref 引用:路径写错过一次就记住了
$ref 是 OpenAPI 里复用能力的关键。引用分两种,第一种是文件内引用,语法是 #/components/schemas/User。这个写法相当于在 JSON 树里按路径往下走:先找 components,再找 schemas,再找 User。
第二种是外部文件引用,例如 ./schemas/User.yaml#/User。它表示去同级 schemas 目录下打开 User.yaml,取里面根级 key 为 User 的节点。这种引用在大型项目里必不可少,但有个关键限制:$ref 只能出现在 schema 对象出现的位置,不能把整个 response 或整个 path 对象整体替换。某些场景你想“引用整个路径定义”时会发现行不通,需要用组合方式或者把公共部分抽到更小的粒度。
我在一个跨团队项目里踩过一次大坑:一个团队成员在引用外部文件时写了 ./schemas/User.yaml#/components/schemas/User,而实际上 User.yaml 里根本没有 components 这一层。结果是文件能解析,但生成的客户端类型全是空对象。因为没有报错,问题在联调时才暴露。后来我给大家立了一个约定:外部文件一律扁平存放顶层 schema,文件名就是类型名。
引用的好处是天然支持递归。比如订单包含商品列表,商品又含分类对象,层层 $ref 下去,结构清晰,不会出现一个几百行的巨型嵌套。坏处是文件一旦拆多,就需要 redocly bundle 或 swagger-cli bundle 这类工具先把分散文件合并成单文件,再给生成器用。实际工作流是:源码用多文件维护,生成物用单文件交付,两者靠命令转换。
2.4 请求参数与响应结构怎么定义才不返工
请求参数从来源上分四种:path、query、header、cookie。path 参数必须写在路径的模板花括号里,例如 /users/{userId},然后在 parameters 里用 in: path 声明,且必须配 required: true,否则路径模板变量没人赋值。
query 参数的一个常见写法问题是把整个请求体当 query 参数。GET 请求传复杂对象建议用 query 参数平铺,不要试图用 JSON 串塞 query。POST/PUT 的复杂结构用 requestBody,在 content 里声明 media type 和 schema。
响应结构设计上,很多团队会在外层包一个统一壳子,比如 { code: 0, data: ..., msg: "success" }。这种做法在 OpenAPI 里完全可行,只需把 envelope 定义成一个通用 schema:
yaml复制ApiResponse:
type: object
required: [code, data]
properties:
code:
type: integer
data:
nullable: true
msg:
type: string
但我要提醒一句:包壳会显著降低 schema 的可读性,每个接口都要嵌套一层,生成代码时多了打包解包的样板。如果你们是内部系统,我更推荐直接用 HTTP 状态码表达成功失败,用 POST body 的 schema 直接表达数据域。别为了“统一”牺牲契约的简洁性。
还有一个小点是枚举。状态字段尽量用 enum 明确取值范围:
yaml复制UserStatus:
type: string
enum: [active, inactive, banned]
这样前端可以直接生成联合类型,比让前端猜字符串安全得多。
3. 让 Spec 立刻“活”起来:文档、Mock 与代码生成
3.1 一条命令生成 Swagger UI / Redoc 文档
写完 spec,最直观的产出就是接口文档。工具生态里最常见的两个是 Swagger UI 和 Redoc。Swagger UI 带交互调试面板,可以在页面上直接发请求;Redoc 更偏向静态阅读,排版适合分享。
如果你有 Node 环境,用 Redoc CLI 是成本最低的方式:
bash复制npx @redocly/cli build-docs openapi.yaml -o docs.html
生成的 docs.html 可以直接扔给 nginx 或放到对象存储当静态站点。我们团队的文档站就是这么发布的,每次 spec 变更只需要重新跑一次命令,提交产物到 git,或者干脆在 CI 里定时构建。
这里有个我踩过的坑:生成的 HTML 默认会把所有安全定义、响应示例全部渲染出来。如果 spec 里有内部隐私信息,发布前要用 Redoc 的 hide-* 配置把这些敏感项隐掉。文档是对外的门面,很容易被测试当作最终依据,所以发布前务必人工点开几个页面确认没有遗漏。
3.2 用 Spec 起一个 Mock Server:前端不必再等后端
Spec 最大的生产力爆发点,其实是 Mock。前端、客户端、甚至后端自己,都能在真实服务没写好之前,按 spec 定义向前推进。
我用的是 Stoplight 家的 Prism,命令非常简单:
bash复制npx @stoplight/prism-cli mock openapi.yaml
默认监听 4010 端口。启动后前端直接请求 http://127.0.0.1:4010/users,就能拿到符合 schema 结构的假数据。Prism 不仅会按 properties 随机生成值,还会自动识别 enum 选其中一个值,遇到 required 字段必定生成数据,optional 字段可能缺省。
你可以在 YAML 里给 response 加 example 字段来指定 mock 返回的精确值:
yaml复制content:
application/json:
schema:
$ref: '#/components/schemas/User'
example:
id: 1
name: 张三
这个机制让测试数据可以按业务场景定制。比如列表接口需要空数组、单条、多页三种数据,就在不同的 response 定义里写不同 example。前端切场景联调时改 URL 参数即可。
Mock 的意义不只是“前端可以先开发”。它还能提前验证响应结构的可用性,如果字段设计不合理,前端在 mock 阶段就会提出来,而不是等后端写完后推翻重来。这不光省时间,还省心情。
3.3 openapi-generator:从 YAML 批量生成客户端与类型
当接口数量多到一定程度,手写请求函数就是纯体力活。openapi-generator 可以根据 spec 直接生成各种语言的客户端。
以 TypeScript 为例:
bash复制npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-fetch \
-o generated-client
生成之后,你会得到一个带完整类型定义、请求封装、API 方法分组的客户端目录。调用方式类似:
typescript复制import { UsersApi } from './generated-client';
const api = new UsersApi();
const users = await api.listUsers({ limit: 20 });
关键在于,函数名来自 operationId,参数类型来自 query/body 的 schema,响应类型来自 response schema。整个类型系统是单向的:spec 变了,重新生成客户端就同步变了。
这里有一个必须注意的边界:不要把生成的代码直接混进手写业务代码里。 我通常的做法是单独建一个 generated/ 目录,gitignore 掉全部生成内容,在构建流水线里重新生成。如果需要定制请求头、超时时间,封装一个薄薄的 adapter 层,业务代码依赖 adapter,adapter 依赖 generated,这样升级工具版本或改 spec 时,业务代码完全不受伤。
4. 把 Spec 焊进研发流程:CI 校验、契约测试与破坏性变更防护
4.1 Lint 先行:用 Spectral 把团队规范变成机器规则
手写 YAML 难免出错,但错误分两种:YAML 语法错和契约规范错。语法错能靠解析器发现,规范错必须靠规则。Spectral 就是专门干这个的 lint 工具。
安装并执行一次 lint:
bash复制npx @stoplight/spectral-cli lint openapi.yaml -r .spectral.yaml
审查规则写在 .spectral.yaml 里。比如我想禁止接口直接返回纯数组外面没有 envelope,虽然每个团队约定不同,但至少可以定一些通用规则:
yaml复制extends: ["spectral:oas", "spectral:oas-ruleset"]
rules:
operation-operationId:
message: 每个 operation 必须定义 operationId
given: $.paths.*[get,post,put,delete]
then:
field: operationId
function: defined
这段规则挂在 paths 下所有 HTTP 方法节点上,要求它们必须定义 operationId。规则一旦进了 CI,谁来提交都一样,漏掉 operationId 就直接构建失败。
我强烈建议规则集从很少几条开始,先约束最核心的:operationId 必须存在、所有 response 必须有 schema、所有 schema 必须有 type。规则太多会变成官僚负担,团队成员反感后就会绕开 spec。找到团队当前最大的三个问题,写成规则,就够了。
4.2 契约测试:消费者驱动到底怎么落地
契约测试的思路是:不是后端写完接口然后通知前端,而是先有契约,双方照着契约开发。OpenAPI spec 本身就是天然的契约,所以落地时可以分两步走。
第一步,把 spec 作为静态契约:前端按照 spec 生成类型和 mock,后端实现必须匹配 spec 的路径和响应。这一步靠 code review 和 lint 保证。第二步,引入运行时契约验证,比如用 Pact 做消费者驱动契约测试。Pact 的核心流程是消费者端写交互期望,生成 pact 文件,提供者端回放 pact 文件验证自己的实现是否符合期望。
结合 Spec Kit 的场景,比较轻量的做法是:后端项目里写一组基于 spec 的冒烟测试,用 axios 或 supertest 请求自己启动的服务,断言响应结构符合 spec 中对应路径的 schema。这不需要引入 Pact 全家桶,但当每个接口都过了这一层验证后,spec 就不再是“纸面契约”,而是运行时约束。
我见过太多项目,spec 只用来生成文档,实际接口字段跟 spec 差十万八千里。想解决这个问题,靠 review 是不够的,要在测试层把 spec 变成验证器。
4.3 防止破坏性变更:diff 工具进 CI
任何接口有多个调用方时,你无法擅自改名、删字段、改类型。但人总会忘。openapi-diff 就是这个场景的防线:
bash复制npx openapi-diff openapi_old.yaml openapi_new.yaml
它会对比两个版本的 spec,输出变更类型。如果输出里出现 breaking 级别的内容,比如删除了路径、修改了必填字段类型、移除了 enum 枚举值,说明这是破坏性变更。
我在 CI 里做了一个简单规则:main 分支的旧 spec 与 PR 中的新 spec 做 diff,发现 breaking 变更时构建直接红。并不是说不允许破坏性变更,而是让它在 pipeline 上显式暴露,由人工决定是否可接受,而不是等上线后被前端一句“你改了字段怎么不说”砸中。
破坏性变更的常见形态,我整理过一份清单:
- 删除某个 path;
- 修改某个 path 参数名;
- 把可选字段改成必填;
- 收紧 enum 枚举值;
- 改变 schema 类型(string 变 integer);
- 删除 operationId。
openapi-diff 跑完会列出这些,不需要肉眼逐个路径去翻。
5. 从熟练到专家:多文件工程化与复杂 Schema 设计
5.1 把 Monster YAML 拆成多个文件
接口少的时候,一个 openapi.yaml 管所有东西很清爽。但接口到了 50 个以上,单文件很难维护:合并冲突频繁、评审看不清改动、一个缩进错误就全文件报废。
实用的拆分方案是这样:
text复制api/
openapi.yaml
paths/
users.yaml
orders.yaml
components/
schemas/
User.yaml
Order.yaml
ApiResponse.yaml
parameters/
limit.yaml
主文件里用相对路径引用:
yaml复制paths:
/users:
$ref: './paths/users.yaml#/~1users'
等等,这里我要单独讲一下 path 里的 $ref。OpenAPI 3 确实允许在 paths 下用 $ref 引用外部 path 对象,但引用的 key 写起来容易出错。比如引用的对象里 key 是 /users,需要用 JSON Pointer 转义,斜杠写成 ~1。上面这个写法是兼容的,但对不熟的人很不友好。所以我的推荐反而是:主文件里显式写出路径名,内容抽到 paths 模块里,不引用整个 path 对象,而是引用 response 或 parameter。也就是说,paths 文件只放本路径的依赖 schema,不外引 path key。
工具方面,redocly bundle 可以把多文件合成单文件:
bash复制npx @redocly/cli bundle api/openapi.yaml -o build/openapi.json
@redocly/cli 会解析所有外部 $ref,把它们合并进一个输出文件。这个合并产物可以丢给文档、Mock、生成器,各种工具都能吃。
5.2 用 oneOf、allOf 处理复杂业务模型
真实业务很少只有简单 object。比如订单有普通订单和团购订单,字段差异很大,不适合塞进同一个 schema,也不适合用 nullable 硬造。oneOf 可以表达“二选一”:
yaml复制Order:
type: object
properties:
orderType:
type: string
enum: [normal, group]
normalOrder:
$ref: '#/components/schemas/NormalOrder'
groupOrder:
$ref: '#/components/schemas/GroupOrder'
但这里有个细节:oneOf 强调的是“一个且仅一个匹配”,工具在生成类型时会变成联合类型,对前端使用有一定心智负担。如果你想表达“基础字段 + 扩展字段”,用 allOf 更合适:
yaml复制PaginatedUsers:
allOf:
- $ref: '#/components/schemas/Pagination'
- type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/User'
allOf 的意思是把多个 schema 合并成一个。生成代码时它通常被解释为继承或交叉类型,前端拿到的是完整对象,使用起来更顺畅。
使用 anyOf/oneOf 时,我建议配合 discriminator 字段帮助生成器判断类型。没有 discriminator,有些生成器在运行时不知道联合类型里哪一个是实际注入的结构,会导致反序列化问题。虽然写 discriminator 有点繁琐,但这是让复杂 schema 真正可落地的重要一步。
再补一个经验:分页响应是所有接口里最容易出隐患的。我建议把分页结构固定成一个组件 Page<T>,虽然 OpenAPI 没有泛型语法,但可以通过 allOf 缠绕业务对象模拟泛型:
yaml复制Page:
type: object
required: [items, page, pageSize, total]
properties:
items:
type: array
items:
type: object
page:
type: integer
pageSize:
type: integer
total:
type: integer
具体使用时,把 items 里的空 object 替换成真正的 schema 引用。这算是我常用的“准泛型”写法,比每个接口都写一套分页字段干净得多。
5.3 代码生成是杠杆,不是万能钥匙
生成代码很好用,但它不是银弹。我在三个地方被生成代码坑过,总结下来很有价值。
第一个坑是生成代码版本绑定。如果你把 generated-client 提交进 git,openapi-generator 版本升级以后,CI 里用新版本生成的东西会和旧提交冲突。解决方案就是不走同一个目录:本地开发时用一个临时目录生成并被 .gitignore 忽略,正式构建时才输出到目标目录。
第二个坑是复杂 schema 生成的代码质量不稳定。oneOf 联合类型在某些语言里生成出来是一堆 AnyOfXxx 包装类,可读性极差。如果你的业务大量使用多态,别指望生成器帮你写出漂亮的类,考虑手写 DTO,再用 adapter 把 spec 类型映射到 DTO。
第三个坑是生成代码容易把项目体积推大。同一个客户端生成到 iOS / Android / Web 三端,会引入重复的运行时依赖。我的建议是:核心类型(DTO)可以考虑生成,而请求封装层尽量在一个共享模块里手写,接口描述集中管理。
5.4 每个 PR 的 Spec 评审,我会盯着这些看
如果你们团队已经把 spec 当成代码,评审流程也得跟上。我每次评审 spec 改动时,按下面顺序检查:
- 有没有破坏性变更,负责人是否知晓并给出理由;
- 每个命名字段是否符合团队命名规范,operationId 是否唯一;
- 新增 schema 是不是放进了 components,有没有复用已有 schema;
- enum 取值是否可控,有没有用魔法字符串;
- 错误响应的 schema 是否定义完整,而不是只返回字符串文本;
- response 中是否包含前端用不到的内部字段;
- 版本号(info.version)是否按规则递增。
这套清单我打印过一阵子,后来发现形成习惯后,扫一遍 spec 改动只要两分钟。它挡住的问题比我想象得多。
6. 我的踩坑清单与当前工作流
6.1 高频问题速查表
写 spec 这一年多来,我整理了一份速查表,团队里同事遇到问题也会先来翻这个:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 生成的客户端缺类型 | 外部 $ref 路径写错 | 检查文件相对路径和节点层级 |
| Mock 只返回 null | response 里没写 example | 在内容里补充 example |
| Redoc 文档显示空 schema | 响应定义里没有 content/schema | 补全完整 response 结构 |
| openapi-diff 报 breaking | 修改了必填字段或路径 | 按破坏性变更流程审批 |
| Spectral 报 operationId 缺失 | operation 节点没写 operationId | 每个操作补齐唯一 ID |
| bundle 失败 | 外部文件循环引用 | 拆解闭环依赖 |
| 前端拿到 undefined | 字段未声明 nullable | schema 显式加 nullable: true |
| Swagger UI 加载空白 | YAML 缩进错误 | 用 YAML 解析器验证再上传 |
这张表不能覆盖所有问题,但能覆盖 80% 的新手困境。剩下的问题基本都是“字段类型不匹配”或“业务语义没对齐”,需要回到场景里跟产品沟通。
6.2 我目前在用的初始化流程
新项目接入 Spec Kit 时,我通常会按这套流程走,基本没有返工:
- 建
api/目录,先写主openapi.yaml,只定义 info 和 servers; - 从最核心的 3 个接口开始,边写边抽 components;
- 本地跑一遍
spectral lint,把命名的、必填的、类型的规则全部过掉; - 用
prism mock起 Mock,前端开始联调页面; - 用 openapi-generator 生成 TS 类型,检查字段语义是否符合预期;
- 把 lint、bundle、mock 命令写进 Makefile 或 NPM scripts;
- 搭建 CI 阶段:lint + 文档构建 + openapi-diff 防破坏变更;
- 每次 PR 都包含 spec 变更,reviewer 按前面那份清单检查。
这套流程跑顺后,新接口从设计到前端可开始开发,通常在一天内完成。对比以前要等地后端接口联调,效率是质的提升。
最后说一点个人体会。Spec Kit 这套东西,表面上是在写 YAML、跑命令,本质上是在逼着团队所有人把“接口长什么样”这个问题在动手前就想清楚。它不解决产品逻辑问题,也不替代 code review,但它能把沟通成本转换成结构化的、可检查的资产。如果你还在用手写文档维护接口,我真的建议从今天开始,哪怕只是把最常用的三个接口写成 spec 先试试。只要你坚持两个星期,多半就再也不想回去写那份没有人看的 Markdown 了。
