OpenAPI คือรูปแบบสำหรับอธิบาย API แบบ HTTP ไว้ในไฟล์ YAML หรือ JSON ธรรมดา ไฟล์นั้นจะระบุว่า API เปิดเส้นทางใดบ้าง แต่ละเส้นทางรับ method อะไร request พกฟิลด์ใดไปบ้าง และ response ส่งอะไรกลับมา ไฟล์นี้ทั้งคนและโปรแกรมอ่านได้ เอกสาร ไลบรารีฝั่งไคลเอนต์ และเซิร์ฟเวอร์จำลองสำหรับทดสอบจึงสร้างขึ้นจากต้นทางเดียวกันได้ทั้งหมด

ชื่อทางการคือ OpenAPI Specification ย่อว่า OAS ผู้ดูแลคือ OpenAPI Initiative โครงการที่มีการกำกับดูแลแบบเปิดภายใต้ Linux Foundation

ต่อจากนี้: โครงสร้างของเอกสาร ความต่างระหว่าง OpenAPI กับ Swagger สิ่งที่เปลี่ยนไปในเวอร์ชัน 3.1 และ 3.2 และเหตุผลที่คำมั่นสำคัญที่สุดของรูปแบบนี้ — เอกสารที่ไม่มีวันล้าสมัย — ไม่เกิดขึ้นเองโดยลำพัง

เอกสาร OpenAPI หน้าตาเป็นอย่างไร

ไฟล์ที่ใช้งานได้จริงสั้นกว่าที่หลายคนคิด

openapi: 3.1.0

info:
  title: ลิงก์
  version: "1.0"

paths:
  /links:
    post:
      summary: สร้างลิงก์ย่อ
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
      responses:
        '201':
          description: สร้างลิงก์แล้ว

ฟิลด์บังคับในระดับบนสุดมีเพียงสองตัวคือ openapi และ info นอกจากนั้นเอกสารต้องมีอย่างน้อยหนึ่งใน paths, components หรือ webhooks ข้อกำหนด OAS 3.1.1 กำหนดไว้เช่นนั้น

ในไม่กี่บรรทัดแรกนี้มีกับดักซ่อนอยู่แล้ว: openapi กับ info.version เป็นคนละเรื่องกัน ตัวแรกคือเวอร์ชันของรูปแบบเอง ซึ่งเครื่องมือใช้ตัดสินว่าจะตีความเอกสารอย่างไร ตัวหลังคือเวอร์ชันของ API ของคุณ ข้อกำหนดระบุเรื่องนี้ไว้ตรงไปตรงมา แต่สัญชาตญาณกระซิบตรงข้าม จึงเกิดไฟล์ที่ประกาศว่า openapi: 1.0 ขึ้นมา

ข้อกำหนดขนาดใหญ่มักถูกแยกออกเป็นหลายไฟล์ด้วย $ref เอกสารหลักชี้ไปที่ ./paths/links.yaml และไฟล์นั้นชี้ต่อไปยัง ./schemas/link.yaml สะดวกดี และเป็นต้นเหตุของปัญหาหนึ่งที่ผมจะย้อนกลับมาพูดตอนท้าย

OpenAPI กับ Swagger ไม่ใช่สิ่งเดียวกัน

นี่คือความสับสนที่พบบ่อยที่สุดในหัวข้อนี้

คืออะไร ใครดูแล
OpenAPI ตัวมาตรฐาน นั่นคือรูปแบบ OpenAPI Initiative, Linux Foundation
Swagger ชุดเครื่องมือที่อ่านรูปแบบนั้น SmartBear

ความยุ่งเหยิงอธิบายได้ด้วยที่มา ตอนแรก Swagger หมายถึงทั้งข้อกำหนดและเครื่องมือพร้อมกัน ต่อมาข้อกำหนดถูกมอบให้ OpenAPI Initiative และเปลี่ยนชื่อเป็น OpenAPI Specification ส่วนชื่อ Swagger ยังอยู่กับเครื่องมือ ได้แก่ Swagger UI, Swagger Editor และตัวอื่น ๆ

