แนวทางการออก REST API ตามมาตรฐาน

สำหรับบทความนี้ผมจะมาอธิบายการออกแบบ REST API ว่าจะมีหลักการอย่างไรบ้าง และเราจะใช้ความสามารถของ HTTP Method นั้นได้อย่างไร

คำที่ควรรู้

  • Resource — เป็นคำที่พูดใช้แทนข้อมูลที่ API จะทำการส่งมาให้สามารถเป็นคำที่แทน Object ต่างๆ ได้ เช่น Users, Categories, Devices, Animals, Tags เป็นต้น ซึ่งแต่ละ recources ก็จะมีความสามารถในการเพิ่ม, ลบ, อัพเดทข้อมูล

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

1. การตั้งชื่อ Resource

  1. ใช้ nouns (คำนาม) อย่าใช้ verbs (คำกริยา)

สมุติว่าเราต้องการทำ API ที่ไว้ทำงานเกี่ยวกับ Tag เพราะงั้นเราก็ต้องการเพิ่ม ลบ อัพเดทข้อมูลของ Tag ได้ ดั้งนั้นเราอาจจะตั้งชื่อแต่ละ Operation ว่า

  • /addTags

แบบนี้ก็ดูจะไม่ผิดอะไรเพราะเราก็สามารถทำให้ REST API เราทำงานได้ตามต้องการ แต่เราจะเห็นว่าการตั้งชื่อที่ถูกควรจะเป็นคำนามที่ใช้เรียก resource ไม่ใช้ action ของ CRUD มาเรียก resource

ซึ่งวิธีการตั้งชื่อให้ถูกควรจะเป็นแบบนี้

  • ใช้ plural nones ไม่ใช้ Singural none หรือพูดง่ายๆ ว่าเติม (s) ให้คำนามลง ตัวไหนมี (y) ให้เป็นเป็น (i) แล้วเติม (es)

ตัวอย่างการดึงข้อมูลและสร้างข้อมูลใน API โดยใช้ GET, POST

  • GET /tags — ดึงข้อมูลทั้งหมดของ Tags

ตัวอย่างถัดมาคือการใช้ PUT, PATCH, DELTE

  • PUT /tags/1 — คือการอัพเดทข้อมูล tag ที่มี ID 1แต่ถ้าไม่มี ID 1จะต้องสร้างข้อมูลใหม่ข้อมูลขึ้นมา

ข้อควรจำ: หลายๆ คนคงสัยว่า PUT/PATCH ต่างกันอย่างไร จริงๆ แล้วถ้าเป็นเรื่อง Action ต่างกันที่ ถ้า PUT และ Server หาข้อมูลไม่เจอจะต้องสร้างข้อมูลชุดใหม่ขึ้นมาแทน หรือเราจะเรียกมันว่า upsert (update or insert) แต่!!!! PUT ต้องส่งข้อมูลทั้งหมดมานะครับ หมายความว่าถ้าโครงสร้าง Database เราออกแบบว่า Tag schema หน้าตาแบบนี้

สิ่งที่เกิดขึ้นกับ PUT คือเราต้องส่ง idnamelv มาแต่ description ไม่จำเป็นก็ได้เพราะเราให้มันเป็นเป็นค่าว่างได้ และการทำงานจะเป็นออกเป็นดังนี้

  • ส่ง idnamelv มาเมื่อ Server ได้รับ request และทำการหาข้อมูลใน Database แล้วค้นเจอว่ามี ID นั้นๆ อยู่ก็จะทำการเอา namelv ไปอัพเดทข้อมูลให้กับ ID ที่เราส่งมา

ส่วน PATCH จะเป็นการอัพเดทข้อมูลอย่างเดียวไม่ต้องสร้างใหม่แต่ว่าการทำงานจะต่างออกไปนิดนึงคือโดยปกติ PUT ต้องส่งทุกฟิลด์มา แต่บางครั้งเราไม่มีความจำเป็นในการส่งข้อมูลมาเยอะขนาดนั้น ซึ่งอาจจะทำให้ payload ใหญ่เกินความจำเป็น เพราะบางทีเราแค่ต้องการอัพเดทข้อมูลบางฟิลด์ ดังนั้นเราจึงมี PATCH ขึ้นมาเพื่อแก้ปัญหาตรงนี้

