[ลองสรุป] การออกแบบ/เลือกใช้ HTTP Methods และ Response Code ของ REST API
จากคราวที่แล้ว ที่ผมได้ทำการ [ลองสรุป] กฏของการออกแบบ URIs จากหนังสือ REST API Design Rulebook โดย Mark Masse ไว้ แต่จากที่ผมได้มีโอกาสพูดคุยและประสบพบเจอในหลาย ๆ ที่ ก็มักจะมีคำถามที่นอกเหนือจากการใช้ URIs ในการทำ REST API ซึ่งหนึ่งในคำถามที่พบบ่อยก็ไม่พ้นเรื่องการเลือกใช้ HTTP Method และการเลือกใช้ Response Code ในแต่ละกรณี ผมจึงขอลองย้อนกลับไปที่หนังสือเล่มเดิม ซึ่งมีเนื้อหาที่คล้ายจะตอบคำถามที่เราเจอกันอยู่นี่แล้วกันครับ
HTTP/1.1
เนื่องจาก REST APIs ถูกคิดค้นขึ้นมา โดยรับเอาทิศทางการออกแบบที่กำหนดไว้ใน HTTP/1.1 ซึ่งระบุถึง Request Methods, Response Code และ Message Header เพราะฉะนั้นการออกแบบ REST APIs จึงจำเป็นจะต้องเข้าใจ HTTP/1.1 และใช้แนวทางของการออกแบบบนแนวทางนั้นเช่นกัน ซึ่งในการสรุปครั้งนี้ผมจะยังไม่พูดถึง Message Header ครับ
Request Methods
เนื่องจากการที่ Client ที่ต้องการจะเชื่อมต่อมายัง Server จะต้องมาในรูปแบบของ Request-Line ซึ่งเป็นส่วนนึงของ HTTP Request Message โดยที่ RFD 2616 กำหนดรูปแบบของ Request Line ไว้ดังนี้
Request-Line = Method + SP + Request-URI + SP + HTTP Version + CRLF
*** SP = Space, CR = C
arriage return, LF = Linefeed ***
โดยที่ HTTP Method แต่ละตัวมีหน้าที่ที่ชัดเจนเพื่อตอบแนวคิดการให้บริการ Resource ต่าง ๆ ให้กับ Client โดย วัตถุประสงค์ของและ Method คือ
- GET — ใช้เพื่อรับข้อมูลสถานะของ Resource นั้น ๆ
- HEAD — ใช้เพื่อรับ Metadata ซึ่งเกี่ยวข้องกับ Resource นั้น ๆ
- PUT — อาจจะใช้สำหรับการสร้าง Resource เข้าไปใน Resource ประเภท Store หรือ ปรับปรุงข้อมูลของ Resource
- DELETE — ใช้ลบ Resource ออกจากแม่ของมัน
- POST — อาจจะใช้สร้าง Resource ใหม่เข้าไปใน Resource ประเภท Collection และใช้การสั่งให้ตัวควบคุม Resource ทำงานอื่น ๆ ที่ไม่ใช้ CRUD (Create — Read — Update — Delete)
Rules of Request Methods
- [RULE]: ไม่ใช้ Method GET และ POST เพื่อทดแทนการทำ Action แบบอื่น ๆ ที่สามารถทำผ่าน HTTP Method — ได้ เช่น Method เป็น POST แต่แอบใส่ Action DELETE เข้าไปใน Header หรือ Body เพื่อให้ Server ทำการลบข้อมูล เพราะการทำแบบนี้จะทำให้ให้เกิดความเข้าใจผิด และสูญเสียความโปร่งใสของ HTTP Protocal ซึ่ง REST API ที่ออกแบบจะต้องไม่เอา HTTP Method ไปใช้อย่างผิดวัตถุประสงค์ เพราะเหตุที่ว่าอยากจะรองรับความต้องการของลูกค้าด้วย Method HTTP ที่มีจำกัด เพราะฉะนั้น เราจะออกแบบ HTTP Method ตามสิ่งที่เราจะพูดกันต่อไป ปล. ผมเคยเจอระบบที่มีแต่ Post อย่างเดียวแล้วทุก ๆ Request จะส่ง Action มาใน Body ซึ่งจะปวดหัวมากเวลาจะ Monitor ว่า Request ไหนน้อย/เยอะ เพราะต้องแกะ Body เท่านั้น หรือไม่ก็ต้องแก้ที่ App ลูกเดียว
- [RULE]: Method GET จะถูกใช้เพื่อรับข้อมูลหรือ ตัวแทนของ Resource ที่ Client ต้องการ เท่านั้น — ส่วนใหญ่แล้วการทำงานของ Web จะหนักไปที่ GET Method อยู่แล้ว เพราะฉะนั้น Client ควรจะวางใจได้เมื่อเรียก GET ซ้ำ ๆ ต้องทำได้โดยไม่มีผลกระทบอะไร ซึ่งมีผลให้เราใช้ความสามารถของ Cache ได้ ซึ่งจะเป็นการที่จะส่งข้อมูลเดิมกลับไปที่ Client โดยที่ไม่ต้องต่อกับ Server จริง ๆ (ซึ่งอันนี้ก็ขึ้นกับความต้องการว่าต้องการข้อมูลที่ทันสมัยแค่ไหน ข้อมูลมีการเปลี่ยนแปลงบ่อยไหม และข้อมูลต้องถูกต้องตรงตามข้อมูลที่เก็บจริง ๆ หรือเปล่า เพราะถ้าต้อง Query Data จาก Database ตลอดเวลา ก็อาจจะไม่ใช่เรื่องดีนักสำหรับการรองรับ Scale ใหญ่)
- [RULE]: Method HEAD ถูกใช้เพื่อดูข้อมูล Response Header — Client อาจจะใช้ Head ก็ต้องเมื่อต้องการข้อมูลที่อยู่ใน Response Header โดยไม่สนใจ Body เช่น การ เช็ค Last-Modified เพื่อดูว่าต้องดึงข้อมูลใหม่หรือยัง หรือจะให้ดู Content-Length ที่อยู่ใน Header ก็ได้ เพราะฉะนั้น Method HEAD ก็เหมือนกับ GET เพียงแต่ไม่เอา Body เท่านั้น
- [RULE]: Method PUT จะถูกใช้เพื่อการ Insert และ Update เข้าไปใน Resource ประเภท Store — PUT ถูกใช้ในการสร้าง Resource ใหม่ใน Store ด้วยการ Client จะต้องระบุ URI ของ Resource (เพียงตัวเดียว และระบุ Identifier) ที่ชัดเจน เช่น /users/12345 รวมถึงถูกใช้ในการเปลี่ยนแปลงแก้ไข Resource นั้น ๆ ด้วย เนื่องจาก Resource ที่ถูกสร้างจากการ PUT ถูกส่งมาจาก Client นั่นไม่ได้แปลว่า ข้อมูลที่ PUT ขึ้นมากับ ข้อมูลที่ถูก GET หลังจากนั้น (ด้วย ID เดียวกัน) จะต้องเหมือนกันเสมอ ยกตัวอย่างเช่น การที่เราจะจะยอมให้ Client ปรับเปลี่ยนข้อมูลเท่าที่เขาสามารถทำได้ และส่งมาใน Request เท่านั้น จุดสำคัญอีกอย่างของ PUT คือการตกลงกันของกรณีเกิด Conflict เนื่องจากเราใช้ PUT เพื่อ Insert และ Update การที่ Client 2 คนจะส่ง Request ที่ Id เดียวกันมาที่ REST นั่นอาจจะหมายถึงการทับกันไปเรื่อย ๆ หรืออาจจะหมายถึงการที่พยายามสร้างข้อมูลซ้ำกันก็ได้ (อาจจะเพิ่ม Header เพื่อบอกเช่น If-Unmodified-Since, If-Match อ่านเพิ่มเติม ที่นี่)
- [RULE]: Method PUT จะถูกใช้เพื่อเปลี่ยนแปลงข้อมูลเดิมของ Resource ได้ — นั่นแปลว่า Client ต้องบอกมาว่าจะเปลี่ยนแปลงอะไร
- [RULE]: Method POST จะถูกใช้เพื่อการสร้าง Resource ใหม่ใน Collection ที่กำหนด — Client จะใช้ POST ในการสร้าง Resource ใหม่ไปยัง Collection ที่ต้องการ เช่น /users ซึ่ง Request Body ของ POST อาจจะเสนอข้อมูลที่ใช้เป็นตัวแทนของ Resource ไปยัง Server ที่จะเก็บ Resource นั้น ๆ ได้ เช่นอาจจะส่ง UUID ไปให้ Server เพื่อใช้เป็น Id ของ Resource นี้ต่อไป ซึ่งโดยปกติในกรณีของ POST หน้าที่ Intial ค่าบางค่าของ Resource มักจะทำที่ Server ให้มองว่าเรากำลัง POST ไปที่ Webboard เราไม่สร้าง Id เองก่อน POST แน่นอน
- [RULE]: Method POST จะถูกใช้เพื่อเพื่อประมวลผลคำสั่งอื่น ๆ ที่ไม่ใช่ CRUD — Client จะใช้ POST สำหรับเรียกใช้ Function ที่ควบคุมการทำงานของ Resource ที่นอกเหนือจาก CRUD Function ซึ่งนั่นแปลว่าจะต้องส่งข้อมูลตาม Function นั้นมาในรูปแบบของ Header หรือไม่ก็ Body ด้วย Protocol HTTP ได้ออกแบบให้ POST เป็นลักษณะของ Method ปลายเปิด เพราะงั้น POST จึงถูกใช้เป็น Operation อะไรก็ได้ ที่ไม่ไปซ้ำกับ การขอดู การเก็บ และการลบ Resource และด้วย HTTP Protocol ระบุให้ POST Request เป็น unsafe และ non-idempotent (อ่านเพิ่มเติม) นั่นแปลว่าเราคาดผลลัพท์ที่จะเกิดขึ้นไม่ได้ และไม่รับประกันว่าการทำซ้ำ จะสามารถทำได้โดยไม่มีผลข้างเคียง เช่น การส่งคำสั่งซื้อซ้ำ ๆ ผ่าน POST ก็จะมีผลใช้การตัดบัตรเครดิตของลูกค้าเกิดขึ้นมากกว่า 1 ครั้งได้ (ซึ่งอันนี้ต้องจัดการเอง)
- [RULE]: Method DELETE จะถูกใช้เพื่อลบ Resource ออกจาก Collection หรือ Store ที่เก็บมัน — DELETE Method ต้องระบุ Resource ที่ต้องการลบ เช่น DELETE /users/1234 แปลว่าต้องการลบ Resource 1234 ใน Collection ที่ชื่อ Users ซึ่งหลังจากนั้นเมื่อทำการเรียกใช้ GET หรือ HEAD มาที่ Resource ดังกล่าวจะต้องได้ Status Code 404 (“Not Found”) กลับไป ใน HTTP Protocol แล้ว DELETE Method ค่อยข้างจะชัดเจนในหน้าที่ของมัน และไม่ควรถูกใช้ไปในความหมายอื่น ๆ กรณีที่ต้องการให้ API รองรับการ ลบแบบชั่วคราว หรือแค่เปลี่ยน State ของข้อมูลให้เป็นแบบอื่น ๆ เช่น Suppended เราอาจจะใช้ Method POST แทน
- [RULE]: Method OPTIONS. อาจจะถูกใช้เพื่อดู Metadata ของ Resource ว่ารองรับการทำงานออะไรบ้าง — เช่น Client อาจจะใช้ OPTIONS ไปยัง Server แล้วได้รับการตอบกลับมาใน Response Header ว่า Allow: GET, PUT, DELETE เป็นต้น ซึ่งในบางกรณีเราอาจจะส่ง Link ของแต่ละ Operation กลับไปใน Body ด้วยก็ได้
PUT vs POST Method
ผมเองอ่านมาถึงตรงเรื่อง Method PUT กับ POST แล้วมีคำถามกับตัวเองว่า แล้วมันต่างกันยังไงนะ ถ้าผมต้องการสร้าง Resource ใหม่ตกลงผมจจะใช้อะไร ผมเลยไปหาข้อมูลปแล้วพบตารางเปรียบเทียบที่น่าสนใจ ขออนุญาติเอามาใส่แล้วกัน
จะเห็นได้ว่า PUT ทำโดยตรงกับ Resource โดยต้องระบุ Id เข้าไปจาก Client ซึ่งนั่นแปลว่า การจัดการสร้าง Resource ของ PUT ทำที่ Client ส่วน POST ระบุแค่ Collection แล้ว REST API จะสร้าง Id ขึ้นมาเพื่อ Response ให้ Client เพื่อเอาไปใช้อีกที นั่นแปลว่าการสร้าง Resource ทำโดย Server
จุดอื่น ๆ ที่สำคัญคือ PUT เป็น Idempotent ซึ่งแปลว่าทำซ้ำกี่ครั้งก็ได้ผลเหมือนเดิม และสามารถ Cache Response ที่เกิดขึ้นได้ (ผมยังนึก Case ที่ผมจะ Cache Response ของ PUT ไม่ออกอะนะครับ) ส่วน POST สร้าง Id ใหม่ทุกครั้งนั่นแปลว่ารับแต่ละครั้งจะได้ผลไม่เหมือนเดิม
Response Status Codes
เนื่องจาก REST API ใช้ Status-Line ซึ่งเป็นส่วนนึงของ HTTP Response Message เพื่อบอกผลของการเรียกใช้ API โดยที่ RFD 2616 กำหนดรูปแบบของ Status-Line ไว้ดังนี้
Status-Line = HTTP Version + SP + Status-Code + SP + Reason-Phrase + CRLF
*** SP = Space, CR = C
arriage return, LF = Linefeed ***
HTTP Protocol ได้กำหนด มาตราฐานของ Status Code ซึ่งสามารถอธิบายผลของการเรียกใช้ผ่าน Request ต่าง ๆ ได้ โดยแบ่งออกเป็น 5 กลุ่มใหญ่ ๆ ดังนี้
- 1xx: Informational — ข้อมูลที่ใช้ในการสื่อสารในระดับของ Protocol เอง
- 2xx: Success — เพื่อบอกว่า Request ที่ส่งมา ถูกยอมรับเรียบร้อยแล้ว
- 3xx:Redirection — เพื่อบอก Client ว่า เขาจะต้องทำอะไรบางอย่าง เพื่อให้สิ่งที่เขาต้องการนั้นสำเร็จจริง ๆ
- 4xx:Client Error — ข้อผิดพลาดต่าง ๆ ที่ชี้นิ้วบอกว่า Client มึงนั่นแหละผิด
- 5xx:Server Error — ข้อผิดพลาดที่เป็นความรับผิดชอบของฝั่ง Server
Status Code 2xx for Success
- [RULE]: 200 (“OK”) จะถูกใช้เพื่อบอกว่า Request สำเร็จ — โดยไม่ระบุได้ระบุว่าสำเร็จแบบไหน
- [RULE]: 200 (“OK”) จะต้องไม่ถูกใช้เพื่อบอกว่า Clientในกรณีเกิดข้อผิดพลาด — และบังคับให้ Client ว่ากรุณาอ่าน Response Body ดูก่อนนะ ว่า OK จริง หรือ OK หลอก ๆ แต่ Error
- [RULE]: 201 (“Created”) จะถูกใช้ในกรณีที่ Resource ใหม่ถูกสร้างสำเร็จ
- [RULE]: 202 (“Accepted”) จะถูกใช้ในกรณีที่ API เป็นลักษณะ Asynchronous เพื่อบอกว่า Request ของคุณได้รับเรียบร้อยแล้วนะ — (ไม่ได้แปลว่า Action จะทำสำเร็จนะ เพราะเดี๋ยวเราจะบอกกลับไปเมื่อเสร็จ หรือ Error)
- [RULE]: 204 (“No Content”) จะถูกใช้เมื่อเราตั้งใจจะให้ Response Body เป็นค่าว่าง — 204 มักจะถูกใช้กับ Response ของ Method ประเภท PUT, POST หรือ DELETE ในกรณีที่ Service ไม่อนุญาตให้ส่งอะไรกลับไปใน Body
Status Code 3xx for Redirection
- [RULE]: 301 (“Moved Permanently”) จะถูกใช้เมื่อ Resource ถูกย้ายไปที่อื่นแล้ว — ซึ่งอาจจะเกิดจากการออกแบบ URI ของ Resource นั้นใหม่ และตัว API เองอาจจะส่ง URI กลับไป ในResponse Header ที่ชื่อว่า Location ด้วย
- [RULE]: 302 (“Found”) อันนี้ไม่ควรใช้
- [RULE]: 303 (“See Other”) จะถูกใช้เมื่อต้องการให้ Client ไปดูต่อที่ URI อื่น ๆ — เรามักใช้กับการประมวลผล Resource เพื่อบอกว่ามันทำงานเสร็จแล้ว แต่ไม่ต้องการส่ง Response body กลับมา เลยส่งกลับมาเป็น URI ที่จะใช้ดูผลของการทำงานแทน ซึ่งอาจจะเป็น GET Method ที่ URI อื่น ๆ ผ่านทาง Response Header ที่ชื่อ Location
- [RULE]: 304 (“Not Modified”) จะถูกใช้เพื่อลดการใช้ Bandwidth — ซึ่งStatus Code นี้จะคล้ายกับ 204 (“No Content”) เพราะมันไม่ส่ง Response Body กลับมา สิ่งที่ต่างกันคือ 204 จะถูกใช้ เมื่อไม่มีอะไรจะส่งกลับมาทาง Body แล้ว แต่ 304 ถูกใช้เมื่อมีการเปลี่ยนแปลงสถานะบางอย่าง แต่สถานะนั้น Client ก็มีสถานะล่าสุดอยู่ที่ตัวอยู่แล้ว (อันนี้ผมไม่เคยใช้เลย 555+)
- [RULE]: 307 (“Temporary Redirect”) จะถูกใช้เพื่อบอก Client ว่าให้ส่ง Request มาใหม่ใน URI อื่น ๆ
Status Code 4xx for Client Errors
Note: สำหรับ Error ในกลุ่ม 4xx เราจำเป็นจะต้องใส่รายละเอียดของ Error เข้าไปใน Response Body (ยกเว้น Method HEAD)
- [RULE]: 400 (“Bad Request”) สามารถใช้เพื่อบอกว่า Failed นะแต่ไม่ได้ระบุชัดเจน
- [RULE]: 401 (“Unauthorized”) จะต้องใช้ในกรณีที่เมื่อเกิดปัญหาเกี่ยวกับการเข้าถึงอย่างไม่ถูกต้องตามสิทธิ์ของ Client — เช่นการที่ Client พยายามจะทำ Operation บางอย่างใน Resource ที่เราป้องกันไว้ โดยเขาไม่มีสิทธิ์ที่จะทำ Operation ซึ่งมันอาจจะหมายถึงการใส่ Credentials ที่ใช้ยืนยันตัวตนมาผิด หรือการไม่ใส่ Credential มาเลย
- [RULE]: 403 (“Forbidden”) จะใช้เพื่อห้ามการเข้าถึงโดยมีสิทธิ์ไม่เพียงพอต่อการรับอนุญาต — ถูกใช้เมื่อ Client เข้ามาอย่างถูกต้อง แต่ REST API ปฏิเสธในสิทธิดังกล่าว 403 ต่างกับกรณีของ 401 ตรงที่ 401 ไม่การยืนยันตัวตนผ่าน Credential แต่ 403 ถูกใช้ในระดับของสิทธิ์การเข้าถึง Application เช่น Client อาจจะสามารถเข้าถึง Resource บางอันที่เขาสร้างขึ้นมา แต่ไม่สามารถเข้าถึง Resource ทุกตัวใน Collection เดียวกันได้ ซึ่งเมื่อเขาพยายายจะเข้าถึง Resource ที่อยู่นอกเหนือจากสิทธิ์ REST API จะต้อง Response 403 กลับไป
- [RULE]: 404 (“Not Found”) จะต้องใช้เมื่อ Client เรียกใช้ URI ไม่สามารถ Map กับ Resource ใด ๆ ได้
- [RULE]: 405 (“Method Not Allowed”) จะต้องใช้เมื่อ REST API ไม่ได้ รองรับ HTTP Method ที่เรียกมา — 405 จะต้องบอกกลับไปใน Allow ของ Response Header ว่า API รองรับ Method อะไรบ้าง เช่น Allow : GET, HEAD
- [RULE]: 406 (“Not Acceptable”) จะต้องใช้เมื่อไม่รองรับตาม Media Type ที่ Request มา — เช่น รองรับแค่ application/json แต่ Client ส่งมาเป็น ใน Request Header ว่า Accept application/xml
- [RULE]: 409 (“Conflict”) สามารถใช้เพื่อบอกว่ากำลังฝ่าฝืนสถานะของ Resource นั้น — เป็น Error ที่เกิดขึ้นจากการพยายามทำให้ Resource แอยู่ในสถานะที่เป็นไปไม่ได้ หรือ ไม่สอดคล้องกับสถานปัจจบัน เช่น พยายามลบ Resource Type Store ที่ยังมีของอยู่ หรือ การพยายามจะบอกให้ Order นี้สำเร็จ ทั้ง ๆ ที่ Order ยกเลิกไปแล้วก่อนหน้านี้
- [RULE]: 412 (“Precondition Failed”) สามารถใช้เพื่อรองรับกับ Operation ที่ต้องมีเงื่อนไข ซึ่งไม่ได้ทำตามก่อนที่จะสร้าง Request นี้
- [RULE]: 415 (“Unsupported Media Type”) จะต้องใช้เมื่อไม่สามารถประมวลผลข้อมูลตาม Media Type ที่อยู่ในข้อมูล Payload ของ Request ได้ — อันนี้ดูจาก Content-Type
Status Code 5xx for Server Errors
- [RULE]: 500 (“Internal Server Error”) ใช้เพื่อบอกว่า API ทำงานผิดพลาดเองนะ — โดยปกติ REST API Framework ต่าง ๆ จะ Return Status Code 500 เมื่อประมวลผลอะไรบางอย่างแล้วเกิด Exception ขึ้นโดยที่ไม่ได้ Handle ไว้ โดยที่ในการทำ REST API จะต้องไม่เกิด 500 ในกรณีที่เป็นความผิดของ Client เพื่อให้ Client สามารถที่จะลองสร้าง Request แบบเดิมเข้ามาใหม่ เพื่อหวังว่าคราวนี้จะได้ Response ที่เปลี่ยนไปเมื่อมีการปรับปรุง REST API ให้ถูกต้อง
สรุป
เพื่อให้เราเข้าใจ และออกแบบ ได้อย่างมีประสิทธิภาพ และไม่สร้างปัญหาใหม่จากการใช้งานอย่างผิด ๆ เราจำเป็นต้องเข้าใจวัตถุประสงค์ของผู้สร้าง และแนวคิดในการนำมันมาใช้ออกแบบของเรา เพื่อให้การออกแบบ REST API นั่นสามารถเอาไปใช้กับเครื่องไม้เครื่องมือ ที่เราจะใช้ต่อไป ไม่ว่าจะเป็น Framework ในการพัฒนา จะเป็น API Gateway หรือแม้แต่อุปกรณ์ Network ต่าง ๆ ที่จะช่วยให้เราสามารถดูแลรักษาระบบโดยไม่ต้องพัฒนาต่อยอดไปอีกเยอะแยะ เพราะเหล่าเครื่องมือที่เราจะใช้ต่อไปนั้น ก็พยายามที่จะสร้างบนพื้นฐาน หรือบนมาตราฐานเดียวกัน กับทั้งของ HTTP Protocol หรือแนวคิดในการออกแบบ REAT API ที่ถูกต้อง
มาถึงตรงนี้ ผมคงไม่ได้คิดจะฝึกแปล หรือพยายามสรุปหนังสือเล่มนี้ต่อแล้วนะครับ เนื้อหาส่วนอื่น ๆ ไม่ว่าจะเป็น กฏสำหรับ Header หรือ Body ซึ่งเป็นส่วนที่มีความสำคัญมากใน REST API นั้น ผมขอแนะนำ เพื่อน ๆ ที่อ่านมาถึงจุดนี้ลองหาซื้อหนังสือเล่มดังกล่าวมาดูในรายละเอียดกันต่อไปนะครับ ผมขอฝากหนังสือดี ๆ ไว้เพื่อให้เราได้อ่านกันครับ