ในทางปฏิบัติ "เราใช้ Swagger" แทบจะหมายถึง "เรามีไฟล์ OpenAPI แล้วแสดงผลด้วย Swagger UI" เสมอ ไม่มีอะไรผูกสองสิ่งนี้ไว้ด้วยกัน ไฟล์เดียวกันเปิดใน Redoc, Scalar หรือตัวสร้างไคลเอนต์ก็ทำงานได้ดีพอกัน

เวอร์ชัน: 3.0, 3.1 และ 3.2

ความต่างระหว่างสายไม่ใช่เรื่องผิวเผิน

3.0 ยังเป็นเวอร์ชันที่ใช้กันกว้างขวางที่สุด จุดอ่อนอยู่ที่ schema ของข้อมูล มันคล้าย JSON Schema แต่เข้ากันไม่ได้ทั้งหมด ทำให้การทำงานร่วมกันระหว่างเครื่องมือพังซ้ำแล้วซ้ำเล่า

3.1 ปิดช่องว่างนั้นลง ออบเจ็กต์ Schema กลายเป็นเซตใหญ่ที่ครอบ JSON Schema Draft 2020-12 ดังนั้น schema ที่คุณดูแลอยู่แล้วในรูป JSON Schema จึงนำกลับมาใช้ได้เลยแทนที่จะต้องเขียนใหม่ ฉบับแก้ไขปัจจุบันคือ 3.1.1 เผยแพร่เมื่อ 24 ตุลาคม 2024

3.2 ออกมาในเดือนกันยายน 2025 จากประกาศของ OpenAPI Initiative สิ่งที่เด่นที่สุดได้แก่

  • method query สำหรับการอ่านแบบ idempotent ที่พารามิเตอร์ใส่ลงใน URL ไม่พอ
  • additionalOperations สำหรับ method นอกมาตรฐาน
  • ชนิดสื่อแบบสตรีม ได้แก่ Server-Sent Events, JSON Lines และลำดับ JSON
  • tag ที่มีลำดับชั้น ผ่าน summary, parent และ kind
  • device flow ของ OAuth 2.0 สำหรับอุปกรณ์ที่ป้อนข้อมูลลำบาก

ควรเลือกเวอร์ชันตามที่เครื่องมือของคุณรองรับได้จริง ไม่ใช่ตามตัวเลขที่สูงที่สุด ระบบนิเวศรองรับ 3.1 อย่างแน่นหนาแล้ว ส่วน 3.2 ใหม่กว่าและเครื่องมือบางส่วนยังตามมาไม่ครบ ข้อกำหนดของเราเองประกาศไว้ที่ 3.1.0

ได้อะไรในทางปฏิบัติ

ไฟล์เดียวจัดการหลายเรื่องพร้อมกัน

  • เอกสารแบบโต้ตอบ — Swagger UI หรือ Redoc สร้างหน้าเว็บจากไฟล์ได้ตรง ๆ พร้อมแบบฟอร์มให้ลองยิง request
  • ไลบรารีฝั่งไคลเอนต์ — ตัวสร้างจะออก SDK ในภาษาที่ต้องการ ไม่ต้องเขียนตัวห่อหุ้มเอง
  • เซิร์ฟเวอร์จำลอง — ตั้ง mock ขึ้นจากคำอธิบายได้ก่อนที่ backend จะเสร็จ ฝั่งหน้าบ้านจึงไม่ต้องรอ
  • การทดสอบสัญญา — การทดสอบจะเทียบ response จริงกับ schema และจับได้ทันทีที่ response เริ่มไม่ตรงกับคำอธิบาย

ข้อโต้แย้งหลักที่สนับสนุน OpenAPI มาจากรายการนี้เอง นั่นคือเอกสารเลิกเป็นข้อความแยกต่างหากที่ใครบางคนลืมอัปเดต

ข้อโต้แย้งนั้นถูกต้อง เพียงแต่มันไม่ทำงานด้วยตัวเอง

ไม่มีอะไรเชื่อมข้อกำหนดเข้ากับโค้ด

นี่คือส่วนที่บทความแนว "OpenAPI คืออะไร" แทบไม่เคยแตะ และเป็นส่วนที่กัดเอาจริงบนระบบที่ใช้งานอยู่

