ออกแบบ REST API ยังไงดี? แนะนำ jsonapi

Nuttavut Thongjor

ดีไซน์ API ไปทำไม คนกันเองทั้งนั้นที่ใช้?

API เป็นช่องทางหรือ Interface ให้ส่วนอื่นของระบบเรียกใช้งาน จึงไม่ต่างอะไรจากประตูเข้าเซเว่น หากเป็นประตูแบบเก่าที่ใช้ระบบผลัก บางคนก็รังเกียจที่จะสัมผัสมัน ผิดกับประตูอัตโนมัติ ใครๆก็ชอบแม้แต่สุนัขยังชอบมานอนขวางเล่นหน้าประตู

ถ้า API เป็นประตู เราจะสร้างยังไงก็ได้ซิ ก็นะฉันเข้าออกคนเดียว? ความจริงอาจเป็นเช่นนั้น แต่ถ้ามองมุมกลับแล้ว คุณจะรู้ได้ยังไงว่าอนาคตจะไม่มีใครมาเรียกใช้ประตูของคุณ? จะดีกว่าไหมถ้าประตูของคุณได้มาตรฐาน เวลามีเหตุขัดข้องก็หาช่างซ่อมง่ายเพราะใครๆก็ใช้ประตูแบบนี้

เราไม่ได้สร้างประตูไปไหนก็ได้

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

ทางเข้าและทางออก

หัวข้อนี้จะกล่าวถึง Request และ Response ว่ามีอะไรที่พอปรับปรุงได้บ้าง...

  • ทำ API Versioning ทุก Request ที่ส่งเข้ามาต้องระบุเสมอว่าจะใช้ API version ไหน ในอนาคตหากเราแก้โค๊ดที่ไม่ Compatible กับของเก่า เราสามารถเปลี่ยนเลขเวอร์ชันของ API ได้ ทั้งนี้ผู้ใช้งาน API ของเรายังคงเรียกใช้ตัวเดิมได้ โดยไม่ได้รับผลกระทบใดๆ ตัวอย่างเช่น https://api.twitter.com/1.1/statuses/retweets_of_me.json ที่ระบุว่าเป็น API Version 1.1

  • อย่าใช้ path ที่ยาว คุณเคยเจอ path แบบนี้ไหมครับ http://looooong.com/api/v1/categories/1/posts/2/discussions/3 เราต้องการข้อความสนทนาที่มี ID เป็น3แค่นั้นเอง ไม่จำเป็นต้องทราบว่าเป็นของ post หรือ category ไหนก็ได้ เหตุนี้เราควรมี Route ที่สั้นสำหรับสิ่งนี้เป็น http://looooong.com/api/v1/discussions/3

  • ใช้ slug พึงหลีกเลี่ยง path แบบนี้ /users/3 แต่ควรใช้ /users/babel-coder ทั้งนี้เพื่อป้องกันไม่ให้ผู้อื่นรู้ไปถึงรายละเอียดฐานข้อมูลของคุณว่าผู้ใช้งานที่ระบุมีไอดีเป็นอะไร นอกจากนี้การใช้ slug ยังช่วยสื่อความหมายให้คุณ ผู้ใช้และ bot เข้าใจ path คุณมากขึ้น

  • ใช้ ISO8601 สำหรับวันที่/เวลาและคืนค่าเป็น UTC (GMT + 0) เสมอ ตัวอย่างเช่น

Code
1{
2 "publishedAt": "2016-04-21T12:00:00Z"
3}

บางคนอาจคิดว่าเราคืนค่ากลับเป็น 2 days ago แบบนี้ไม่ได้หรอ ผมคิดว่าไม่ควรครับเพราะข้อความแบบนี้เอาไปดัดแปลงต่อไม่ได้ เอาไปเปรียบเทียบก็ยาก i18n ก็ยาก

  • ใช้ HTTP Status Code ให้ถูกต้อง เราไม่ควรส่ง 200 OK พร้อมแปะ error ไปว่า "Not Found" กลับไป แต่ควรส่ง 404 กลับไปแทน หรือกรณี login ด้วยอีเมล์ ถ้าเราหา email ไม่พบนั่นคือไม่มีผู้ใช้งานนี้ในระบบ เราไม่ควรส่ง 404 Not Found กลับไป แต่ต้องส่ง 401 Unauthorized

  • ทำ Sideloading พิจารณากรณีของ article พบว่าเวลาเราแสดงผล เราจะแสดงบทความที่เกี่ยวข้องด้วย ถ้าเราไม่ทำ Sideloading เราต้องร้องขอข้อมูลถึงสองครั้งเพื่อดึงบทความที่ต้องการ จากนั้นจึงดึงบทความที่เกี่ยวข้องอีกรอบ แน่นอนว่าเสียเวลา แต่หากเราสร้าง http://example.com/api/v1/articles/1?include=related_articles ที่ return ทั้งบทความและบทความที่เกี่ยวข้องจะดีกว่า ดังนี้

