REST API ควรใช้ลิงก์ไฮเปอร์มีเดีย HATEOAS หรือไม่?
สรุปสั้นๆ
HATEOAS (Hypermedia as the Engine of Application State) มีความสง่างามตามทฤษฎี แต่ซับซ้อนในทางปฏิบัติ API ส่วนใหญ่ข้ามการใช้งาน HATEOAS เต็มรูปแบบ และใช้ลิงก์ไฮเปอร์มีเดียแบบเลือกเพื่อการแบ่งหน้า (pagination) ทรัพยากรที่เกี่ยวข้อง (related resources) และการดำเนินการ (actions) Modern PetstoreAPI ได้นำลิงก์ไฮเปอร์มีเดียที่ใช้งานได้จริงมาใช้ โดยไม่บังคับให้ไคลเอ็นต์ต้องขับเคลื่อนด้วยไฮเปอร์มีเดียทั้งหมด
บทนำ
คุณกำลังอ่านเกี่ยวกับการออกแบบ REST API คุณได้พบกับ HATEOAS (Hypermedia as the Engine of Application State) คำอธิบายระบุว่า: “ไคลเอ็นต์ควรค้นพบการดำเนินการทั้งหมดผ่านลิงก์ไฮเปอร์มีเดีย ไม่ใช่การฮาร์ดโค้ด URL”
คุณคิดว่า: “ฟังดูซับซ้อนนะ มีใครทำแบบนี้จริงๆ หรือเปล่า?”
คำตอบคือ: ไม่ค่อยมีใครทำ HATEOAS เป็นข้อจำกัด REST ที่ถูกละเลยมากที่สุด Roy Fielding (ผู้คิดค้น REST) กล่าวว่ามันเป็นสิ่งสำคัญ นักออกแบบ API ส่วนใหญ่กล่าวว่ามันไม่สามารถทำได้จริง ผลลัพธ์คือ: API ที่เรียกว่า “REST” ส่วนใหญ่ไม่ได้เป็น RESTful อย่างแท้จริงตามคำจำกัดความของ Fielding
Modern PetstoreAPI ใช้วิธีการที่เน้นการใช้งานจริง: ใช้ลิงก์ไฮเปอร์มีเดียในจุดที่เพิ่มคุณค่า (การแบ่งหน้า, ทรัพยากรที่เกี่ยวข้อง, การดำเนินการ) แต่ไม่บังคับให้ไคลเอ็นต์ต้องขับเคลื่อนด้วยไฮเปอร์มีเดียทั้งหมด
ในคู่มือนี้ คุณจะได้เรียนรู้ว่า HATEOAS คืออะไร ทำไมจึงเป็นที่ถกเถียง และวิธีนำลิงก์ไฮเปอร์มีเดียที่ใช้งานได้จริงมาใช้โดยใช้ Modern PetstoreAPI เป็นข้อมูลอ้างอิง
HATEOAS คืออะไร?
HATEOAS เป็นข้อจำกัดของ REST ที่ระบุว่าไคลเอ็นต์ควรค้นพบความสามารถของ API ผ่านลิงก์ไฮเปอร์มีเดีย ไม่ใช่จากเอกสาร
แนวคิด
แทนที่จะฮาร์ดโค้ด URL:
// Client hardcodes URLs
const response = await fetch('https://petstoreapi.com/v1/pets/123');
const pet = await response.json();
// Client knows the URL structure
await fetch(`https://petstoreapi.com/v1/pets/${pet.id}/orders`);
ไคลเอ็นต์ติดตามลิงก์จากผลตอบรับ:
// Client starts at root
const root = await fetch('https://petstoreapi.com/v1');
const rootData = await root.json();
// Client follows link to pets
const petsUrl = rootData._links.pets.href;
const pets = await fetch(petsUrl);
const petsData = await pets.json();
// Client follows link to specific pet
const petUrl = petsData._links.self.href;
const pet = await fetch(petUrl);
const petData = await pet.json();
// Client follows link to orders
const ordersUrl = petData._links.orders.href;
const orders = await fetch(ordersUrl);
ทฤษฎี
ด้วย HATEOAS:
1. ไคลเอ็นต์ไม่ฮาร์ดโค้ด URL
URL สามารถเปลี่ยนแปลงได้โดยไม่ทำให้ไคลเอ็นต์เสียหาย เซิร์ฟเวอร์เป็นผู้ควบคุมโครงสร้าง URL
2. ไคลเอ็นต์ค้นพบความสามารถ
หากมีลิงก์อยู่ แสดงว่าการดำเนินการนั้นพร้อมใช้งาน หากไม่มี แสดงว่าไม่พร้อมใช้งาน (หรือไม่ได้รับอนุญาตสำหรับผู้ใช้นี้)
3. API สามารถอธิบายตัวเองได้
ไคลเอ็นต์สำรวจ API โดยการติดตามลิงก์ เหมือนกับการท่องเว็บไซต์
ตัวอย่าง: การตอบสนอง HATEOAS แบบเต็มรูปแบบ
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Fluffy",
"species": "CAT",
"status": "AVAILABLE",
"_links": {
"self": {
"href": "https://petstoreapi.com/v1/pets/019b4132-70aa-764f-b315-e2803d882a24"
},
"update": {
"href": "https://petstoreapi.com/v1/pets/019b4132-70aa-764f-b315-e2803d882a24",
"method": "PUT"
},
"delete": {
"href": "https://petstoreapi.com/v1/pets/019b4132-70aa-764f-b315-e2803d882a24",
"method": "DELETE"
},
"orders": {
"href": "https://petstoreapi.com/v1/pets/019b4132-70aa-764f-b315-e2803d882a24/orders"
},
"adopt": {
"href": "https://petstoreapi.com/v1/pets/019b4132-70aa-764f-b315-e2803d882a24/adopt",
"method": "POST"
}
}
}
ไคลเอ็นต์ไม่จำเป็นต้องรู้รูปแบบ URL เพียงแค่ติดตามลิงก์
ข้อถกเถียงเรื่อง HATEOAS
HATEOAS เป็นที่ถกเถียงกันเพราะทฤษฎีและการปฏิบัติแตกต่างกัน
ข้อโต้แย้งที่สนับสนุน HATEOAS
1. การเชื่อมโยงอย่างหลวมๆ
ไคลเอ็นต์ไม่ขึ้นอยู่กับโครงสร้าง URL เซิร์ฟเวอร์สามารถเปลี่ยน URL ได้โดยไม่ทำให้ไคลเอ็นต์เสียหาย
2. การค้นพบได้
ไคลเอ็นต์สามารถสำรวจ API ได้โดยไม่ต้องอ่านเอกสาร
3. การดำเนินการที่ขับเคลื่อนด้วยสถานะ
ลิงก์แสดงการดำเนินการที่พร้อมใช้งาน หากสัตว์เลี้ยงถูกรับเลี้ยงไปแล้ว ลิงก์ “adopt” จะหายไป
4. REST ที่แท้จริง
Roy Fielding กล่าวว่า HATEOAS เป็นสิ่งสำคัญสำหรับ REST หากไม่มี HATEOAS คุณก็ไม่ได้กำลังทำ REST
ข้อโต้แย้งที่คัดค้าน HATEOAS
1. ความซับซ้อน
ไคลเอ็นต์ต้องการตรรกะการแยกวิเคราะห์ไฮเปอร์มีเดีย ไคลเอ็นต์ HTTP แบบง่ายจะกลายเป็นเครื่องสถานะที่ซับซ้อน
2. ประสิทธิภาพ
ไคลเอ็นต์ส่งคำขอหลายครั้งเพื่อค้นหา URL การเข้าถึง URL โดยตรงเร็วกว่า
3. ความยากในการดีบัก
การติดตามลิงก์ทำให้การดีบักยากขึ้น คุณไม่สามารถแค่ curl URL ได้ — คุณต้องติดตามสายโซ่ของลิงก์
4. เครื่องมือที่ไม่ดี
ไคลเอ็นต์ HTTP, เครื่องมือทดสอบ และเครื่องมือสร้างเอกสารส่วนใหญ่สันนิษฐานว่าใช้ URL ที่ฮาร์ดโค้ด
5. ไม่มีใครทำมัน
GitHub, Stripe, Twilio, Twitter — API หลักๆ ไม่ได้ใช้ HATEOAS เต็มรูปแบบ ถ้าพวกเขาไม่ต้องการมัน แล้วคุณล่ะ?
ความเป็นจริง
API ส่วนใหญ่กล่าวอ้างว่าเป็น “REST” แต่ข้าม HATEOAS ไป แท้จริงแล้วเป็น “HTTP API” หรือ “REST-like API” REST ที่แท้จริง (พร้อม HATEOAS) นั้นหาได้ยาก
ลิงก์ไฮเปอร์มีเดียที่ใช้งานได้จริง
แทนที่จะใช้ HATEOAS เต็มรูปแบบ ให้ใช้ลิงก์ไฮเปอร์มีเดียในจุดที่เพิ่มคุณค่า
1. ลิงก์การแบ่งหน้า (Pagination Links)
ปัญหา: ไคลเอ็นต์ต้องสร้าง URL สำหรับการแบ่งหน้า
วิธีแก้ไข: จัดเตรียมลิงก์ถัดไป/ก่อนหน้า
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"totalPages": 10
},
"links": {
"self": "https://petstoreapi.com/v1/pets?page=2&limit=20",
"first": "https://petstoreapi.com/v1/pets?page=1&limit=20",
"prev": "https://petstoreapi.com/v1/pets?page=1&limit=20",
"next": "https://petstoreapi.com/v1/pets?page=3&limit=20",
"last": "https://petstoreapi.com/v1/pets?page=10&limit=20"
}
}
ประโยชน์: ไคลเอ็นต์ไม่ต้องสร้าง URL สำหรับการแบ่งหน้า พวกเขาเพียงแค่ติดตามลิงก์
2. ลิงก์ทรัพยากรที่เกี่ยวข้อง (Related Resource Links)
ปัญหา: ไคลเอ็นต์ต้องทราบรูปแบบ URL สำหรับทรัพยากรที่เกี่ยวข้อง
วิธีแก้ไข: จัดเตรียมลิงก์ไปยังทรัพยากรที่เกี่ยวข้อง
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Fluffy",
"_links": {
"self": "https://petstoreapi.com/v1/pets/019b4132-70aa-764f-b315-e2803d882a24",
"orders": "https://petstoreapi.com/v1/pets/019b4132-70aa-764f-b315-e2803d882a24/orders",
"owner": "https://petstoreapi.com/v1/users/019b4127-54d5-76d9-b626-0d4c7bfce5b6"
}
}
ประโยชน์: ไคลเอ็นต์ค้นพบทรัพยากรที่เกี่ยวข้องได้โดยไม่ต้องมีเอกสาร
3. ลิงก์การดำเนินการ (Action Links)
ปัญหา: ไคลเอ็นต์ต้องทราบว่ามีดำเนินการใดบ้างที่พร้อมใช้งาน
วิธีแก้ไข: จัดเตรียมลิงก์สำหรับการดำเนินการที่พร้อมใช้งาน
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"status": "AVAILABLE",
"_links": {
"adopt": {
"href": "https://petstoreapi.com/v1/pets/019b4132-70aa-764f-b315-e2803d882a24/adopt",
"method": "POST"
}
}
}
หากสัตว์เลี้ยงถูกรับเลี้ยงไปแล้ว:
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"status": "ADOPTED",
"_links": {
// No "adopt" link - action not available
}
}
ประโยชน์: ลิงก์ระบุการดำเนินการที่พร้อมใช้งานตามสถานะ
4. การแบ่งหน้าแบบ Cursor-Based
ปัญหา: ไคลเอ็นต์ต้องสร้าง URL แบบ cursor
วิธีแก้ไข: จัดเตรียม URL ถัดไป/ก่อนหน้าแบบทึบ (opaque)
{
"data": [...],
"links": {
"next": "https://petstoreapi.com/v1/pets?cursor=eyJpZCI6IjAxOWI0MTMyIn0"
}
}
ประโยชน์: ไคลเอ็นต์ไม่แยกวิเคราะห์ cursor พวกเขาเพียงแค่ติดตามลิงก์
Modern PetstoreAPI ใช้ไฮเปอร์มีเดียอย่างไร
Modern PetstoreAPI ใช้ลิงก์ไฮเปอร์มีเดียแบบเลือก
ลิงก์การแบ่งหน้า
ปลายทางคอลเลกชันทั้งหมดมีลิงก์การแบ่งหน้า:
GET /v1/pets?limit=20
{
"data": [...],
"pagination": {
"limit": 20,
"hasMore": true
},
"links": {
"self": "https://petstoreapi.com/v1/pets?limit=20",
"next": "https://petstoreapi.com/v1/pets?cursor=eyJpZCI6IjAxOWI0MTMyIn0&limit=20"
}
}
ลิงก์ทรัพยากรที่เกี่ยวข้อง
ผลตอบรับของทรัพยากรประกอบด้วยลิงก์ไปยังทรัพยากรที่เกี่ยวข้อง:
GET /v1/pets/019b4132-70aa-764f-b315-e2803d882a24
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Fluffy",
"ownerId": "019b4127-54d5-76d9-b626-0d4c7bfce5b6",
"_links": {
"self": "https://petstoreapi.com/v1/pets/019b4132-70aa-764f-b315-e2803d882a24",
"owner": "https://petstoreapi.com/v1/users/019b4127-54d5-76d9-b626-0d4c7bfce5b6",
"orders": "https://petstoreapi.com/v1/pets/019b4132-70aa-764f-b315-e2803d882a24/orders"
}
}
ไม่มี HATEOAS เต็มรูปแบบ
Modern PetstoreAPI ไม่บังคับให้ไคลเอ็นต์ต้องขับเคลื่อนด้วยไฮเปอร์มีเดีย ไคลเอ็นต์สามารถ:
ตัวเลือกที่ 1: ติดตามลิงก์ (ขับเคลื่อนด้วยไฮเปอร์มีเดีย)
const pet = await fetch(petUrl);
const ownerUrl = pet._links.owner.href;
const owner = await fetch(ownerUrl);
ตัวเลือกที่ 2: สร้าง URL (แบบดั้งเดิม)
const pet = await fetch(`https://petstoreapi.com/v1/pets/${petId}`);
const owner = await fetch(`https://petstoreapi.com/v1/users/${pet.ownerId}`);
ทั้งสองวิธีใช้งานได้ ลิงก์มีไว้เพื่อความสะดวก ไม่ใช่การบังคับ
การทดสอบ Hypermedia API ด้วย Apidog
Apidog ช่วยให้คุณทดสอบลิงก์ไฮเปอร์มีเดียและตรวจสอบความถูกต้องของลิงก์
ทดสอบการมีอยู่ของลิงก์
ตรวจสอบว่าการตอบสนองมีลิงก์ที่คาดไว้หรือไม่:
// Apidog test script
pm.test("Response includes pagination links", () => {
const links = pm.response.json().links;
pm.expect(links).to.have.property('self');
pm.expect(links).to.have.property('next');
});
ทดสอบความถูกต้องของลิงก์
ติดตามลิงก์และตรวจสอบว่าใช้งานได้:
// Apidog test script
const nextUrl = pm.response.json().links.next;
pm.sendRequest(nextUrl, (err, response) => {
pm.test("Next link returns 200", () => {
pm.expect(response.code).to.equal(200);
});
});
ทดสอบรูปแบบลิงก์
ตรวจสอบว่าลิงก์เป็นไปตามรูปแบบที่คาดไว้:
pm.test("Links are absolute URLs", () => {
const links = pm.response.json().links;
Object.values(links).forEach(link => {
pm.expect(link).to.match(/^https:\/\//);
});
});
เมื่อใดควรใช้ HATEOAS
ใช้ลิงก์ไฮเปอร์มีเดียเมื่อพวกมันเพิ่มคุณค่า ข้ามไปเมื่อพวกมันไม่เพิ่ม
ใช้ลิงก์ไฮเปอร์มีเดียสำหรับ:
1. การแบ่งหน้า - ไคลเอ็นต์ไม่ควรสร้าง URL สำหรับการแบ่งหน้า
2. ทรัพยากรที่เกี่ยวข้อง - การนำทางที่สะดวก
3. การดำเนินการที่ขึ้นอยู่กับสถานะ - แสดงการดำเนินการที่พร้อมใช้งานตามสถานะของทรัพยากร
4. เวิร์กโฟลว์ที่ซับซ้อน - แนะนำไคลเอ็นต์ผ่านกระบวนการหลายขั้นตอน
ข้าม HATEOAS สำหรับ:
1. Simple CRUD API - ไคลเอ็นต์สามารถสร้าง URL ได้อย่างง่ายดาย
2. Internal API - ทีมสามารถประสานงานการเปลี่ยนแปลง URL ได้
3. API ที่เน้นประสิทธิภาพ - ลิงก์เพิ่มเติมเพิ่มขนาดการตอบสนอง
4. Mobile API - แบนด์วิดท์มีความสำคัญ ลิงก์เพิ่มภาระ
สรุป
HATEOAS มีความสง่างามตามทฤษฎี แต่ซับซ้อนในทางปฏิบัติ API ส่วนใหญ่ข้ามการใช้งาน HATEOAS เต็มรูปแบบ และใช้ลิงก์ไฮเปอร์มีเดียแบบเลือกในจุดที่เพิ่มคุณค่า
Modern PetstoreAPI แสดงให้เห็นถึงไฮเปอร์มีเดียที่ใช้งานได้จริง: ลิงก์การแบ่งหน้า, ลิงก์ทรัพยากรที่เกี่ยวข้อง และลิงก์การดำเนินการ—โดยไม่บังคับให้ไคลเอ็นต์ต้องขับเคลื่อนด้วยไฮเปอร์มีเดียทั้งหมด
ใช้ Apidog เพื่อทดสอบลิงก์ไฮเปอร์มีเดีย, ตรวจสอบความถูกต้องของลิงก์ และรับประกันว่า API ของคุณมีการนำทางที่มีประโยชน์
ประเด็นสำคัญ:
- HATEOAS เต็มรูปแบบนั้นหาได้ยากและซับซ้อน
- ลิงก์ไฮเปอร์มีเดียแบบเลือกเพิ่มคุณค่าโดยไม่เพิ่มความซับซ้อน
- ลิงก์การแบ่งหน้าเป็นคุณสมบัติไฮเปอร์มีเดียที่มีประโยชน์ที่สุด
- อย่าบังคับให้ไคลเอ็นต์ต้องขับเคลื่อนด้วยไฮเปอร์มีเดีย
- ทดสอบลิงก์เพื่อให้แน่ใจว่าทำงานได้อย่างถูกต้อง
สำรวจ เอกสารของ Modern PetstoreAPI เพื่อดูการใช้งานไฮเปอร์มีเดียที่ใช้งานได้จริง
คำถามที่พบบ่อย
HATEOAS จำเป็นสำหรับ REST API หรือไม่?
ตามที่ Roy Fielding (ผู้คิดค้น REST) กล่าวไว้คือจำเป็น แต่ในทางปฏิบัติแล้วไม่ใช่ API ที่เรียกว่า “REST” ส่วนใหญ่ข้าม HATEOAS และในทางเทคนิคแล้วเป็น “HTTP API” หรือ “REST-like API”
HATEOAS ย่อมาจากอะไร?
Hypermedia as the Engine of Application State หมายความว่าไคลเอ็นต์ค้นพบความสามารถของ API ผ่านลิงก์ไฮเปอร์มีเดีย ไม่ใช่ URL ที่ฮาร์ดโค้ด
API หลักๆ ใช้ HATEOAS หรือไม่?
ไม่ GitHub, Stripe, Twilio และ API หลักๆ ส่วนใหญ่ไม่ได้ใช้ HATEOAS เต็มรูปแบบ พวกเขาอาจมีลิงก์ไฮเปอร์มีเดียบางส่วน (การแบ่งหน้า, ทรัพยากรที่เกี่ยวข้อง) แต่ไม่บังคับให้ไคลเอ็นต์ต้องขับเคลื่อนด้วยไฮเปอร์มีเดีย
ความแตกต่างระหว่าง HATEOAS และลิงก์ไฮเปอร์มีเดียคืออะไร?
HATEOAS เป็นข้อจำกัดที่บังคับให้ไคลเอ็นต์ต้องขับเคลื่อนด้วยไฮเปอร์มีเดียทั้งหมด ส่วนลิงก์ไฮเปอร์มีเดียเป็นเพียงลิงก์ที่อยู่ในผลตอบสนอง คุณสามารถรวมลิงก์ได้โดยไม่ต้องบังคับใช้ HATEOAS
ฉันควรใช้ HATEOAS ใน API ของฉันหรือไม่?
อาจจะไม่ใช่ HATEOAS เต็มรูปแบบ ให้ใช้ลิงก์ไฮเปอร์มีเดียแบบเลือกสำหรับสำหรับข้อมูลแบบแบ่งหน้าและทรัพยากรที่เกี่ยวข้อง อย่าบังคับให้ไคลเอ็นต์ต้องขับเคลื่อนด้วยไฮเปอร์มีเดียเว้นแต่คุณจะมีเหตุผลเฉพาะ
ฉันจะทดสอบ HATEOAS API ได้อย่างไร?
ใช้ Apidog เพื่อตรวจสอบการมีอยู่ของลิงก์, ติดตามลิงก์ และตรวจสอบความถูกต้องของลิงก์ ทดสอบว่าลิงก์ส่งคืนผลตอบรับที่คาดไว้หรือไม่
รูปแบบ HAL คืออะไร?
HAL (Hypertext Application Language) เป็นรูปแบบมาตรฐานสำหรับลิงก์ไฮเปอร์มีเดีย โดยใช้ฟิลด์ _links และ _embedded Modern PetstoreAPI ใช้รูปแบบลิงก์ที่ได้รับแรงบันดาลใจจาก HAL
ไคลเอ็นต์สามารถละเว้นลิงก์ไฮเปอร์มีเดียได้หรือไม่?
ได้ หาก API ของคุณมีลิงก์แต่ไม่บังคับให้ไคลเอ็นต์ต้องใช้ ไคลเอ็นต์ก็สามารถสร้าง URL ได้โดยตรง นี่คือวิธีการเชิงปฏิบัติที่ API ส่วนใหญ่ใช้