ไฟล์ข้อกำหนดก็แค่ไฟล์หนึ่ง ตัว router ไม่รู้ว่ามันมีอยู่ controller ก็เช่นกัน คุณจะลบ endpoint ทิ้ง เพิ่มอันใหม่ หรือเปลี่ยนฟิลด์บังคับให้เป็นทางเลือกก็ได้ — ไม่มีการทดสอบสักตัวที่ล้ม เอกสารไม่ซิงก์ตัวเอง มันคงความถูกต้องไว้ได้พอดีกับช่วงเวลาที่ยังมีคนจำได้ว่าต้องแก้มัน

ความคลาดเคลื่อนเกิดได้ทั้งสองทาง และแต่ละทางสร้างความเสียหายคนละแบบ

  • เขียนไว้แต่ใช้ไม่ได้ นักพัฒนาอ่านเอกสาร ยิง request ไปแล้วได้ 404 กลับมา ความเชื่อมั่นในเอกสารส่วนที่เหลือพังลงทันที ถ้าตรงนี้ยังผิด แล้วตรงไหนอีก
  • ใช้ได้แต่ไม่ได้เขียนไว้ สำหรับคนที่กำลังตามหา ความสามารถนั้นเท่ากับไม่มีอยู่ ไม่มีใครเจอ ไม่มีใครจ่ายเงินให้มัน

กรณีหลังอันตรายกว่า เพราะไม่มีใครบ่น และก็ไม่มีใครจะบ่นได้ด้วย ผู้ใช้ไม่รู้เลยว่า endpoint นั้นมีอยู่

เราเจอเรื่องนี้กับตัวเอง API ของ Lix.li มี endpoint เกี่ยวกับคอนเวอร์ชันทำงานอยู่สามตัว ได้แก่ รายการเหตุการณ์หนึ่งตัว กับการรับ postback อีกสองตัวคือแบบเดี่ยวและแบบเป็นชุด แต่ไม่มีสักตัวปรากฏในข้อกำหนด โค้ดทำงานได้ การทดสอบผ่าน ขณะที่หน้าแนะนำ API กลับแสดงเส้นทางคนละอย่างสองเส้นซึ่งไม่มีอยู่ในโค้ดเลย เรื่องนี้โผล่ขึ้นมาตอนตรวจทานทั้งหมด ไม่ใช่จากการแจ้งข้อผิดพลาด

จับความคลาดเคลื่อนแบบอัตโนมัติ

เรื่องนี้แก้ด้วยวินัยไม่ได้ ต้องแก้ด้วยการทดสอบ แนวคิดตรงไปตรงมา คือดึงรายการเส้นทางจากข้อกำหนด ดึงรายการ route จาก router แล้วเทียบสองชุดนั้นทั้งสองทิศทาง

เส้นทางใน openapi.yaml   →   มี route นั้นไหม        →   ถ้าไม่มี เอกสารกำลังโกหก
route ใน router          →   เขียนไว้ในข้อกำหนดไหม   →   ถ้าไม่ ฟีเจอร์นั้นล่องหน

ฝั่ง "สิ่งที่เขียนไว้ใช้งานได้" ตรวจด้วย request จริงสะดวกที่สุด คือเรียก endpoint โดยไม่ใส่ข้อมูลยืนยันตัวตน แล้วดูว่าได้ 401 ไม่ใช่ 404 ความต่างตรงนี้สำคัญ 401 แปลว่าเจอ route แล้วและมันขอกุญแจ ส่วน 404 แปลว่าไม่มีเส้นทางนั้นอยู่เลย ระหว่างนั้นไม่มีการเขียนข้อมูลใด ๆ เพราะการยืนยันตัวตนทำงานก่อน controller

การตรวจฝั่งกลับกันอ่านจากการตั้งค่า route และยังบังคับให้คุณไล่รายการข้อยกเว้นออกมาอย่างชัดเจนด้วย นั่นคือ endpoint ภายในที่ไม่ควรอยู่ในสัญญาสาธารณะ รายการข้อยกเว้นนั้นมีค่าในตัวเอง เพราะมันบังคับให้ตัดสินใจสักครั้งอย่างตั้งใจว่าอะไรคือ API สาธารณะของคุณ

