↓ 跳过正文

API OpenAPI API Design

API 设计优先(Design-First)工作流:每个 Sprint 先定好 OpenAPI 契约

我们每个 Sprint 跑一遍的 API 设计优先流程:前后端各出一名负责人敲定 OpenAPI 契约,CI 拉起模拟服务器,两边并行开发。附示例 YAML。

API 设计优先(Design-First)工作流:每个 Sprint 先定好 OpenAPI 契约

前不久我写过一篇用 OpenAPI 做 API 优先开发的泛泛而谈,这篇是具体版:我们团队正在试行的计划工作流,外加配套的 YAML 示例。

简单说,这套设计优先的流程每个 Sprint 走一遍:

  1. 为本 Sprint 的 API 工作定两名负责人,后端一名、前端一名。
  2. 两人对照本 Sprint 的需求编写(或更新)OpenAPI 接口定义,反复评审,直到双方都认可。
  3. 把接口定义提交到仓库。从这一刻起,它就是契约。
  4. CI 根据提交的接口定义拉起一个模拟服务器。
  5. 前端对着模拟服务器开发,后端同时实现真实的端点。
  6. 真实服务部署后,由它生成的规范文件就是下一个 Sprint 接口定义的起点。

事情的起因
#

起因是一次回顾会。反馈说得很不客气:API 的规划几乎没有任何可见性。后端按自己对需求的理解去实现端点,前端事后才知道响应长什么样,分歧全都在集成阶段——也就是代价最高的时刻——才暴露出来。于是有了这个提案:把两边都拉进计划阶段,让 OpenAPI 接口定义(schema)成为一切的锚点。

流程
#

每周选两名代表,后端一名、前端一名。他们的任务是吃透本次 Sprint 的需求,并让计划用的 OpenAPI 接口定义与之对齐。两人一起评审这份定义,把对不上的地方改掉。等这一步做完,在动手之前,双方就已经确切知道要造的是什么了。

修订定稿后,把接口定义提交到仓库,CI/CD 会根据这份 yaml 生成模拟 API。Sprint 结束时,再切一份覆盖下个 Sprint API 需求的新 yaml,循环往复。

工具备忘
#

可视化 yaml 用 Swagger Editor 就够了:把文件传上去,就能看到渲染好的 Swagger 文档。

CI/CD 流水线需要能接收 yaml 并据此拉起一个模拟服务器。Stoplight 的 Prism 用的人很多,也支持 OpenAPI 3.1:

npm install -g @stoplight/prism-cli
prism mock openapi.yaml

这样接口定义里的所有路径都会在 http://127.0.0.1:4010 上提供服务,返回的就是定义里的 examples,比如请求 GET /lists/10,拿到的就是附录里那条示例列表。它还会替你守住契约:示例声明了 API key,所以不带 X-API-Key 请求头的请求会直接收到 401,而这正是前端应该照着去做的行为。在容器里跑的话,记得加上 -h 0.0.0.0,外面才访问得到。openapi-mock 是个更轻量的替代品,会用随机数据填充响应。

拉起模拟服务器之前,最好先给接口定义做一遍 lint。契约写坏了,构建直接失败,总比让前端一头雾水强。Spectral 只要一行规则集就能跑起来:

npm install -g @stoplight/spectral-cli
echo 'extends: ["spectral:oas"]' > .spectral.yaml
spectral lint openapi.yaml

最后,这份接口定义纯粹是计划产物。真实 API 部署之后,应该由各服务自己生成 openapi.json,下载下来再喂给下一轮计划。

为什么值得费这个劲
#

因为争论被挪到了代价最低的地方。写接口定义的时候前后端工程师都在场,误解还没变成代码就被抓住了。API 需求一次谈妥,不用在 Sprint 中途反复扯皮;有了模拟服务器,前端立刻就能开工,不用干等;关键细节被漏掉的机会也小得多——它们得同时躲过两双眼睛和一份白纸黑字的契约才行。

这里没有任何革命性的东西。只是把计划做得足够具体,具体到机器能把它当成模拟服务还给你——事实证明,一份计划需要的具体程度,恰好就是这个程度。

附录A - yaml示例
#

openapi: 3.1.0
info:
  title: MyApp Sprint 3
  description: Planning schema for sprint 3. Drives the mock server only.
  version: 0.3.0
servers:
  - url: https://api-mock.example.com/v1
tags:
  - name: lists
    description: Saved lists feature (PROJ-123)
security:
  - api_key: []
paths:
  /lists:
    get:
      tags: [lists]
      summary: Return the current user's saved lists
      operationId: getLists
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/List'
  /lists/{listId}:
    get:
      tags: [lists]
      summary: Find a list by ID
      operationId: getListById
      parameters:
        - name: listId
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/List'
        '404':
          description: List not found
components:
  schemas:
    List:
      type: object
      required: [id, title]
      properties:
        id:
          type: integer
          format: int64
          examples: [10]
        title:
          type: string
          examples: [Weekend reading]
        itemCount:
          type: integer
          examples: [7]
  securitySchemes:
    api_key:
      type: apiKey
      name: X-API-Key
      in: header