OpenAPI là định dạng mô tả một API HTTP trong một tệp YAML hoặc JSON thông thường. Tệp đó liệt kê API công bố những đường dẫn nào, mỗi đường dẫn nhận phương thức gì, request mang theo những trường nào và response trả về những gì. Cả người lẫn máy đều đọc được tệp này, nhờ vậy tài liệu, thư viện client và máy chủ giả lập dùng cho kiểm thử đều sinh ra từ cùng một nguồn.

Tên chính thức là OpenAPI Specification, viết tắt OAS. Đơn vị duy trì là OpenAPI Initiative, một dự án quản trị mở thuộc Linux Foundation.

Phần tiếp theo: tài liệu được dựng ra sao, OpenAPI khác Swagger ở chỗ nào, phiên bản 3.1 và 3.2 thay đổi những gì, và vì sao lời hứa cốt lõi của định dạng này — tài liệu không bao giờ lỗi thời — tự nó không thành hiện thực.

Một tài liệu OpenAPI trông thế nào

Tệp chạy được ngắn hơn nhiều người hình dung:

openapi: 3.1.0

info:
  title: Liên kết
  version: "1.0"

paths:
  /links:
    post:
      summary: Tạo liên kết rút gọn
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
      responses:
        '201':
          description: Đã tạo liên kết

Ở cấp cao nhất chỉ có hai trường bắt buộc là openapiinfo; ngoài ra tài liệu phải chứa ít nhất một trong paths, components hoặc webhooks. Đặc tả OAS 3.1.1 quy định như vậy.

Ngay trong mấy dòng đầu đã có một cái bẫy: openapiinfo.version là hai thứ khác nhau. Cái thứ nhất là phiên bản của chính định dạng, công cụ dựa vào đó để biết cách diễn giải tài liệu. Cái thứ hai là phiên bản API của bạn. Đặc tả nói rõ điều này, nhưng trực giác lại mách ngược, nên mới sinh ra những tệp khai báo openapi: 1.0.

Đặc tả lớn thường được tách thành nhiều tệp bằng $ref: tài liệu gốc trỏ tới ./paths/links.yaml, tệp này lại trỏ tới ./schemas/link.yaml. Tiện lợi, và cũng là nguồn gốc của một vấn đề tôi sẽ quay lại ở cuối bài.

OpenAPI và Swagger không phải một

Đây là nhầm lẫn phổ biến nhất trong chủ đề này.

Là gì Ai duy trì
OpenAPI tiêu chuẩn, tức bản thân định dạng OpenAPI Initiative, Linux Foundation
Swagger bộ công cụ đọc định dạng đó SmartBear

Lịch sử giải thích mớ rối này. Ban đầu Swagger vừa chỉ đặc tả vừa chỉ công cụ. Về sau đặc tả được chuyển cho OpenAPI Initiative và đổi tên thành OpenAPI Specification, còn cái tên Swagger ở lại với các công cụ: Swagger UI, Swagger Editor và những thứ khác.

Trên thực tế, "bên mình dùng Swagger" gần như luôn có nghĩa "bên mình có tệp OpenAPI và hiển thị nó bằng Swagger UI". Không có ràng buộc nào giữa hai thứ: cùng tệp ấy mở trong Redoc, Scalar hay một trình sinh client đều chạy tốt như nhau.

Phiên bản: 3.0, 3.1 và 3.2

Khác biệt giữa các nhánh không nằm ở hình thức.

3.0 vẫn được triển khai rộng nhất. Điểm yếu của nó là schema dữ liệu: trông giống JSON Schema nhưng không tương thích hoàn toàn, khiến khả năng phối hợp giữa các công cụ liên tục trục trặc.

3.1 đã lấp khoảng trống đó. Đối tượng Schema trở thành tập cha của JSON Schema Draft 2020-12, nên các schema bạn đang duy trì bằng JSON Schema dùng lại được thay vì phải viết lại. Bản hiệu chỉnh hiện hành 3.1.1 công bố ngày 24 tháng 10 năm 2024.