2. อย่าใช้ GET ในการอัพเดทหรือสร้างข้อมูลใหม่

เพื่อป้องกันการถูกแฮกหรือขโมยข้อมูลเราไม่ควรใช้ GET ในการทำงานกับการสร้างหรืออัพเดทข้อมูล ตัวอย่างที่ไม่ควรทำเลยเช่นการสร้าง User ใหม่ที่มีการส่ง Username และ ​Password ไป

ผิด

GET /users?username=jonh&password=123456789

ถูก

POST /users (data in HTTP body)

แบบนี้ไม่ควรทำเพราะมีความเสี่ยงที่ข้อมูลที่ส่งกันระหว่าง network จะถูกดักได้ และควรทำ HTTPS ด้วย อย่าใช้ HTTP อย่างเดียวและควรใช้ POST เพื่อสร้างข้อมูลและ PUT หรือ PATCH ในการอัพเดทข้อมูล และอีกข้อนึงเลย GET มีข้อจำกัดความ URL เพราะงั้น ถ้าส่ง payload ยาวๆ ไป เราจะพบกับบัคทันที ผมจำไม่ได้ว่ายาวกี่อักษร

3. เราอาจจะมี resource ที่อยู่ใต้ resource ได้

เราสามารถเอากฏของข้อ 1 มาใช้เมื่อเราต้องมี resource ที่ต้องเป็น sub ของ resource ใดๆ เช่น เรามี Tags และ Articles แต่ articles สามารถอยู่ภายใต้ tag ได้

  • GET /tags/1/articles — ดึงข้อมูลบทความทั้งหมดที่อยู่ใน tag ที่มี ID เป็น 1

4. ระบุ API version

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

/api/v1/tags
/api/v2/tags

5. ใช้ HTTP Status Codes

เมื่อมีการทำงานอะไรบ้างอย่างเราต้องมีการตั้งค่า HTTP Code ด้วยเสมอ เพื่อทำให้ Client เข้าใจว่าการทำงานนั้นสำเร็จหรือไม่ และที่สำคัญตัว Client Lib ที่เกี่ยวกับ HTTP เดี๋ยวนี้จะมีการจัดการเช็ค Status code หากว่าไม่ตั้งค่า code และในกรณีที่ API ไม่สามารถทำงานตามที่ Client ขอมาได้ แต่กลับว่าเราโยน code กลับไป 200 เพราะปกติแล้วมันจะโยน 200 เสมอ กลายเป็นว่า Client จะเข้าใจเสมอว่าการทำงานนั้นผ่าน

ตัวอย่าง Client request ด้วย Axios

axios.get('/user?ID=12345')
  .then(function (response) {
    // handle success
    console.log(response);
  })
  .catch(function (error) {
    // handle error
    console.log(error);
  })
  .then(function () {
    // always executed
  });

ตัวอย่างการ Respose ของ Server ด้วย ExpressJs

res.json({ data: {} , message: 'user notfound' })

แบบตัวอย่างข้างบน Client request จะไม่ทำงานในส่วนของ catch เพราะ ExpressJS ส่ง 200 มาตลอด

.catch(function (error) {
  // handle error
  console.log(error);
})

วิธีการแก้คือใส่ status code ลงไปด้วยเสมอ

res.json({ data: {} , message: 'user notfound' }).status(404)

แต่การใส่ status code ของแต่ละ Framework และภาษาจะต่างกันออกไปนะครับ อ่าน Docs กันดีๆ

ลองมาดู Categories ของ HTTP code แต่ละแบบกันครับ เอาที่ใช้กันหลักๆ

