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 อยู่แล้วแต่ยังไม่มีคำอธิบาย ลำดับคร่าว ๆ เป็นแบบนี้
- เขียนอธิบาย endpoint หนึ่งตัวด้วยมือ แค่นั้นก็พอจะเข้าใจโครงสร้าง
- เลือกเวอร์ชันที่เครื่องมือของคุณรับมือได้อย่างมั่นใจ ค่าเริ่มต้นคือ 3.1
- แยกไฟล์ด้วย
$refเมื่อมันยาวเกินหนึ่งหน้าจอ และตัดสินใจไปพร้อมกันเลยว่าจะส่งมันจากที่อยู่ใด - วางการทดสอบที่เทียบข้อกำหนดกับ router ทั้งสองทิศทาง ก่อนจะถึงขั้นนี้ คำพูดใด ๆ เรื่องเอกสารที่ทันสมัยล้วนเป็นคำสัญญา ไม่ใช่คุณสมบัติ
อยากเห็นว่าบน API จริงหน้าตาเป็นอย่างไร ดูได้ที่เอกสาร API ของ Lix.li ซึ่งครอบคลุมลิงก์ กลุ่ม การทดสอบ A/B และคอนเวอร์ชัน ส่วนบริการที่สร้างขึ้นรอบ API นั้นอยู่ที่หน้า API ย่อลิงก์ และหัวข้อใกล้เคียงมีบทวิเคราะห์เรื่องredirect 301, 302, 307, 308 กับ method ของคำขอ ซึ่งอธิบายว่าทำไม 301 ถึงเปลี่ยน POST ให้กลายเป็น GET เรื่องที่กระทบ endpoint แบบ postback ข้างต้นโดยตรง