สำหรับบทความนี้ผมจะมาอธิบายการออกแบบ 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
- ใช้
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
— ดึงข้อมูลทั้งหมดของ TagsGET /tags/1
— ดึงข้อมูล Tag ที่มี ID 1POST /tags
— สร้าง Tag ใหม่
ตัวอย่างถัดมาคือการใช้ PUT, PATCH, DELTE
PUT /tags/1
— คือการอัพเดทข้อมูล tag ที่มี ID 1แต่ถ้าไม่มี ID 1จะต้องสร้างข้อมูลใหม่ข้อมูลขึ้นมาPATCH /tags/1
— คือการอัพเดทข้อมูล tag ที่ ID 1DELTE /tags/1
— คือการลบข้อมูล tags ที่ ID 1
ข้อควรจำ: หลายๆ คนคงสัยว่า PUT/PATCH ต่างกันอย่างไร จริงๆ แล้วถ้าเป็นเรื่อง Action ต่างกันที่ ถ้า PUT และ Server หาข้อมูลไม่เจอจะต้องสร้างข้อมูลชุดใหม่ขึ้นมาแทน หรือเราจะเรียกมันว่า upsert (update or insert) แต่!!!! PUT ต้องส่งข้อมูลทั้งหมดมานะครับ หมายความว่าถ้าโครงสร้าง Database เราออกแบบว่า Tag schema หน้าตาแบบนี้
สิ่งที่เกิดขึ้นกับ 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 เป็น 1GET /tags/1/articles/16
— ดึงข้อมูลบทความที่มี ID เป็น 16 และต้องอยู่ภายใต้ tag ที่มี ID เป็น 1PUT /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 กำหนดไว้ อาจจะฟิลด์ไม่ครบหรือส่งผิด Format401 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
ไปใช้ในการทำงานต่อ
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