หลังเรื่องคอนเวอร์ชัน เราก็เขียนการทดสอบแบบนี้ขึ้นมา มันพิสูจน์คุณค่าทันที ถ้าเอาคอนเวอร์ชันออกจากข้อกำหนดอีกครั้ง การทดสอบจะล้มและเรียกชื่อเส้นทางที่หายไปทั้งสองเส้น

กับดักสองอย่างที่รู้ตัวช้า

$ref กับเส้นทางแบบสัมพัทธ์ เมื่อข้อกำหนดถูกแยกเป็นหลายไฟล์ การอ้างอิงภายในจะถูกแก้ไขโดยอิงกับที่อยู่ที่ไฟล์นั้นถูกส่งออกมา เอกสารหลักที่วางไว้ที่ /openapi จะไปหา ./paths/links.yaml ที่ /paths/links.yaml แล้วไม่เจออะไรเลย บนเบราว์เซอร์ทุกอย่างดูปกติ เพราะ Swagger UI โหลดไฟล์จากตำแหน่งจริงของมัน แต่ตัวสร้างไคลเอนต์ที่ชี้มาที่เอกสารเดียวกันจะสะดุด ทางเลือกคือส่งข้อกำหนดจากไดเรกทอรีของมันเองเท่านั้น หรือรวบให้เป็นไฟล์เดียวที่ไม่มี $ref ภายนอก

Swagger UI ว่างเปล่าในสายตาโปรแกรมเก็บข้อมูล หน้า Swagger UI คือมาร์กอัปไม่กี่บรรทัดบวกกับสคริปต์ที่วาดเนื้อหาขึ้นในเบราว์เซอร์ ใน HTML ที่ส่งออกมาไม่มีเส้นทาง endpoint แม้แต่เส้นเดียว สำหรับคนอ่านหน้านั้นปกติดี แต่สำหรับการจัดทำดัชนีมันคือหน้าขาว เติมความผิดพลาดที่พบบ่อยใน robots.txt เข้าไปอีก กฎ Disallow: /api/ ที่เขียนเพื่อซ่อน endpoint แบบ JSON จะซ่อนเอกสารสำหรับคนที่อยู่ใต้คำนำหน้าเดียวกันไปด้วย สิ่งที่ควรปิดคือเส้นทางที่ระบุเวอร์ชันแน่นอน เช่น /api/1.0/ ไม่ใช่ทั้งส่วน

เริ่มจากตรงไหน

ถ้ามี API อยู่แล้วแต่ยังไม่มีคำอธิบาย ลำดับคร่าว ๆ เป็นแบบนี้

  1. เขียนอธิบาย endpoint หนึ่งตัวด้วยมือ แค่นั้นก็พอจะเข้าใจโครงสร้าง
  2. เลือกเวอร์ชันที่เครื่องมือของคุณรับมือได้อย่างมั่นใจ ค่าเริ่มต้นคือ 3.1
  3. แยกไฟล์ด้วย $ref เมื่อมันยาวเกินหนึ่งหน้าจอ และตัดสินใจไปพร้อมกันเลยว่าจะส่งมันจากที่อยู่ใด
  4. วางการทดสอบที่เทียบข้อกำหนดกับ router ทั้งสองทิศทาง ก่อนจะถึงขั้นนี้ คำพูดใด ๆ เรื่องเอกสารที่ทันสมัยล้วนเป็นคำสัญญา ไม่ใช่คุณสมบัติ

อยากเห็นว่าบน API จริงหน้าตาเป็นอย่างไร ดูได้ที่เอกสาร API ของ Lix.li ซึ่งครอบคลุมลิงก์ กลุ่ม การทดสอบ A/B และคอนเวอร์ชัน ส่วนบริการที่สร้างขึ้นรอบ API นั้นอยู่ที่หน้า API ย่อลิงก์ และหัวข้อใกล้เคียงมีบทวิเคราะห์เรื่องredirect 301, 302, 307, 308 กับ method ของคำขอ ซึ่งอธิบายว่าทำไม 301 ถึงเปลี่ยน POST ให้กลายเป็น GET เรื่องที่กระทบ endpoint แบบ postback ข้างต้นโดยตรง