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

NSLog0
NSLog0
Feb 2, 2020 · 4 min read

โพสเก่าจาก Blog เดิม เมษายน 28, 2016

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

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

คำที่ควรรู้

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

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

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

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

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

  • /addTags
  • /updateTags
  • /removeTags
  • /removeAllTags

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

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

  • ใช้ plural nones ไม่ใช้ Singural none หรือพูดง่ายๆ ว่าเติม (s) ให้คำนามลง ตัวไหนมี (y) ให้เป็นเป็น (i) แล้วเติม (es)
  • ใช้ HTTP Mrthod ในการทำ CRUD (GET, POST, PUT, PATCH, DELETE)

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

  • GET /tags — ดึงข้อมูลทั้งหมดของ Tags
  • GET /tags/1 — ดึงข้อมูล Tag ที่มี ID 1
  • POST /tags — สร้าง Tag ใหม่

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

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

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

id -> int, NOT NULL |auto increment
name -> string, NOT NULL
lv -> int, NOT NULL
description -> string, NULL

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

  • ส่ง id, name, lv มาเมื่อ Server ได้รับ request และทำการหาข้อมูลใน Database แล้วค้นเจอว่ามี ID นั้นๆ อยู่ก็จะทำการเอา name, lv ไปอัพเดทข้อมูลให้กับ ID ที่เราส่งมา
  • ถ้า id ที่ส่งมาผ่าน PUT ไม่มีใน Database สิ่งที่เกิดขึ้นคือ PUT ต้องสร้างข้อมูลใหม่จาก payload ที่ส่งมาก็คือ id, name, lv

ส่วน 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
  • GET /tags/1/articles/16 — ดึงข้อมูลบทความที่มี ID เป็น 16 และต้องอยู่ภายใต้ tag ที่มี ID เป็น 1
  • PUT /tags/1/articles/20 — ในทางเดียวกันคืออัพเดทข้อมูล articles ที่มี ID 20 ถ้าไม่มีก็ใช้สร้างขึ้นมาใหม่ แต่แบบนี้จะทำให้ Over engineering ไป เราสามารถใช้แค่ PUT /articles/20 แบบนี้ไปตรงๆ ได้เลย

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 ด้านบน
  • 201 Created — สำหรับ POST เมื่อมีการสร้างข้อมูลสำเร็จและถ้าใช้ POST ในการสร้างข้อมูลขึ้นเราจะใช้ 201 เสมอ
  • 204 No Content — สำหรับ DELETE หรือบางที POST PUT PATCH เป็นการบอกว่าการประมวลผล Request สำเร็จแต่ไม่มีข้อมูลส่งไป

3xx Redirection

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

4xx Client Errors

  • 400 Bad Request — เป็นการส่งไปบอกว่า HTTP body ไม่สามารถใช้งานได้ เช่นเราส่ง POST ไปแต่ API พบว่า body นั้นไม่ตรงตามที่ API กำหนดไว้ อาจจะฟิลด์ไม่ครบหรือส่งผิด Format
  • 401 Unauthorized — ตัวนี้สำคัญมากใช้บ่อยสุดๆ เพราเป็นการระบุว่าการยืนยันตัวตนไม่สำเร็จ อาจจะเพราะ Token ผิด ถ้าการระบุตัวตนใช้ Token base หรือ Username/Password ผิด
  • 403 Forbidden — เป็นการบอกว่าไม่มีการให้เข้าถึง Action ที่ Request มาได้ แต่ว่าการระบุตัวตนสำเร็จก็คือ Token หรือ Username/Password ถูกแต่ว่า User นั้นไม่มีสิทธิ์ใน API นั้นๆ
  • 404 Not Found — Resource ไม่มีอยู่บน Server หรือ API ค้นหาของไม่เจอ
  • 405 Method Not Allowed — ไม่อนุญาตให้ทำการเรียก API ด้วย Method ที่ไม่ได้กำหนดไว้ เช่น หากต้องการสร้างข้อมูลใหม่ปกติเราจะใช้ POST แต่มีการใช้ PUT เรียกเข้ามาแทน

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 ออกมาเป็นโค้ดได้เลย
  • ทำ Docs ด้วย POSTMAN และยังสามารถทำ ENV Mockup หรือทำ Load Test ตัว API ใน POSTMAN ได้เลย และยังทำเป็นหน้า Web ส่งให้กันได้ด้วย

8. การทำ Authentication

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

Authorization = credentials

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

Authorization = Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1.....

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

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

AlgorithmTut

May the force be with you. **Tut stand for Tutorial**

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store