Code
1{
2 "article": {
3 "id": 1,
4 "title": "...",
5 "relatedArticles": [
6 {
7 "id": 2,
8 "title": "..."
9 }
10 ]
11 }
12}
  • ให้ API เป็นเช่น API ความหมายคืออย่าตามใจ Client เกินไปจนลืมไปว่า API ของเรามี Client อื่นใช้งานด้วย ยกตัวอย่างจากข้างบนคือ /api/v1/articles/1 ไม่ควร return บทความที่เกี่ยวข้องกลับมาด้วย เพราะคนอื่นที่เรียกใช้ API นี้อาจไม่ต้องการมัน หากใครต้องการให้ระบุเป็น Query String ว่า ?include=related_articles แทน

  • ใช้ Nonce สำหรับ authentication REST API ที่ใช้ token-based ทั้งหลายพึงระวังให้ token ที่ใช้งานแตกต่างและใช้แค่ครั้งเดียว

  • ตรวจสอบว่าใช้ HTTP Verb ถูกต้อง ตัวอย่างเช่นใช้ PATCH สำหรับการอัพเดท article บางส่วน และใช้ POST สำหรับการสร้าง article ใหม่

จะดีกว่าไหม ถ้ามีคนกำหนดมาตรฐานมาให้?

ความฝันเป็นจริงครับ เมื่อมีคนกำหนดมาตรฐานนี้มาให้แล้วนั่นคือ jsonapi.org ลองมาดูมาตรฐานบางส่วนครับ

ใน response ถ้าก้อนอ็อบเจ็กต์ของเรา (resource) เกี่ยวข้องกับ resource อื่นใด ให้โยนสิ่งนั้นใส่ relationships เช่นผู้เขียนเป็นส่วนที่เกี่ยวข้องกับบทความ เราจึงยัด author ไว้ภายใต้ relationships

Code
1// ตัวอย่างจากเอกสารของ jsonapi.org
2{
3 "type": "articles",
4 "id": "1",
5 "attributes": {
6 "title": "Rails is Omakase"
7 },
8 "relationships": {
9 "author": {
10 "links": {
11 "self": "/articles/1/relationships/author",
12 "related": "/articles/1/author"
13 },
14 "data": { "type": "people", "id": "9" }
15 }
16 }
17}
18// ...

ถ้าต้องการสร้าง resource ใหม่ต้องทำผ่าน POST และต้องระบุประเภทของ resource ด้วย ทั้งนี้สามารถระบุ resource ที่เกี่ยวข้องผ่าน relationships ได้ใน request เดียว

Code
1POST /photos HTTP/1.1
2Content-Type: application/vnd.api+json
3Accept: application/vnd.api+json
4
5{
6 "data": {
7 "type": "photos",
8 "attributes": {
9 "title": "Ember Hamster",
10 "src": "http://example.com/images/productivity.png"
11 },
12 "relationships": {
13 "photographer": {
14 "data": { "type": "people", "id": "9" }
15 }
16 }
17 }
18}

ทำอย่างไรถึงจะใช้มาตรฐาน jsonapi ได้

มีสารพัดไลบรารี่หรือเฟรมเวิร์กที่สนับสนุน jsonapi ครับ ตัวอย่างเช่น ActiveModel::Serializer ของ Rails หรือถ้าใครใช้ Ember เป็น Front-end ก็สนับสนุนเป็นตัวหลักเช่นกัน

แล้วถ้าไม่ได้ใช้เฟรมเวิร์กที่สนับสนุนอยู่แล้วหละ? ตัว jsonapi เองเขามีลิสต์ของไลบรารี่ที่อิมพลีเมนต์การทำงานตามมาตรฐานในภาษาต่างๆครับ ลองเข้าไปดูกันที่ Implementations

เอกสารอ้างอิง

A SPECIFICATION FOR BUILDING APIS IN JSON. Retrieved April, 21, 2016, from http://jsonapi.org/

Wesley Beary (2016). HTTP API Design Guide. Retrieved April, 21, 2016, from https://geemus.gitbooks.io/http-api-design/content/en/

สารบัญ

สารบัญ

  • ดีไซน์ API ไปทำไม คนกันเองทั้งนั้นที่ใช้?
  • เราไม่ได้สร้างประตูไปไหนก็ได้
  • ทางเข้าและทางออก
  • จะดีกว่าไหม ถ้ามีคนกำหนดมาตรฐานมาให้?
  • ทำอย่างไรถึงจะใช้มาตรฐาน jsonapi ได้
  • เอกสารอ้างอิง