3.2 ra mắt tháng 9 năm 2025. Theo thông báo của OpenAPI Initiative, đáng chú ý nhất là:

  • phương thức HTTP query, dành cho các truy vấn idempotent mà tham số không nhét vừa URL;
  • additionalOperations cho những phương thức ngoài chuẩn;
  • các kiểu media dạng luồng: Server-Sent Events, JSON Lines, chuỗi JSON;
  • thẻ có phân cấp, qua summary, parentkind;
  • luồng thiết bị của OAuth 2.0, dành cho máy móc khó nhập liệu.

Nên chọn phiên bản mà bộ công cụ của bạn thực sự hỗ trợ, chứ không phải con số lớn nhất. Hệ sinh thái hỗ trợ 3.1 khá vững; 3.2 mới hơn và một phần chuỗi công cụ vẫn đang theo kịp. Đặc tả của chúng tôi khai báo 3.1.0.

Nó mang lại gì trên thực tế

Một tệp giải quyết cùng lúc nhiều việc:

  • tài liệu tương tác — Swagger UI hoặc Redoc dựng trang thẳng từ tệp, kèm biểu mẫu để thử request;
  • thư viện client — trình sinh mã xuất ra SDK theo ngôn ngữ bạn cần, khỏi phải viết lớp bọc bằng tay;
  • máy chủ giả lập — dựng được mock từ bản mô tả trước khi backend tồn tại, frontend không phải chờ;
  • kiểm thử hợp đồng — bài kiểm thử đối chiếu response thật với schema và bắt được đúng lúc response bắt đầu lệch khỏi mô tả.

Lập luận chính ủng hộ OpenAPI xuất phát từ danh sách này: tài liệu thôi là một văn bản riêng mà ai đó quên cập nhật.

Lập luận ấy đúng. Chỉ có điều nó không tự vận hành.

Không có gì nối đặc tả với mã nguồn

Đây là phần mà các bài "OpenAPI là gì" gần như không nhắc tới, và là phần cắn người ta trên môi trường thật.

Tệp đặc tả chỉ là một tệp. Router không biết nó tồn tại. Controller cũng vậy. Bạn có thể xóa một endpoint, thêm một endpoint, đổi trường bắt buộc thành tùy chọn — không một bài kiểm thử nào hỏng. Tài liệu không tự đồng bộ; nó chính xác đúng chừng nào còn có người nhớ sửa nó.

Sự lệch pha xảy ra theo cả hai chiều, và mỗi chiều gây hại một kiểu:

  • có ghi nhưng không chạy. Lập trình viên đọc tài liệu, gửi request và nhận về 404. Niềm tin vào phần còn lại của tài liệu sụp ngay tại chỗ: chỗ này đã sai thì chỗ nào còn sai nữa?
  • chạy nhưng không ghi. Với người đang tìm nó, tính năng đó đơn giản là không tồn tại. Không ai tìm thấy, không ai trả tiền cho nó.

Trường hợp thứ hai nguy hiểm hơn, vì chẳng ai phàn nàn. Cũng không có ai để phàn nàn: người dùng đâu biết endpoint đó có tồn tại.

Chúng tôi đã dính đúng chuyện này. API của Lix.li có ba endpoint về chuyển đổi đang chạy bình thường — một danh sách sự kiện, cộng với nhận postback theo từng cái và theo lô — mà không cái nào xuất hiện trong đặc tả. Mã chạy tốt, kiểm thử vẫn xanh, trong khi trang giới thiệu API lại trưng ra hai đường dẫn hoàn toàn khác, vốn không hề có trong mã. Chuyện này lộ ra trong một đợt rà soát toàn bộ, chứ không phải từ báo lỗi.

Bắt lệch pha một cách tự động

Việc này không giải quyết bằng kỷ luật mà bằng một bài kiểm thử. Ý tưởng rất thẳng: lấy danh sách đường dẫn từ đặc tả, lấy danh sách route từ router, rồi so hai tập hợp theo cả hai chiều.

