<?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>API Design · Nhân Tài Đức</title>
    <link>https://jared.lynskey.co.nz/vi/tags/api-design/</link>
    <description>Những câu chuyện ngắn về kỹ thuật phần mềm và lãnh đạo đội ngũ kỹ sư — performance review, tuyển dụng và mở rộng team, CI/CD, giám sát DevOps và AI agent. Viết từ Seoul.</description>
    <generator>Hugo</generator>
    <language>vi</language>
    <copyright>© 2026 Nhân Tài Đức</copyright>
    <lastBuildDate>Wed, 28 Oct 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/vi/tags/api-design/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Quy trình API design-first: lên kế hoạch hợp đồng OpenAPI cho từng sprint</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2023/2023-10-08-planning-an-api/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/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>Quy trình API design-first (thiết kế trước) mà đội tôi chạy mỗi sprint: hai người phụ trách backend và frontend chốt hợp đồng OpenAPI, CI dựng máy chủ giả lập, hai bên làm song song. Kèm YAML mẫu.</description>
      <content:encoded><![CDATA[<p>Cách đây không lâu, tôi có viết một bài nói chung chung về <a href="https://jared.lynskey.co.nz/vi/posts/2023/2023-10-08-open-api/" >phát triển API-first với OpenAPI</a>. Bài này thì cụ thể hơn: đây là quy trình lên kế hoạch mà đội tôi đang chạy thử, kèm theo tệp YAML mẫu.</p>
<p>Nói ngắn gọn, quy trình design-first (thiết kế API trước rồi mới viết mã) này lặp lại sau mỗi sprint như sau:</p>
<ol>
<li>Chọn hai người phụ trách phần API của sprint, một người bên backend và một người bên frontend.</li>
<li>Hai người cùng viết (hoặc cập nhật) bản đặc tả OpenAPI dựa trên yêu cầu của sprint, rồi rà soát cho đến khi cả hai bên đều đồng ý.</li>
<li>Commit bản đặc tả vào repo. Từ lúc này, nó chính là hợp đồng giữa hai bên.</li>
<li>CI dựng một máy chủ giả lập (mock server) từ bản đặc tả vừa commit.</li>
<li>Frontend làm việc với máy chủ giả lập, trong lúc backend viết các endpoint thật.</li>
<li>Khi dịch vụ thật đã được triển khai, bản đặc tả do chính dịch vụ đó sinh ra sẽ là điểm xuất phát cho bản đặc tả của sprint tiếp theo.</li>
</ol>

<h2 class="relative group">Chuyện này bắt nguồn từ đâu
    <div id="chuyện-này-bắt-nguồn-từ-đâu" 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="#chuy%e1%bb%87n-n%c3%a0y-b%e1%ba%aft-ngu%e1%bb%93n-t%e1%bb%ab-%c4%91%c3%a2u" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Mọi chuyện bắt đầu từ một buổi họp nhìn lại (retrospective). Góp ý hôm đó khá thẳng thắn: gần như không ai nắm được khâu lên kế hoạch API đang diễn ra thế nào. Backend xây endpoint theo cách hiểu yêu cầu của riêng mình, frontend chỉ biết phản hồi (response) của API có cấu trúc ra sao khi mọi thứ đã xong xuôi, và những chỗ vênh nhau chỉ lộ ra lúc tích hợp, đúng thời điểm sửa tốn kém nhất. Đề xuất đưa ra là kéo cả hai bên vào ngay từ giai đoạn lên kế hoạch, và lấy bản đặc tả OpenAPI (OpenAPI schema) làm trục chính cho mọi thứ xoay quanh.</p>

<h2 class="relative group">Quy trình
    <div id="quy-trình" 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="#quy-tr%c3%acnh" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Mỗi tuần, chúng tôi chọn ra hai đại diện, một người bên backend và một người bên frontend. Nhiệm vụ của họ là nắm rõ yêu cầu của sprint và chỉnh bản đặc tả OpenAPI dùng cho khâu lên kế hoạch sao cho khớp với yêu cầu đó. Hai người cùng rà soát bản đặc tả, sửa những chỗ chưa khớp, và đến cuối buổi thì cả hai bên đều biết chính xác sẽ xây cái gì, trước khi có ai bắt tay vào làm.</p>
<p>Khi các chỉnh sửa đã chốt xong, bản đặc tả được commit vào kho mã (repo), và quy trình CI/CD sẽ sinh ra API giả lập (mock API) từ tệp yaml đó. Hết sprint, chúng tôi tạo một tệp yaml mới cho yêu cầu API của sprint kế tiếp, và vòng lặp cứ thế tiếp tục.</p>

<h2 class="relative group">Ghi chú về công cụ
    <div id="ghi-chú-về-công-cụ" 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="#ghi-ch%c3%ba-v%e1%bb%81-c%c3%b4ng-c%e1%bb%a5" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Muốn xem trực quan tệp yaml thì <a href="https://editor.swagger.io/"  target="_blank" rel="noreferrer">Swagger Editor</a> là đủ: tải tệp lên là bạn sẽ thấy nó hiển thị thành một tài liệu Swagger đàng hoàng.</p>
<p>Quy trình CI/CD cần nhận vào một tệp yaml và dựng máy chủ giả lập từ đó. <a href="https://github.com/stoplightio/prism"  target="_blank" rel="noreferrer">Prism</a> của Stoplight là một lựa chọn được dùng rộng rãi, và hỗ trợ cả 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>Lệnh này phục vụ mọi đường dẫn (path) trong bản đặc tả tại <code>http://127.0.0.1:4010</code>, và trả về đúng các giá trị trong <code>examples</code> của bản đặc tả. Chẳng hạn, gọi <code>GET /lists/10</code> sẽ nhận được danh sách mẫu trong phần phụ lục. Prism cũng bắt tuân thủ hợp đồng: vì ví dụ có khai báo API key, request nào thiếu header <code>X-API-Key</code> sẽ nhận về mã 401, và đó chính là hành vi mà frontend cần xây dựng theo. Nếu chạy Prism trong container, hãy thêm <code>-h 0.0.0.0</code> để bên ngoài container truy cập được. <a href="https://github.com/muonsoft/openapi-mock"  target="_blank" rel="noreferrer">openapi-mock</a> là một lựa chọn gọn nhẹ hơn, tự điền dữ liệu ngẫu nhiên vào phản hồi.</p>
<p>Bạn cũng nên kiểm tra lỗi (lint) bản đặc tả trước khi dựng máy chủ giả lập. Như vậy, nếu hợp đồng bị viết sai thì bản build sẽ báo lỗi ngay, thay vì để frontend phải loay hoay không hiểu vì sao. <a href="https://github.com/stoplightio/spectral"  target="_blank" rel="noreferrer">Spectral</a> chỉ cần một tệp quy tắc (ruleset) dài đúng một dòng là chạy được:</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>Cuối cùng, bản đặc tả này chỉ phục vụ khâu lên kế hoạch. Khi API thật đã được triển khai, dịch vụ nên tự sinh ra openapi.json của riêng nó; bạn có thể tải tệp đó về rồi dùng lại cho vòng lên kế hoạch tiếp theo.</p>

<h2 class="relative group">Sao phải mất công như vậy
    <div id="sao-phải-mất-công-như-vậy" 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="#sao-ph%e1%ba%a3i-m%e1%ba%a5t-c%c3%b4ng-nh%c6%b0-v%e1%ba%ady" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Vì làm vậy thì các cuộc tranh cãi sẽ diễn ra ở nơi rẻ nhất có thể. Khi có một kỹ sư frontend và một kỹ sư backend cùng ngồi viết bản đặc tả, những chỗ hiểu lầm sẽ được phát hiện trước khi kịp biến thành mã. Yêu cầu API chỉ cần thống nhất một lần, thay vì phải thương lượng lại giữa sprint. Nhờ có máy chủ giả lập, frontend bắt tay vào làm được ngay chứ không phải ngồi chờ. Và số chi tiết quan trọng bị bỏ sót cũng ít hơn hẳn, vì muốn lọt được thì chúng phải qua mắt hai người, lại còn phải qua một bản hợp đồng giấy trắng mực đen.</p>
<p>Chẳng có gì mang tính cách mạng ở đây cả. Chỉ là kế hoạch được làm cụ thể đến mức máy có thể dựng lại nó thành bản giả lập cho bạn dùng. Và hóa ra, đó cũng chính là mức cụ thể mà một bản kế hoạch cần có.</p>

<h2 class="relative group">Phụ lục A - Ví dụ yaml
    <div id="phụ-lục-a---ví-dụ-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="#ph%e1%bb%a5-l%e1%bb%a5c-a---v%c3%ad-d%e1%bb%a5-yaml" aria-label="Cọc dấu">#</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>Phát triển API-first với OpenAPI: frontend và backend làm song song</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2023/2023-10-08-open-api/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/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>Phát triển theo hướng API-first trong thực tế: thống nhất hợp đồng API bằng OpenAPI ngay từ đầu, dựng một máy chủ giả lập (mock server), thế là frontend không còn phải ngồi chờ backend nữa.</description>
      <content:encoded><![CDATA[<p>Trong những dự án tôi từng làm, thứ làm trễ tiến độ đều đặn nhất không phải là những bài toán kỹ thuật khó, mà là cảnh frontend ngồi không chờ một endpoint &ldquo;sắp xong rồi&rdquo;. Cách chữa mà đội tôi ở công ty chốt lại vừa nhàm vừa hiệu quả: thống nhất hợp đồng API (API contract) bằng OpenAPI trước, rồi mới có ai bắt đầu viết mã.</p>

<h3 class="relative group">Cái vòng chờ đợi
    <div id="cái-vòng-chờ-đợi" 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="#c%c3%a1i-v%c3%b2ng-ch%e1%bb%9d-%c4%91%e1%bb%a3i" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Quy trình truyền thống thì chạy tuần tự. Backend thiết kế và xây API, và chỉ khi endpoint đã chạy được thật thì frontend mới bắt đầu tích hợp. Backend chậm ngày nào thì lịch của frontend lãnh đủ ngày đó, mà ngày phát hành thường lại phụ thuộc vào lịch của frontend.</p>

<h3 class="relative group">Hợp đồng trước, mã nguồn sau
    <div id="hợp-đồng-trước-mã-nguồn-sau" 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="#h%e1%bb%a3p-%c4%91%e1%bb%93ng-tr%c6%b0%e1%bb%9bc-m%c3%a3-ngu%e1%bb%93n-sau" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Với quy trình đặc tả trước (schema-first), hai bên bắt đầu gần như cùng lúc. Trước khi có dòng mã nào, toàn bộ cấu trúc của API được đưa vào một bản đặc tả OpenAPI (OpenAPI schema): mọi endpoint, mô hình dữ liệu của yêu cầu và phản hồi, phương thức xác thực và mã lỗi. Tài liệu đó trở thành bản hợp đồng giữa frontend và backend.</p>
<p>Lợi ích thấy ngay. Trước khi tính năng được xây, ai cũng hiểu nó theo cùng một cách. Frontend có thể dựng giao diện trong lúc backend vẫn đang hiện thực các endpoint. Khi có thay đổi thì xử lý cũng nhanh hơn, vì chỉ có một nguồn thông tin chuẩn đã được thống nhất cần cập nhật. Và bạn có thể sinh máy chủ giả lập (mock server) thẳng từ bản đặc tả, nên frontend bắt đầu kiểm thử được ngay cả khi chưa có endpoint thật nào.</p>

<h3 class="relative group">Áp dụng thực tế ra sao
    <div id="áp-dụng-thực-tế-ra-sao" 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="#%c3%a1p-d%e1%bb%a5ng-th%e1%bb%b1c-t%e1%ba%bf-ra-sao" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<ol>
<li>Thu thập yêu cầu, như với bất kỳ việc gì khác.</li>
<li>Thiết kế bản đặc tả: endpoint, mô hình yêu cầu/phản hồi, xác thực, mã lỗi.</li>
<li>Cho cả frontend lẫn backend cùng rà soát. Đây là lúc bản hợp đồng thể hiện rõ giá trị của nó.</li>
<li>Sinh máy chủ giả lập (Swagger và Postman đều làm được) và để frontend tích hợp với bản giả lập đó.</li>
<li>Backend hiện thực các endpoint thật theo đúng bản đặc tả.</li>
<li>Khi endpoint thật lần lượt xong thì thay dần bản giả lập bằng endpoint thật, đồng thời kiểm thử liên tục để chắc chắn hai bên vẫn làm đúng hợp đồng.</li>
<li>Tiếp tục trao đổi với nhau. Bản đặc tả là tài liệu sống, chứ không phải thứ khắc lên bia đá.</li>
</ol>

<h3 class="relative group">Những điểm vướng
    <div id="những-điểm-vướng" 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="#nh%e1%bb%afng-%c4%91i%e1%bb%83m-v%c6%b0%e1%bb%9bng" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Tất nhiên cách này cũng có cái giá của nó. Viết một bản đặc tả kỹ lưỡng ngay từ đầu tốn khá nhiều thời gian. Khi yêu cầu thay đổi thì bản đặc tả phải đổi trước, và ảnh hưởng sẽ lan sang cả hai đội. Ngoài ra, ai cũng cần biết dùng OpenAPI và các công cụ đi kèm ở mức cơ bản.</p>
<p>Dù vậy, tôi chưa từng thấy trường hợp nào mà công sức bỏ ra lúc đầu lại tốn hơn mớ hỗn loạn lúc tích hợp mà nó giúp tránh được. Frontend bắt tay vào làm từ ngày đầu tiên, còn các cuộc tranh cãi thì diễn ra trên một bản tài liệu, chứ không phải trên những bản dựng hỏng.</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>
