<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>OpenAPI · 仁才德</title>
    <link>https://jared.lynskey.co.nz/zh-cn/tags/openapi/</link>
    <description>关于软件工程与研发团队领导力的短文——绩效评估、招聘与团队扩张、CI/CD、DevOps 监控、智能体 AI。写于首尔。</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <copyright>© 2026 仁才德</copyright>
    <lastBuildDate>Wed, 28 Oct 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/zh-cn/tags/openapi/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Orval：从OpenAPI规范自动生成带类型的API客户端和mock</title>
      <link>https://jared.lynskey.co.nz/zh-cn/posts/2024/2024-04-10-openapitools/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/zh-cn/posts/2024/2024-04-10-openapitools/</guid>
      <pubDate>Wed, 10 Apr 2024 00:30:03 +0000</pubDate>
      <dc:creator>Me</dc:creator>
      <category>API Development</category>
      <category>Orval</category>
      <category>OpenAPI</category>
      <category>TypeScript</category>
      <category>React Query</category>
      <category>MSW</category>
      <category>Frontend</category>
      <category>Backend</category>
      <category>Teamwork</category>
      <category>Efficiency</category>
      <description>Orval读取OpenAPI规范，生成带类型的TypeScript客户端、React Query hooks和MSW mock，没人需要再手写API层。本文讲怎么配置、mock怎么用，以及它和openapi-generator、openapi-typescript、Hey API比起来怎么样。</description>
      <content:encoded><![CDATA[<p>Orval是一个代码生成器：它读取OpenAPI（Swagger）规范，替你写好前端的API层。每个接口一个带类型的函数或React Query hook，TypeScript模型和契约完全一致，需要的话还有填好Faker数据的MSW mock handler。你只要提交一个配置文件，运行<code>orval</code>，规范一变就再运行一次。</p>
<p>这个系列的第一篇里，我主张在写任何代码之前先把OpenAPI契约定下来。这一篇要讲的，是让这份契约真正“回本”的工具。</p>
<p>手写前端的API层，是那种没人会承认自己喜欢的活儿。从后端文档里抄类型，第四十次写同一个请求封装，三周后有人把某个字段改了名，一半的调用就悄无声息地坏掉了。Orval直接把这个活儿删掉了。把它指向你的OpenAPI规范，客户端代码就自动生成：每个接口都有带类型的函数，模型和契约完全一致，还有后端根本不存在时就能用来开发的mock。</p>

<h2 class="relative group">前端团队为什么喜欢它
    <div id="前端团队为什么喜欢它" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%89%8d%e7%ab%af%e5%9b%a2%e9%98%9f%e4%b8%ba%e4%bb%80%e4%b9%88%e5%96%9c%e6%ac%a2%e5%ae%83" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>mock生成是最被低估的部分。规范一旦谈定，前端就能起一个假服务器，对着它把整个UI做完，不用再等那些永远“快好了”的接口。等真正的后端上线，把客户端指过去，大部分东西直接就能跑，因为两边是照着同一份文档做的。</p>
<p>另一个收获是：无聊的代码从此“天生正确”。没有人会在手写的请求体里敲错字段名了，因为根本没有人再手写请求体。</p>

<h2 class="relative group">后端团队为什么也喜欢它
    <div id="后端团队为什么也喜欢它" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%90%8e%e7%ab%af%e5%9b%a2%e9%98%9f%e4%b8%ba%e4%bb%80%e4%b9%88%e4%b9%9f%e5%96%9c%e6%ac%a2%e5%ae%83" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>规范成为唯一事实来源之后，日常沟通都变了。不再有“这个接口到底返回什么来着？”的群聊追问，前端直接看生成的类型就行。后端开发者被打断得更少，反馈来得更早：前端对着mock开发时觉得哪个API别扭，你在改起来还便宜的阶段就知道了，而不是等它进了生产环境。</p>

<h2 class="relative group">怎么融入工作流
    <div id="怎么融入工作流" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%80%8e%e4%b9%88%e8%9e%8d%e5%85%a5%e5%b7%a5%e4%bd%9c%e6%b5%81" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>没什么花哨的。后端提出规范，两个团队一起评审；前端用Orval生成客户端和mock，直接开工；后端按同一份契约实现真实接口。规范一变，两边重新生成，编译器会准确告诉你哪里坏了。最后这一点才是全部意义所在：前后端之间的偏差，从生产事故变成了编译错误。</p>

<h2 class="relative group">配置
    <div id="配置" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%85%8d%e7%bd%ae" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>先把它装成开发依赖，再装上生成的代码要import的那些包。要生成React Query hooks和mock的话，就是TanStack Query、MSW和Faker：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm install --save-dev orval msw @faker-js/faker
</span></span><span class="line"><span class="cl">npm install @tanstack/react-query</span></span></code></pre></div></div>
<p>然后在项目根目录加一个<code>orval.config.ts</code>。下面这份和Orval官方文档里的示例很接近：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;orval&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">petstore</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">input</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">target</span><span class="o">:</span> <span class="s1">&#39;./openapi.yaml&#39;</span><span class="p">,</span> <span class="c1">// a path or a URL
</span></span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nx">output</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">mode</span><span class="o">:</span> <span class="s1">&#39;single&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">target</span><span class="o">:</span> <span class="s1">&#39;./src/api/petstore.ts&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">schemas</span><span class="o">:</span> <span class="s1">&#39;./src/api/model&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">client</span><span class="o">:</span> <span class="s1">&#39;react-query&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">httpClient</span><span class="o">:</span> <span class="s1">&#39;fetch&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">mock</span>: <span class="kt">true</span><span class="p">,</span> <span class="c1">// MSW handlers plus Faker data
</span></span></span><span class="line"><span class="cl">      <span class="nx">override</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">fetch</span><span class="o">:</span> <span class="p">{</span> <span class="nx">includeHttpResponseReturnType</span>: <span class="kt">false</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p><code>client</code>决定你拿到什么：普通的<code>fetch</code>或<code>axios</code>函数，React Query、Vue Query、Svelte Query或SWR的hooks，Angular service，或者Zod schema。<code>includeHttpResponseReturnType: false</code>这一行让fetch客户端直接返回响应体，而不是<code>{ data, status }</code>，组件要的通常就是这个。</p>
<p>再加一条script，省得每个人都得记命令：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;scripts&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;api&#34;</span><span class="p">:</span> <span class="s2">&#34;orval&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p><code>npm run api</code>会生成hooks和模型。组件里用起来和别的React Query hook没什么两样，类型都已经填好了：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-tsx" data-lang="tsx"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">useListPets</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;./api/petstore&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kd">function</span> <span class="nx">PetList() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="p">{</span> <span class="nx">data</span>: <span class="kt">pets</span><span class="p">,</span> <span class="nx">isLoading</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">useListPets</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">isLoading</span><span class="p">)</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span><span class="nx">Loading</span><span class="err">…</span><span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;;</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span><span class="nx">pets</span><span class="o">?</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">pet</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">&lt;</span><span class="nt">li</span> <span class="na">key</span><span class="o">=</span><span class="p">{</span><span class="nx">pet</span><span class="p">.</span><span class="nx">id</span><span class="p">}&gt;{</span><span class="nx">pet</span><span class="p">.</span><span class="nx">name</span><span class="p">}&lt;/</span><span class="nt">li</span><span class="p">&gt;)}</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>在规范里给某个字段改个名，再跑一次<code>npm run api</code>，所有用到<code>pet.name</code>的地方都会变红。这就是上面说的偏差检查，而且跑一次不花任何成本。</p>

<h2 class="relative group">对着mock开发
    <div id="对着mock开发" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%af%b9%e7%9d%80mock%e5%bc%80%e5%8f%91" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>开了<code>mock: true</code>之后，Orval还会为每个接口生成一个MSW请求handler，返回按响应结构填充的Faker数据，外加一个把所有handler一次性返回的函数。把这份列表交给浏览器里MSW的<code>setupWorker</code>，或者测试里的<code>setupServer</code>，应用就跑在一个已经符合契约的假后端上了。每个handler还能传入覆盖值，测试想返回特定的payload、空列表或错误状态都行，不用有人手写fixture。</p>
<p>前端能从第一天就开工，靠的就是这一点。mock不是另一份需要有人维护的产物：它和客户端从同一份规范重新生成，所以不可能落后于规范。</p>

<h2 class="relative group">Orval vs openapi-generator、openapi-typescript和Hey API
    <div id="orval-vs-openapi-generatoropenapi-typescript和hey-api" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#orval-vs-openapi-generatoropenapi-typescript%e5%92%8chey-api" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>它们都从OpenAPI规范出发，只是走到的地方不一样。</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>生成什么</th>
					<th>最适合</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Orval</strong></td>
					<td>带类型的客户端函数或框架hooks（React Query、SWR、Vue、Svelte、Angular）、模型、MSW mock、Zod schema</td>
					<td>想用一份配置同时拿到hooks和mock的TypeScript前端</td>
			</tr>
			<tr>
					<td><strong>openapi-generator</strong></td>
					<td>几十种语言的客户端和服务端桩代码，包括TypeScript</td>
					<td>需要把同一份规范转成Java、Kotlin、Swift和TypeScript的团队</td>
			</tr>
			<tr>
					<td><strong>openapi-typescript</strong></td>
					<td>只生成类型，配合<code>openapi-fetch</code>得到一个轻量的带类型fetch封装</td>
					<td>只要类型，几乎不想要运行时代码</td>
			</tr>
			<tr>
					<td><strong>Hey API</strong>（<code>@hey-api/openapi-ts</code>）</td>
					<td>带类型的SDK，有TanStack Query、Zod等插件</td>
					<td>和Orval定位相近，围绕插件系统构建</td>
			</tr>
	</tbody>
</table>
<p>如果只需要类型，openapi-typescript最轻，很难挑出毛病。openapi-generator的用武之地，是规范要喂给不止一种语言的时候；它的TypeScript输出能用，但读起来像是先为Java写的。Orval和Hey API之间，基本就看口味，以及你更愿意读哪一家生成的代码。我一直用Orval，是因为mock和hooks出自同一次运行，而正是这一点改变了两个团队的协作方式。</p>
<p>这不是什么光鲜的工具。它只是安安静静地消灭了团队之间一整类争吵，这笔买卖我每次都愿意做。</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2024/2024-04-10-openapitools/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>API 设计优先（Design-First）工作流：每个 Sprint 先定好 OpenAPI 契约</title>
      <link>https://jared.lynskey.co.nz/zh-cn/posts/2023/2023-10-08-planning-an-api/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/zh-cn/posts/2023/2023-10-08-planning-an-api/</guid>
      <pubDate>Mon, 09 Oct 2023 00:30:03 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>API</category>
      <category>OpenAPI</category>
      <category>API Design</category>
      <category>Planning</category>
      <category>Mock Server</category>
      <description>我们每个 Sprint 跑一遍的 API 设计优先流程：前后端各出一名负责人敲定 OpenAPI 契约，CI 拉起模拟服务器，两边并行开发。附示例 YAML。</description>
      <content:encoded><![CDATA[<p>前不久我写过一篇<a href="https://jared.lynskey.co.nz/zh-cn/posts/2023/2023-10-08-open-api/" >用 OpenAPI 做 API 优先开发</a>的泛泛而谈，这篇是具体版：我们团队正在试行的计划工作流，外加配套的 YAML 示例。</p>
<p>简单说，这套设计优先的流程每个 Sprint 走一遍：</p>
<ol>
<li>为本 Sprint 的 API 工作定两名负责人，后端一名、前端一名。</li>
<li>两人对照本 Sprint 的需求编写（或更新）OpenAPI 接口定义，反复评审，直到双方都认可。</li>
<li>把接口定义提交到仓库。从这一刻起，它就是契约。</li>
<li>CI 根据提交的接口定义拉起一个模拟服务器。</li>
<li>前端对着模拟服务器开发，后端同时实现真实的端点。</li>
<li>真实服务部署后，由它生成的规范文件就是下一个 Sprint 接口定义的起点。</li>
</ol>

<h2 class="relative group">事情的起因
    <div id="事情的起因" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%ba%8b%e6%83%85%e7%9a%84%e8%b5%b7%e5%9b%a0" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>起因是一次回顾会。反馈说得很不客气：API 的规划几乎没有任何可见性。后端按自己对需求的理解去实现端点，前端事后才知道响应长什么样，分歧全都在集成阶段——也就是代价最高的时刻——才暴露出来。于是有了这个提案：把两边都拉进计划阶段，让 OpenAPI 接口定义（schema）成为一切的锚点。</p>

<h2 class="relative group">流程
    <div id="流程" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%b5%81%e7%a8%8b" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>每周选两名代表，后端一名、前端一名。他们的任务是吃透本次 Sprint 的需求，并让计划用的 OpenAPI 接口定义与之对齐。两人一起评审这份定义，把对不上的地方改掉。等这一步做完，在动手之前，双方就已经确切知道要造的是什么了。</p>
<p>修订定稿后，把接口定义提交到仓库，CI/CD 会根据这份 yaml 生成模拟 API。Sprint 结束时，再切一份覆盖下个 Sprint API 需求的新 yaml，循环往复。</p>

<h2 class="relative group">工具备忘
    <div id="工具备忘" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%b7%a5%e5%85%b7%e5%a4%87%e5%bf%98" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>可视化 yaml 用 <a href="https://editor.swagger.io/"  target="_blank" rel="noreferrer">Swagger Editor</a> 就够了：把文件传上去，就能看到渲染好的 Swagger 文档。</p>
<p>CI/CD 流水线需要能接收 yaml 并据此拉起一个模拟服务器。Stoplight 的 <a href="https://github.com/stoplightio/prism"  target="_blank" rel="noreferrer">Prism</a> 用的人很多，也支持 OpenAPI 3.1：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm install -g @stoplight/prism-cli
</span></span><span class="line"><span class="cl">prism mock openapi.yaml</span></span></code></pre></div></div>
<p>这样接口定义里的所有路径都会在 <code>http://127.0.0.1:4010</code> 上提供服务，返回的就是定义里的 <code>examples</code>，比如请求 <code>GET /lists/10</code>，拿到的就是附录里那条示例列表。它还会替你守住契约：示例声明了 API key，所以不带 <code>X-API-Key</code> 请求头的请求会直接收到 401，而这正是前端应该照着去做的行为。在容器里跑的话，记得加上 <code>-h 0.0.0.0</code>，外面才访问得到。<a href="https://github.com/muonsoft/openapi-mock"  target="_blank" rel="noreferrer">openapi-mock</a> 是个更轻量的替代品，会用随机数据填充响应。</p>
<p>拉起模拟服务器之前，最好先给接口定义做一遍 lint。契约写坏了，构建直接失败，总比让前端一头雾水强。<a href="https://github.com/stoplightio/spectral"  target="_blank" rel="noreferrer">Spectral</a> 只要一行规则集就能跑起来：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm install -g @stoplight/spectral-cli
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;extends: [&#34;spectral:oas&#34;]&#39;</span> &gt; .spectral.yaml
</span></span><span class="line"><span class="cl">spectral lint openapi.yaml</span></span></code></pre></div></div>
<p>最后，这份接口定义纯粹是计划产物。真实 API 部署之后，应该由各服务自己生成 openapi.json，下载下来再喂给下一轮计划。</p>

<h2 class="relative group">为什么值得费这个劲
    <div id="为什么值得费这个劲" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e5%80%bc%e5%be%97%e8%b4%b9%e8%bf%99%e4%b8%aa%e5%8a%b2" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>因为争论被挪到了代价最低的地方。写接口定义的时候前后端工程师都在场，误解还没变成代码就被抓住了。API 需求一次谈妥，不用在 Sprint 中途反复扯皮；有了模拟服务器，前端立刻就能开工，不用干等；关键细节被漏掉的机会也小得多——它们得同时躲过两双眼睛和一份白纸黑字的契约才行。</p>
<p>这里没有任何革命性的东西。只是把计划做得足够具体，具体到机器能把它当成模拟服务还给你——事实证明，一份计划需要的具体程度，恰好就是这个程度。</p>

<h2 class="relative group">附录A - yaml示例
    <div id="附录a---yaml示例" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%99%84%e5%bd%95a---yaml%e7%a4%ba%e4%be%8b" aria-label="锚点">#</a>
    </span>
    
</h2>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">openapi</span><span class="p">:</span><span class="w"> </span><span class="m">3.1.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">info</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">MyApp Sprint 3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Planning schema for sprint 3. Drives the mock server only.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="m">0.3.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">servers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">https://api-mock.example.com/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">tags</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">lists</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Saved lists feature (PROJ-123)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">security</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">api_key</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">/lists</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">get</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">tags</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">lists]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">summary</span><span class="p">:</span><span class="w"> </span><span class="l">Return the current user&#39;s saved lists</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">operationId</span><span class="p">:</span><span class="w"> </span><span class="l">getLists</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">responses</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">&#39;200&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">OK</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">content</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">application/json</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">array</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">items</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/List&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">/lists/{listId}:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">get</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">tags</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">lists]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">summary</span><span class="p">:</span><span class="w"> </span><span class="l">Find a list by ID</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">operationId</span><span class="p">:</span><span class="w"> </span><span class="l">getListById</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">parameters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">listId</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">in</span><span class="p">:</span><span class="w"> </span><span class="l">path</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">integer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">int64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">responses</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">&#39;200&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">OK</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">content</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">application/json</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/List&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">&#39;404&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">List not found</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">components</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">schemas</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">List</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">object</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">id, title]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">id</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">integer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">int64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">examples</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="m">10</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">title</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">examples</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">Weekend reading]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">itemCount</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">integer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">examples</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="m">7</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">securitySchemes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">api_key</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">apiKey</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">X-API-Key</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">in</span><span class="p">:</span><span class="w"> </span><span class="l">header</span></span></span></code></pre></div></div>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2023/2023-10-08-planning-an-api/featured.jpeg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>API 优先开发：用 OpenAPI 让前后端并行开发</title>
      <link>https://jared.lynskey.co.nz/zh-cn/posts/2023/2023-10-08-open-api/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/zh-cn/posts/2023/2023-10-08-open-api/</guid>
      <pubDate>Sun, 08 Oct 2023 00:30:03 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>OpenAPI</category>
      <category>backend</category>
      <category>frontend</category>
      <category>API design</category>
      <category>software development</category>
      <category>team collaboration</category>
      <description>API 优先开发的实际做法：先用 OpenAPI 把接口契约定下来，再起一个模拟服务器（mock server），前端就不用干等后端了。</description>
      <content:encoded><![CDATA[<p>我见过的最靠谱的进度杀手，不是什么高难度的工程问题，而是前端干坐着，等一个“马上就好”的接口。我们在公司最后定下的解法很无聊，但很管用：写代码之前，先用 OpenAPI 把接口契约谈妥。</p>

<h3 class="relative group">等待的游戏
    <div id="等待的游戏" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%ad%89%e5%be%85%e7%9a%84%e6%b8%b8%e6%88%8f" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>传统流程是串行的：后端先设计并实现 API，等接口真正能跑了，前端才开始对接。后端的每一次延误都会直接砸到前端的日程表上，而发布日期恰恰写在前端的日程表里。</p>

<h3 class="relative group">契约在前，代码在后
    <div id="契约在前代码在后" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%a5%91%e7%ba%a6%e5%9c%a8%e5%89%8d%e4%bb%a3%e7%a0%81%e5%9c%a8%e5%90%8e" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>按“接口定义优先”（schema-first）的流程，两边几乎可以同时起跑。在一行代码都不存在的时候，先把 API 的结构写进 OpenAPI 接口定义——每个端点、请求和响应模型、认证方式、错误码。这份文档就是前后端之间的合同。</p>
<p>回报来得很快。功能还没开工，所有人对它的理解就已经一致。后端还在实现，前端就能开始搭 UI。变更也转得更快，因为只有一份公认的事实来源需要更新。而且接口定义可以直接生成模拟服务器，在一个真实接口都没有的时候，前端测试就能开跑。</p>

<h3 class="relative group">实际怎么跑
    <div id="实际怎么跑" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%9e%e9%99%85%e6%80%8e%e4%b9%88%e8%b7%91" aria-label="锚点">#</a>
    </span>
    
</h3>
<ol>
<li>收集需求，跟做任何东西一样。</li>
<li>设计接口定义：端点、请求/响应模型、认证、错误码。</li>
<li>让前端和后端一起评审——契约的价值就体现在这一步。</li>
<li>生成模拟服务器（Swagger 和 Postman 都能做），前端先对着它集成。</li>
<li>后端照着同一份接口定义实现真实接口。</li>
<li>接口做好一个就替换一个模拟端点，持续测试，确保契约没被破坏。</li>
<li>保持沟通。接口定义是活文档，不是石碑。</li>
</ol>

<h3 class="relative group">要付的代价
    <div id="要付的代价" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%a6%81%e4%bb%98%e7%9a%84%e4%bb%a3%e4%bb%b7" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>这套流程不是免费的。提前写一份周全的接口定义要花实打实的时间。需求一变，得先改接口定义，涟漪会波及两个团队。另外每个人都得对 OpenAPI 和配套工具有基本的手感。</p>
<p>即便如此，我还没见过这笔前期投入输给它省下的集成混乱。前端第一天就能开工，而争论发生在文档上，而不是发生在挂掉的构建里。</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2023/2023-10-08-open-api/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