đường dẫn trong openapi.yaml   →   route có tồn tại không?   →   nếu không, tài liệu nói dối
route trong router             →   đã được mô tả chưa?       →   nếu chưa, tính năng vô hình

Nửa "cái được ghi thì chạy" kiểm bằng một request thật là tiện nhất: gọi endpoint không kèm thông tin xác thực và xác nhận phản hồi là 401 chứ không phải 404. Phân biệt này quan trọng. 401 nghĩa là đã tìm thấy route và nó đòi khóa; 404 nghĩa là không có đường dẫn ấy. Quá trình này không ghi gì cả, vì khâu xác thực chạy trước controller.

Chiều ngược lại thì đọc cấu hình route. Nó còn buộc bạn liệt kê ngoại lệ một cách tường minh — những endpoint nội bộ chẳng có lý do gì nằm trong hợp đồng công khai. Bản danh sách ngoại lệ ấy tự nó đã đáng giá: nó bắt bạn quyết định một lần cho dứt khoát, đâu mới là API công khai của mình.

Sau vụ chuyển đổi, chúng tôi viết đúng bài kiểm thử này. Nó chứng minh giá trị ngay lập tức: nếu gỡ phần chuyển đổi khỏi đặc tả một lần nữa, bài kiểm thử hỏng và gọi tên từng đường dẫn còn thiếu.

Hai cái bẫy phát hiện muộn

$ref và đường dẫn tương đối. Khi đặc tả bị tách thành nhiều tệp, các tham chiếu bên trong được phân giải tương đối với địa chỉ mà tệp được phục vụ. Tài liệu gốc đặt tại /openapi sẽ đi tìm ./paths/links.yaml/paths/links.yaml và không thấy gì. Trên trình duyệt mọi thứ trông ổn, vì Swagger UI tải tệp từ vị trí thật của nó — nhưng một trình sinh client trỏ vào cùng tài liệu ấy sẽ vấp. Hoặc bạn chỉ phục vụ đặc tả từ đúng thư mục của nó, hoặc gói nó thành một tệp duy nhất không còn $ref ngoài.

Swagger UI rỗng đối với trình thu thập dữ liệu. Trang Swagger UI chỉ gồm vài dòng đánh dấu cộng một script vẽ nội dung ngay trên trình duyệt. Trong HTML được phục vụ không có lấy một đường dẫn endpoint nào. Với người đọc thì trang vẫn bình thường; với việc lập chỉ mục thì nó trắng trơn. Cộng thêm một lỗi hay gặp trong robots.txt: quy tắc Disallow: /api/ viết ra để giấu các endpoint JSON lại giấu luôn phần tài liệu dành cho người nằm dưới cùng tiền tố. Thứ cần chặn là đường dẫn chính xác có số phiên bản — chẳng hạn /api/1.0/ — chứ không phải cả khu.

Bắt đầu từ đâu

Nếu API đã có mà bản mô tả thì chưa, trình tự đại khái như sau:

  1. Mô tả tay một endpoint. Chừng đó đủ để nắm cấu trúc.
  2. Chọn phiên bản mà bộ công cụ của bạn xử lý chắc tay; mặc định là 3.1.
  3. Tách tệp bằng $ref khi nó không còn vừa một màn hình, và chốt ngay nó được phục vụ từ địa chỉ nào.
  4. Đặt một bài kiểm thử đối chiếu đặc tả với router theo cả hai chiều. Trước bước này, mọi khẳng định về tài liệu luôn mới đều là lời hứa, không phải thuộc tính.

Để xem nó ra sao trên một API thật, tài liệu API của Lix.li bao gồm liên kết, nhóm, kiểm thử A/B và chuyển đổi. Dịch vụ dựng quanh API đó được trình bày ở trang API rút gọn liên kết. Hai chủ đề lân cận đáng tìm hiểu riêng: webhook, nếu bạn muốn nhận sự kiện thay vì đi hỏi, và khác biệt giữa các redirect 301, 302, 307 và 308 — bởi 301 biến POST thành GET, điều này chạm thẳng vào những endpoint postback nhắc ở trên.