- ในวิศวกรรมซอฟต์แวร์ API คือเครื่องมือหลัก และ API ที่ดีควรคุ้นเคยและเรียบง่ายจนแทบจะน่าเบื่อ
- เมื่อ API ถูกเปิดเผยออกไปแล้ว การเปลี่ยนแปลงจะทำได้ยาก จึงต้องยึดหลัก ไม่ทำลายสภาพแวดล้อมของผู้ใช้ (WE DO NOT BREAK USERSPACE)
- หากจำเป็นต้องเปลี่ยนจริง ๆ ก็ต้องมี การจัดการเวอร์ชัน (versioning) แต่สิ่งนี้เป็น สิ่งจำเป็นที่ไม่พึงประสงค์ เพราะเพิ่มความซับซ้อนและต้นทุนการดูแลรักษาอย่างมาก
- คุณภาพของ API ท้ายที่สุดแล้วขึ้นอยู่กับ คุณค่าของตัวผลิตภัณฑ์เอง และหากผลิตภัณฑ์ถูกออกแบบมาไม่ดี ก็ยากที่จะสร้าง API ที่ดีได้
- เพื่อความเสถียรและการขยายระบบ ควรคำนึงถึง การยืนยันตัวตนด้วย API key, idempotency, rate limit, การแบ่งหน้าแบบ cursor-based เป็นต้น
บทนำ: ความสำคัญและบริบทของการออกแบบ API
- หนึ่งในงานหลักของวิศวกรซอฟต์แวร์ยุคใหม่คือการ โต้ตอบกับ API
- ผู้เขียนเองก็มีประสบการณ์ในการออกแบบ/พัฒนา/ใช้งาน API ทั้งแบบสาธารณะและภายในองค์กร ในหลายรูปแบบ เช่น REST, GraphQL และเครื่องมือบรรทัดคำสั่ง
- คำแนะนำด้านการออกแบบ API ที่มีอยู่ในปัจจุบันมักหมกมุ่นกับแนวคิดที่ซับซ้อนเกินไป (เช่น นิยามของ REST, HATEOAS เป็นต้น)
- บทความนี้จึงสรุป หลักการออกแบบ API เชิงปฏิบัติ จากประสบการณ์จริง
สมดุลระหว่างความคุ้นเคยกับความยืดหยุ่น: เงื่อนไขแรกของ API ที่ดี
- API ที่ดีคือ API ที่ธรรมดาและน่าเบื่อ กล่าวคือ ควรมีวิธีใช้งานคล้ายกับ API อื่น ๆ ที่ผู้ใช้เคยพบมาก่อน
- ผู้ใช้มัก โฟกัสที่การบรรลุเป้าหมายของตนเองมากกว่าตัว API ดังนั้นจึงต้องออกแบบให้มีอุปสรรคในการเริ่มใช้งานต่ำ
- API ที่เปิดเผยออกไปแล้วนั้น เปลี่ยนแปลงได้ยากมาก จึงต้องระมัดระวังอย่างยิ่งตั้งแต่ขั้นตอนการออกแบบครั้งแรก
- นักพัฒนามักต้องการ API ที่กระชับที่สุดเท่าที่จะเป็นไปได้ แต่ก็ต้องคิดเผื่อความยืดหยุ่นระยะยาวอยู่เสมอ
- สุดท้ายแล้ว ประเด็นสำคัญคือ การหาสมดุลระหว่างความคุ้นเคยกับความยืดหยุ่นในระยะยาว
ห้ามทำลาย userspace เด็ดขาด (WE DO NOT BREAK USERSPACE)
- การ เพิ่มฟิลด์ เข้าไปในโครงสร้าง response เดิมนั้น โดยมากแล้วไม่ก่อปัญหา
- แต่ การลบฟิลด์ หรือเปลี่ยนชนิดข้อมูลและโครงสร้าง จะทำให้โค้ดของผู้ใช้ทุกคนพังทันที
- ผู้ดูแล API มี ความรับผิดชอบ ที่จะไม่ทำให้ซอฟต์แวร์ของผู้ใช้เดิมเสียหายโดยเจตนา
- แม้แต่การสะกดผิดของ HTTP header อย่าง
referer ก็ยังไม่ถูกแก้ เพราะมี วัฒนธรรมในการรักษา userspace
เปลี่ยน API โดยไม่ทำให้พัง: กลยุทธ์การจัดการเวอร์ชัน
- ควรยอมรับ การเปลี่ยนแปลงแบบทำลายความเข้ากันได้ กับ API เฉพาะเมื่อจำเป็นจริง ๆ และในกรณีนั้น การจัดการเวอร์ชัน คือคำตอบ
- ควร เปิดให้เวอร์ชันเก่าและใหม่ทำงานพร้อมกัน เพื่อให้ผู้ใช้ค่อย ๆ ย้ายผ่านได้
- ตัวระบุเวอร์ชันอาจใส่ใน URL (
/v1/) หรือ header ก็ได้ ทำให้ผู้ใช้เลือกย้ายเมื่อพร้อมตามจังหวะของตัวเอง
- การจัดการเวอร์ชันมีข้อเสียคือ ต้นทุนการดูแลรักษาที่มหาศาล (endpoint เพิ่มขึ้น, การทดสอบ, การซัพพอร์ต) และ ความสับสนของผู้ใช้
- ต่อให้มีชั้นแปลภายในแบบ Stripe ก็ยังหลีกเลี่ยงความซับซ้อนพื้นฐานนี้ไม่ได้
- การนำ API versioning มาใช้ควรเป็นทางเลือกสุดท้าย
ปัจจัยความสำเร็จของ API ขึ้นอยู่กับคุณค่าของผลิตภัณฑ์ล้วน ๆ
- API โดยเนื้อแท้แล้วเป็นเพียง อินเทอร์เฟซของผลิตภัณฑ์ธุรกิจจริง เท่านั้น
- API อย่าง OpenAI หรือ Twilio ก็สุดท้ายแล้ว สิ่งที่ผู้ใช้ต้องการคือ ความสามารถที่ API มอบให้โดยตรง
- หากเป็น ผลิตภัณฑ์ที่มีคุณค่า ผู้ใช้ก็จะยอมใช้ แม้ API จะใช้งานไม่สะดวกนักก็ตาม
- คุณภาพของ API เป็นคุณสมบัติแบบ “ส่วนต่าง” กล่าวคือ จะกลายเป็นปัจจัยในการเลือกก็ต่อเมื่อความสามารถหลักของคู่แข่งใกล้เคียงกัน
- ในทางกลับกัน หากผลิตภัณฑ์ ไม่มี API เลย ก็เป็นอุปสรรคใหญ่สำหรับผู้ใช้สายเทคนิค
ถ้าออกแบบผลิตภัณฑ์ไม่ดี API ก็ไม่มีทางดีได้
- ต่อให้มี API ที่ยอดเยี่ยมในเชิงเทคนิค แต่ถ้า ผลิตภัณฑ์ไม่มีศักยภาพทางตลาด ก็แทบไม่มีความหมาย
- ที่สำคัญกว่านั้นคือ หาก โครงสร้าง resource พื้นฐานไม่มีตรรกะหรือไร้ประสิทธิภาพ ปัญหานั้นก็จะสะท้อนออกมาที่ API ด้วย
- ตัวอย่างเช่น ระบบที่เก็บคอมเมนต์เป็น linked list จะทำให้แม้แต่การออกแบบแบบ RESTful ก็ออกมาอย่างเป็นธรรมชาติได้ยาก
- ปัญหาทางเทคนิคที่อาจซ่อนไว้ได้ใน UI จะถูกเปิดเผยออกมาทั้งหมดใน API และบังคับให้ผู้ใช้ต้องเข้าใจระบบมากเกินความจำเป็น
การยืนยันตัวตน (Authenticaton) และความหลากหลายของผู้ใช้
- ต้องรองรับ การยืนยันตัวตนด้วย API key ที่มีอายุใช้งานยาว
- แม้จะรองรับวิธีที่ปลอดภัยสูงกว่าอย่าง OAuth เพิ่มเติมได้ แต่ API key มี อุปสรรคในการเริ่มใช้งานต่ำกว่ามาก
- ผู้ใช้ API ไม่ได้มีแค่วิศวกร แต่ยังรวมถึงผู้ที่ไม่ใช่นักพัฒนา (ฝ่ายขาย, ผู้วางแผน, นักเรียน, นักพัฒนางานอดิเรก ฯลฯ) อีกมาก
- การบังคับใช้วิธีการยืนยันตัวตนที่ยากหรือซับซ้อน (เช่น OAuth) เป็น กำแพงสำหรับผู้ใช้ที่ไม่เชี่ยวชาญ
Idempotency และการจัดการ retry
- คำขอที่ก่อให้เกิดการกระทำ (เช่น การชำระเงิน, การเปลี่ยนสถานะ ฯลฯ) ต้องให้ความสำคัญกับความปลอดภัยในการ retry เมื่อเกิดความล้มเหลว
- Idempotency คือการ รับประกันว่าแม้ส่งคำขอเดิมหลายครั้ง ผลลัพธ์จะถูกประมวลผลเพียงครั้งเดียว
- วิธีมาตรฐานคือส่ง “idempotency key” ผ่านพารามิเตอร์หรือ header เพื่อป้องกันการประมวลผลซ้ำ
- การเก็บ idempotency key ใช้เพียง ที่เก็บ key/value แบบง่าย ๆ อย่าง Redis ก็เพียงพอ และในกรณีส่วนใหญ่จะตั้งหมดอายุเป็นระยะ ๆ ก็ได้
- โดยทั่วไปไม่จำเป็นสำหรับคำขออ่าน/ลบ (แนวทางแบบ REST)
ความปลอดภัยของ API และการจำกัดอัตรา (Rate limiting)
- คำขอ API ผ่านโค้ดสามารถเกิดขึ้นได้เร็วกว่าการกระทำของผู้ใช้มาก
- API ที่ปล่อยออกไปอย่างไม่ทันคิดเพียงรายการเดียว อาจถูกนำไปใช้ในทางที่ไม่ได้ตั้งใจ (เช่น ระบบแชตขนาดใหญ่)
- การจำกัดอัตรา (ratelimit) เป็นสิ่งจำเป็น และควรเข้มงวดมากขึ้นกับงานที่มีต้นทุนสูง
- ควรพิจารณาทางเลือกอย่างการปิดการใช้งาน API ชั่วคราวสำหรับลูกค้าบางรายโดยเฉพาะ (killswitch)
- ควรแจ้งข้อมูลการจำกัดอัตราผ่าน response header (
X-Limit-Remaining, Retry-After เป็นต้น)
กลยุทธ์การแบ่งหน้า (Pagination)
- หากต้องคืนข้อมูลจากชุดข้อมูลขนาดใหญ่ (เช่น ticket หลายล้านรายการ) อย่างมีประสิทธิภาพ การแบ่งหน้าเป็นสิ่งจำเป็น
- การแบ่งหน้าแบบ offset-based นั้นง่าย แต่เมื่อข้อมูลมีจำนวนมากจะยิ่งช้าลงเรื่อย ๆ
- การแบ่งหน้าแบบ cursor-based มีประสิทธิภาพแม้กับชุดข้อมูลขนาดใหญ่มาก โดยไม่ทำให้ประสิทธิภาพ query แย่ลง
- แม้ cursor-based จะทั้งพัฒนาและใช้งานยากกว่าเล็กน้อย แต่ในระยะยาวมีแนวโน้มสูงว่าจะกลายเป็นการเปลี่ยนแปลงที่จำเป็น
- การใส่ฟิลด์อย่าง
next_page ไว้ใน response เพื่อบอก cursor สำหรับคำขอถัดไปอย่างชัดเจนถือเป็นแนวทางที่เหมาะสม
มุมมองต่อฟิลด์แบบเลือกได้และ GraphQL
- ฟิลด์ที่มีต้นทุนสูงหรือช้า ควรถูกตัดออกจาก response เริ่มต้น และค่อยเพิ่มเข้ามาแบบเลือกได้เมื่อจำเป็น
- สามารถรองรับการใส่ข้อมูลที่เกี่ยวข้องผ่านพารามิเตอร์อย่าง
includes
- GraphQL มีข้อดีด้านความยืดหยุ่นของโครงสร้างข้อมูล แต่ก็มีปัญหาเรื่อง การเข้าถึงที่ยากขึ้นสำหรับผู้ที่ไม่ใช่นักพัฒนา, ความซับซ้อนของ caching/edge case, และความยากของการพัฒนาฝั่ง backend
- จากประสบการณ์จริง การใช้ GraphQL ควร จำกัดเฉพาะกรณีที่จำเป็นจริง ๆ
ลักษณะเฉพาะของ API ภายในองค์กร
- API ภายในองค์กรมีเงื่อนไขหลายอย่างที่ต่างจาก API ภายนอก (แบบเปิดสู่สาธารณะ)
- ผู้ใช้ส่วนใหญ่เป็น วิศวกรซอฟต์แวร์มืออาชีพ จึงสามารถใช้การยืนยันตัวตนที่ซับซ้อนกว่า หรือยอมรับการเปลี่ยนแปลงแบบทำลายความเข้ากันได้มากกว่าได้
- ถึงอย่างนั้น หลักการออกแบบเพื่อ idempotency, การป้องกันอุบัติเหตุ, และการลดภาระการปฏิบัติการ ก็ยังใช้ได้อยู่
สรุป
- API เปลี่ยนยาก แต่ควรใช้งานง่าย
- การไม่ทำลาย userspace คือหน้าที่สำคัญที่สุดของผู้ดูแล API
- การจัดการเวอร์ชันของ API มีต้นทุนสูง จึงควรใช้เป็นทางเลือกสุดท้ายเท่านั้น
- ท้ายที่สุดแล้ว คุณภาพของ API ถูกกำหนดโดยคุณค่าที่แท้จริงของผลิตภัณฑ์
- หากผลิตภัณฑ์ถูกออกแบบผิดพลาด การพยายามชดเชยในระดับ API ก็มีขีดจำกัดมาก
- การ รองรับวิธียืนยันตัวตนที่เรียบง่าย, การมี idempotency สำหรับคำขอประเภท action ที่จำเป็น, และมาตรการด้านเสถียรภาพอย่าง rate limit/pagination ล้วนสำคัญ
- API ภายในมีแนวทางต่างกันตามวัตถุประสงค์และกลุ่มผู้ใช้ แต่ก็ยังต้องออกแบบอย่างรอบคอบเช่นเดิม
- REST, JSON หรือสเปกอย่าง OpenAPI ไม่ใช่ประเด็นแก่นแท้ สิ่งที่สำคัญกว่าคือการทำเอกสารให้ชัดเจน
ยังไม่มีความคิดเห็น