2xx Success

  • 200 OK — สำหรับ GET, PUT, PATCH และ POST และอาจจะมีการส่ง payload กลับไปด้วย ปกติก็จะส่ง Response ไปเสมออยู่แล้วถ้าดูจาก Mthod ด้านบน

3xx Redirection

  • 304 Not Modified — เป็นการบอกว่า Request นี้ส่ง cache กลับไปให้

4xx Client Errors

  • 400 Bad Request — เป็นการส่งไปบอกว่า HTTP body ไม่สามารถใช้งานได้ เช่นเราส่ง POST ไปแต่ API พบว่า body นั้นไม่ตรงตามที่ API กำหนดไว้ อาจจะฟิลด์ไม่ครบหรือส่งผิด Format

5xx Server Errors

  • เป็น Error ที่เกิดกับ Server อันนี้เราไม่ต้องทำอะไร

6. ในการดึงข้อมูลที่ซับซ้อนหรือมี state เยอะๆ ให้ทำ Query String ด้วย GET

การดึงเอาสถานะของหนังสือที่ยังขายอยู่เช่น

GET /api/v1/books?state=true

หรือการเลือกแสดงผลข้อมูลว่าต้องการที่จะดูฟิลด์ Database ในฟิลด์ไหนบ้าง

GET /api/v1/books?fields=name,price,author,type,categories

ถึงตรงนี้จะฟังดูแปลกๆ หน่อยแต่ถ้าจะทำแบบนี้เลือกฟิลด์ที่จะแสดง ผมแนะนำไปใช้ GraphQL ครับเป็นทางออกที่ดีกว่าการมากำหนดใน URL

อัพเดท: หรือไปใช้ POST แทนครับ

การทำ sorting หรือ paginate ก็เช่นกัน

GET /api/v1/books?fields=name,price,author,type,categories&sort=asc&page=1&limit=10

แบบนี้ก็ไปทำ GraphQL เถอะ พี่น้องครับ ถ้ายิ่งส่ง URL ยาวมันก็จะเกินขนาดอักษรไปทำให้บัคครับ

อัพเดท: หรือไปใช้ POST แทนครับ

ข้อควรจำ: อย่าลืมเช็ค Escape string ในกรณีที่ฐานข้อมูลเป็นแบบ Relationship เช่นพวกที่ใช้คำสั่ง SQL ทั้งหลายในการคิวรี่และอย่าลืมเช็คความถูกต้องของข้อมูลก่อนส่งไปดึงที่ Database เสมอ เพื่อไม่ให้โดน Cross-site scripting หรือ Injection ต่างๆ

7. การทำ Document

การทำ Document ผมมีตัวเลือกให้ 2 แบบคือ

  • ทำด้วย Swagger สามารถ Deploy ให้เป็น API จริงๆ ได้เพราะตอนนี้ Swagger มี Swagger codegen ที่ช่วยให้เราทำ API ได้เร็วมากแค่เขียน YAML มาแล้ว export ออกมาเป็นโค้ดได้เลย

8. การทำ Authentication

เพื่อให้เป็นไปตามกำหนดของ RFC7235 ว่าด้วยเรื่องการทำ Authorization ต้องมีรูปแบบดังนี้

Authorization = credentials

โดยใส่ไปใน header ของการ Request ทุกครั้ง เช่นใครใช้ JWT จะต้องเขียนแบบนี้

Authorization = Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1.....

และใน API ก็ทำการดักเอา ​key ชื่อ Authorization ไปใช้ในการทำงานต่อ

Credit : https://medium.com/algorithmtut/%E0%B9%81%E0%B8%99%E0%B8%A7%E0%B8%97%E0%B8%B2%E0%B8%87%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B8%AD%E0%B8%AD%E0%B8%81-rest-api-%E0%B8%95%E0%B8%B2%E0%B8%A1%E0%B8%A1%E0%B8%B2%E0%B8%95%E0%B8%A3%E0%B8%90%E0%B8%B2%E0%B8%99-d0426aa65df1