Microservices Architecture: Concepts, Design, and Best Practices

บทความโดย ผศ.ดร.ณัฐโชติ พรหมฤทธิ์ และ อ.ดร.สัจจาภรณ์ ไวจรรยา
ภาควิชาคอมพิวเตอร์
คณะวิทยาศาสตร์
มหาวิทยาลัยศิลปากร



ยุคดิจิทัลในปัจจุบัน การพัฒนา Software ในแบบเดิม เช่น Monolithic Architecture ที่ Function ในการทำงานของธุระกิจจะถูกรันอยู่ใน Process เดียวกัน อาจไม่เพียงพอต่อความต้องการที่ซับซ้อนและเปลี่ยนแปลงตลอดเวลาของธุรกิจสมัยใหม่

Microservices Architecture เป็นแนวคิดหนึ่งที่ได้รับความนิยมในการออกแบบและพัฒนา Software เนื่องจากมันช่วยให้การจัดการระบบขนาดใหญ่เป็นเรื่องง่ายขึ้น

เราจะเริ่มจากการอธิบายแนวคิดหลักการของ Microservices และเปรียบเทียบกับ Monolithic แบบดั้งเดิม จากนั้นจะกล่าวถึงหลักการ 12-Factor App ซึ่งเป็นแนวทางสำคัญในการพัฒนา Microservices ให้มีประสิทธิภาพ รวมถึงการเข้าใจความแตกต่างระหว่าง Single Page Application (SPA) และ Multi-Page Application (MPA) ซึ่งเป็นรูปแบบการพัฒนาฝั่ง Frontend 2 รูปแบบหลัก

นอกจากนี้เราจะมีการเจาะลึกถึงการออกแบบ REST API ซึ่งเป็นหัวใจสำคัญของการสื่อสารระหว่าง Frontend แบบ SPA กับ Microservices โดยเฉพาะในบริบทของ E-commerce Paltform เพื่อให้การพัฒนาระบบเป็นไปอย่างมีประสิทธิภาพ

แนวคิดของ Microservices

Microservices เป็นวิธีการในการพัฒนา Software หรือ Application โดยการแบ่ง Function ออกเป็น Service ขนาดเล็ก (Functional Decomposition) ที่แต่ละ Service มีความรับผิดชอบที่ชัดเจนตามความต้องการทางธุรกิจ (Bounded Context) ซึ่งจะมีการรันอยู่ใน Process ของตัวเอง และแยกกัน Deploy ได้อย่างอิสระ โดยอาจมีการใช้ภาษาในการเขียนโปรแกรม และ Data Storage Technology ที่แตกต่างกัน แต่ละ Service จะมีการสื่อสารระหว่างกันด้วย API โดยใช้ Protocol ที่ค่อนข้าง Lightweight เช่น HTTP ซึ่งเป็น Stateless Protocol ที่คําขอแต่ละรายการเป็นอิสระต่อกัน และ Server จะไม่เก็บข้อมูลใด ๆ เกี่ยวกับคําขอก่อนหน้า

ขณะที่ในการพัฒนา Application แบบ Monolithic นั้น ส่วนประกอบทั้งหมดจะถูก Deploy อยู่ภายใน 1 Process หรือ Instance เดียว การสื่อสารระหว่างส่วนประกอบต่าง ๆ เกิดขึ้นภายใน Instance โดยไม่ต้องผ่าน External API ซึ่งข้อมูลของ Component ต่าง ๆ มักถูกจัดเก็บอยู่ใน Database เดียว

การแบ่ง Function ออกเป็น Service ขนาดเล็ก ทำให้ระบบมีความสามารถในการปรับขนาดได้ (Scalability) ด้วยการทำซ้ำ Service หรือ Instance ซึ่งระบบที่สามารถ Scale ได้จะมีความสามารถในการรับ Load ได้มากขึ้น นอกจากนี้ยังเป็นการเพิ่มความพร้อมใช้งาน (Availability) ของระบบ จากการมีความซ้ำซ้อนกันของ Instance (Redundancy) ทำให้เหมาะสำหรับระบบของธุระกิจที่มีการเติบโตสูง และมีทีมพัฒนาจำนวนมาก

เราสามารถสรุปประเด็นสำคัญของ Microservices Architecture ได้ดังต่อไปนี้

  1. เป็นการแบ่ง Function ของ Monolithic Application ออกเป็น Service ขนาดเล็ก แต่ละ Service มีความเป็นอิสระต่อกันสามารถใช้ทีมพัฒนา Test และ Deploy ที่แตกต่างกันซึ่งมีการปรับให้เหมาะสมกับแต่ละส่วนของธุระกิจ มีประโยชน์สําหรับองค์กรขนาดใหญ่ที่ต้องการปรับปรุงประสิทธิภาพการทํางาน
  2. แต่ละ Service มีการสื่อสารระหว่างกันอย่างหลวม ๆ ผ่าน API โดย Frontend และ Backend จะสื่อสารกันผ่าน REST API ในขณะที่การสื่อสารระหว่าง Service ด้วยกันเอง มักใช้ RPC หรือ Message Queue
  3. แต่ละ Service มีขอบเขตของการให้บริการที่ชัดเจนตามความต้องการทางธุรกิจ (Bounded Context)
  4. ในทางปฏิบัติมักใช้แนวทางของทีมพัฒนาแบบ DevOps ทีมมีขนาดเล็ก เป็น Full-stack หรือมีครบทั้ง Fontend, Backend และผู้ดูแลระบบ (Operation)
  5. ออกแบบให้สามารถ Scale ได้ในแนวนอน (Horizontally Scalable) มีความทนทานต่อความล้มเหลว
  6. มีการกำกับดูแลแบบกระจายอำนาจ ทีมสามารถเลือกเทคโนโลยีที่เหมาะสมกับ Service ของเขาได้

ดังนั้น Microservices Architecture จึงเป็นรูปแบบหนึ่งในการปรับขนาดของ Distributed System ที่ประกอบด้วยชุดของคอมพิวเตอร์หรือที่เรียกว่า Node ที่ทํางานร่วมกันเพื่อให้บริการเฉพาะ

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

12-Factor App สำหรับการพัฒนา Microservices

12-Factor App มีความสัมพันธ์กับ Microservice อย่างใกล้ชิด โดยทั้ง 2 แนวคิดมีจุดมุ่งหมายในการสร้าง Application ที่เชื่อถือได้ สามารถ Scale ได้ดี และบำรุงรักษาได้ง่าย

12-Factor App สามารถนำมาใช้เป็นแนวทางในการออกแบบและพัฒนา Microservice ให้มีประสิทธิภาพได้เป็นอย่างดี

ใน 12-Factor App นั้น Processes คือ First Class Citizen

12-Factor App Principle

Codebase

Codebase ควรมีที่เดียวเพื่อเก็บ Code ทั้งหมดของ Project และจัดการโดยใช้ Version Control เช่น GitHub หรือ GitLab ฯลฯ ทั้ง Test, UAT และ Production Environment ควรมาจาก Codebase เดียวกัน

Codebase คือ ชุดของ Code ที่ใช้ในการพัฒนา Software ได้แก่ Source Code และไฟล์อื่น ๆ ที่จำเป็น เช่น Image, Audio คู่มือการใช้งาน และเอกสารต่าง ๆ ที่เกี่ยวข้อง, Script สำหรับการ Build, Test และ Deploy ไฟล์การตั้งค่า รวมทั้ง Code สำหรับการทดสอบโดยอัตโนมัติ ฯลฯ

Codebase เป็นส่วนสำคัญในการพัฒนา Software ทำให้ทีมพัฒนาสามารถทำงานร่วมกัน ติดตามการเปลี่ยนแปลง และบำรุงรักษา Project ได้อย่างมีประสิทธิภาพ ในทางปฏิบัติสำหรับการพัฒนา Software แบบ Microservices เราสามารถแยกเก็บ Codebase ของแต่ละ Service ลงในแต่ละ Git Repository

Dependency

List ของสิ่งที่จำเป็นในการแปลง Source Code ให้อยู่ในรูปแบบที่คอมพิวเตอร์ หรือ Execution Environment สามารถเข้าใจได้ (Compiled Atifact) ควรเก็บไว้ใน Codebase

module example.com/hello

go 1.21.0

require (
    github.com/gin-gonic/gin v1.7.4
    github.com/go-sql-driver/mysql v1.6.0
)
ตัวอย่าง Dependency File (go.mod) ใน Golang 

Config

แยกการตั้งค่าที่สําคัญ เช่น ข้อมูลสำหรับการ Connect Database ออกจาก Code เพื่อให้สามารถเปลี่ยนมันได้โดยไม่ต้องเขียน Code ใหม่ เมื่อต้อง Deploy ในแต่ละ Environment โดยในทางปฏิบัติเราสามารถเก็บ Config แยกแต่ละ Enveronment ไว้นอก Codebase ได้

Backing Services

Backing Services หรือบริการต่าง ๆ ที่ Application ต้องพึ่งพาเพื่อทำงานได้อย่างสมบูรณ์ โดยเป็นส่วนที่แยกออกจาก Application หลัก และมักจะเข้าถึงผ่านเครือข่าย เช่น Database, ระบบ Cache, Message Queue, SMTP Service สำหรับส่ง Email, File Service ควรจัดการเชื่อมต่อผ่านการตั้งค่าผ่าน Environment Variables ไม่ใช่การ Hard-code

Build, Release, Run

แยกขั้นตอนการพัฒนาและการ Deploy Application ออกเป็น 3 ส่วน ได้แก่ Build คือ การแปลงโค้ดเป็น Build Atifact, Release คือ การรวม Build Artifact กับการตั้งค่าสำหรับ Environment ที่จะ Deploy และ Run คือ การรัน Application ใน Execution Environment อย่างชัดเจน

การแยก Build, Release, Run ทำให้มั่นใจได้ว่าส่วนที่ทดสอบแล้วจะถูก Deploy จริง ง่ายต่อการ Rollback หากเกิดปัญหา สนับสนุนการทำ Continuous Deployment

นอกจากนี้ยังช่วยลดความเสี่ยงจากการแก้ไข Code บน Production Environment แยกหน้าที่ความรับผิดชอบได้ชัดเจนระหว่างทีมพัฒนาที่สามารถโฟกัสที่การ Build ทีม Operation ที่จัดการเรื่องการ Release และการ Run และทำให้การจัดการการตั้งค่าแยกจาก Code ซึ่งสามารถเปลี่ยนแปลงการตั้งค่าโดยไม่ต้อง Rebuild

สำหรับการพัฒนา Software แบบ Microservices แต่ละ Service จะมีกระบวนการ Build, Release, Run ของตัวเอง

Processes

ออกแบบ Application เพื่อให้แต่ละส่วนไม่ต้องพึ่งพาคอมพิวเตอร์หรือหน่วยความจำเป็นการเฉพาะ โดยไม่เก็บ State ต่าง ๆ  ไว้ภายใน Process (No Local State) แต่ข้อมูลที่ต้องเก็บควรอยู่ใน Backing Service ซึ่งจะช่วยให้ Application มีความยืดหยุ่น Scale ได้ง่ายเพื่อรองรับ Load ที่เพิ่มขึ้น และทนทานต่อความล้มเหลว

Port Binding

Application ควรเปิดให้เข้าถึงได้ผ่าน Network Port โดยตรง โดยไม่ต้องพึ่งพา Web Servcer ภายนอก เช่น Apache หรือ Nginx ในการรัน

เมื่อ Application สามารถควบคุม HTTP Request กำหนด Route และจัดการ Thread ได้โดยตรงด้วยตัวเอง การ Deploy จะง่ายขึ้นและสามารถ Scale ได้อย่างอิสระ

Port Binding ช่วยให้ Application เป็นอิสระมากขึ้น ง่ายต่อการ Deploy และ Scale ในสภาพแวดล้อมแบบ Cloud-native หรือ Containerized

หมายเหตุ เราสามารถพัฒนา PHP Application ให้สอดคล้องกับหลักการ Port Binding ได้ แม้ว่าบ่อยครั้งมันมักถูกใช้ร่วมกับ Web Server ภายนอกก็ตาม

เพื่อให้การพัฒนา PHP Application สอดคล้องกับหลักการ Port Binding ผู้พัฒนาสามารถใช้ PHP-FPM (FastCGI Process Manager) ร่วมกับ Docker โดยให้ Nginx ทำหน้าที่รับ HTTP Request จากภายนอกและส่งต่อไปยัง PHP-FPM Container (PHP Application)

Concurrency

ออกแบบ Application ให้สามารถจัดการงานได้มากขึ้นโดยการเพิ่ม Copy ของ Instance (Horizontal Scaling) แทนการเพิ่มขนาดของ Instance (Vertical Scaling) หรือเรียกว่าการ Scale โดยการเพิ่ม Process เพื่อให้สามารถทำงานได้พร้อมกัน (Concurrency)

Disposability

ออกแบบให้ Application สามารถเริ่มทำงานอย่างรวดเร็วภายในไม่กี่วินาที และปิดตัวลงอย่างสง่างาม โดยการรับสัญญาณ SIGTERM (Signal Terminate) แล้วหยุดการทำงานอย่างเป็นระบบ ด้วยการหยุดรับ Request ใหม่ ทำงานเก่าให้เสร็จสิ้น แล้วจึงปิดตัว

Dev/Prod Parity

ทำให้มั่นใจว่า Environment ที่ใช้ในการพัฒนา และทอสอบ Application เหมือนกับ Environment ที่ใช้บน Production เพื่อหลีกเลี่ยงความ Surprise ลดปัญหา "Works on my machine"

Logs

การเก็บบันทึกสิ่งที่เกิดขึ้นใน Application เพื่อให้สามารถเข้าใจและแก้ปัญหาได้ ในระบบที่มีหลาย Process หรือหลาย Service การเก็บ Logs ควรถูกรวมศูนย์ โดยแต่ละ Service ไม่ควรจัดการกับการเก็บ Log เอง

Admin Processes

ควรแยกงานพิเศษ เช่น การ Migrate ฐานข้อมูล การทํา Data Cleansing ออกจากการทำงานปกติของ Application แต่เก็บไว้ใน Codebase เดียวกัน และใช้การตั้งค่าแบบเดียวกันกับ Application หลักผ่าน Environment Variable

Single Page Application vs Multi-Page Application

ขณะที่เราสามารถนำหลักการของ 12-Factor App มาประยุกต์ใช้ในการออกแบบ Backend ให้บำรุงรักษาได้ง่าย และสามารถ Scale ได้ Single Page Application (SPA) ก็เป็น Modern Frontend Framework แบบหนึ่ง ที่มีความเข้ากันได้ดีกับ Backend ที่พัฒนาแบบ Microservices เพื่อสร้างแอปพลิเคชันที่มีประสิทธิภาพสูง

ในช่วงไม่กี่ปีที่ผ่านมา มีการเปลี่ยนแปลงที่สำคัญในวิธีการสร้าง Web Application จาก Multi-Page Application (MPA) ไปสู่ Single Page Application (SPA)

SPA ทำให้ผู้ใช้งานได้รับประสบการณ์ที่ราบรื่นขึ้นโดยการ Update หน้าปัจจุบันแบบ Dynamic แทนที่จะ Load หน้าใหม่ทุกครั้งที่ผู้ใช้มีการโต้ตอบกับ Application

ใน SPA, HTML และ Resource ที่จำเป็นจะถูก Load เพียงครั้งเดียว หลังจากนั้นการโต้ตอบกับ Application จะถูกดำเนินการโดย JavaScript เพื่อจัดการเนื้อหาในหน้าที่ Load ไว้ทีแรก

ขณะที่วิธีการดั้งเดิมแบบ MPA จะต้องมีการ Load หน้า HTML ใหม่ทุกครั้งเมื่อผู้ใช้ Click ที่ Link หรือ Submit Form คำขอข้อมูลในแต่ละครั้งจะต้องมีการ Refresh หน้าทั้งหมด

SPA จะ Load HTML เริ่มต้น แล้วส่งคำขอข้อมูลในรูป JSON เท่าที่จำเป็นผ่าน API ดังนั้น SPA จึงเป็น API-centric ที่มีการแบ่งความรับผิดชอบในการประมวลผลระหว่าง Client และ Server ซึ่งฝั่ง Client มักพัฒนาด้วย JavaScript Frameword เช่น React ขณะที่ MPA มีความเป็น Server-centric ที่เน้นการประมวลผลที่ซับซ้อนในฝั่ง Server ทำให้มีความท้ายมากขึ้นในการ Scale และบำรุงรักษาเมื่อ Application มีความซับซ้อนมากขึ้น

API First

ในช่วงไม่กี่ปีที่ผ่านมา API First ได้กลายเป็นแนวทางในการพัฒนา Software ยอดนิยม โดยการให้ความสำคัญกับการออกแบบ API ก่อนการออกแบบระบบ และการเขียน Code ซึ่งทีม Frontend Developer, Backend Developer และ QA ฯลฯ จะทำงานร่วมกันเพื่อออกแบบ API ตาม System Requirement

Code First จะเริ่มต้นด้วยการพัฒนา Code การทดสอบและการทำ API Document จะมาทีหลัง โดยจะเป็นการมุ่งเน้นการพัฒนาและการใช้งาน Code เป็นหลัก

API First จะเริ่มต้นด้วยการออกแบบ API โดยมีการ Review และ Mock API ก่อนการพัฒนาจริง การพัฒนา Code จะมาทีหลังหลังจากที่ API ได้รับการออกแบบแล้ว ซึ่งจะเน้นการทำ API Document และการทดสอบ API อย่างละเอียด

การพัฒนา Application ตามแนวคิด API First จะช่วยให้เราคำนึงถึงปัจจัยสำคัญ ๆ ในการออกแบบ API ที่สามารถ Scale ได้ดีตั้งแต่เริ่มต้น จึงเป็นแนวคิดที่เสริมกันอย่างดีกับการพัฒนาแบบ Single Page Application (SPA)

REST API Design for E-commerce Platform

เมื่อเราพัฒนา Software ฝั่ง Fontend เรามักจะคิดถึงคนธรรมดาที่ไม่มีความเชี่ยวชาญด้านเทคโนโลยีมากนัก โดยการสร้าง Interface ที่เป็นมิตร รับข้อมูลในสิ่งที่ผู้ใช้ต้องการ แต่การพัฒนา API (Application Programming Interfaces) นั้นแตกต่างออกไป เรากําลังสร้าง Interface สําหรับ Programmer ที่มีทักษะ ซึ่งสามารถสังเกตุถึงปัญหาทางเทคนิคเล็ก ๆ น้อย

API มีมานานแล้วในรูปแบบใดรูปแบบหนึ่ง

ในการพัฒนาแบบ API First นั้น REST (Representational State Transfer) เป็นมาตรฐานการสื่อสารที่พบบ่อยที่สุดระหว่างสอง Application หรือ Software ที่ทําหน้าที่แตกต่างกันผ่านเครือข่าย เพื่อให้เชี่ยวชาญในการสร้าง REST API เราต้องปฏิบัติตามแนวทางบางประการเพื่อให้แน่ใจว่าเราได้ออกแบบ API ที่มีประสิทธิภาพและใช้งานง่าย

หลักการสำคัญของ REST คือ

ใช้ HTTP (Hypertext Transfer Protocol) ในการสื่อสาร
HTTP เป็น Stateless Protocol ที่ Request แต่ละรายการเป็นอิสระต่อกัน โดย Server จะไม่เก็บข้อมูลใด ๆ เกี่ยวกับ Request ก่อนหน้า

HTTP Request แต่ละรายการประกอบด้วย Method เช่น GET, POST, PUT, DELETE, Header และ Body ที่เป็น Option สำหรับบรรจุข้อมูล

HTTP Response จะประกอบไปด้วย Status Code, Header, และ Body ที่เป็น Option สำหรับบรรจุข้อมูลเช่นกัน

ใช้ URL (Uniform Resource Locator) ในการเข้าถึง Resource ต่าง ๆ
URL คือที่อยู่ที่ใช้เพื่อเข้าถึง Resource บนเว็บ

ใช้สถาปัตยกรรมการสื่อสารแบบ Client-Server
Client จะส่งคําขอไปยัง Server จากนั้น Server จะประมวลผล Request และส่งการตอบกลับมายัง Client โดย Client จะดูแลการนําเสนอสำหรับ User Interface ในขณะที่ Server จัดการ Business Logic การประมวลผลข้อมูล และการจัดเก็บข้อมูล

แนวคิดหลักของ REST คือ Resource
Resource คือ Information ที่สามารถตั้งชื่อและเข้าถึงได้ผ่าน URL ซึ่งแต่ละ Resource จะถูกกำหนดโดย URL ที่ไม่ซ้ำกัน (Resource Endpoint) โดยอาจจะเป็น User,  Product หรือ Order ฯลฯ และถูกส่งกลับในรูปแบบ JSON

ใน REST API ส่วนของ Client จะโต้ตอบกับ Resource โดยการ Create, Update หรือ Delete โดยการส่ง HTTP Request ที่เหมาะสมไปยัง URL ที่เกี่ยวข้อง

Resource Endpoint จะต้องเป็นคำนาม ไม่ใช่คำกิริยา ยกตัวอย่างเช่น /users แทนที่จะใช้ /getUsers หรือ /createUser

ห้ใช้คํานามพหูพจน์ เช่น /products หรือ /users แทนที่จะเป็น /product หรือ /user (แม้ว่าการใช้คำนามพหูพจน์จะเป็นแนวปฏิบัติที่พบบ่อย แต่ไม่ได้เป็นกฎตายตัว)

ใช้ Path แบบลำดับชั้น ที่แสดงความสัมพันธ์ของ Resource ยกตัวอย่างเช่น /orders/123/items

ใช้ - เพื่อแยกคํา หากชื่อ Resource ประกอบด้วยหลายคํา ตัวอย่างเช่น /product-categories หรือ /user-profiles

ใช้ตัวพิมพ์เล็กทั้งหมด เพื่อรักษาความสอดคล้องและหลีกเลี่ยงความสับสน

ตัวอย่าง URL ของ Resource ต่าง ๆ

/users
แทนกลุ่มข้อมูลผู้ใช้

/users/123
แทนข้อมูลของผู้ใช้ที่มี ID 123

/products
แทนกลุ่มข้อมูลสินค้า

/orders/123/items
แทนกลุ่มข้อมูลสินค้าที่อยู่ในคำสั่งซื้อหมายเลข 123

/categories
แทนกลุ่มข้อมูลประเภทสินค้า

/categories/xyz/products
แทนกลุ่มข้อมูลสินค้าตามประเภท xyz ที่ระบุ

การใช้ HTTP Method ที่เหมาะสมใน REST API

ในการพัฒนา Application เรามักใช้หลักการแยกส่วน (Separation of Concern) ซึ่งแนะนำให้นักพัฒนาแยกชั้นของ Application เป็น Web Service Layer ซึ่งรับผิดชอบในการจัดการคําขอเว็บ (Route และ Handler Function) และ Service Layer  ซึ่งรับผิดชอบ Core Business Logic ต่าง ๆ เช่น การโต้ตอบกับ Database การจัดการ Message Queue และการสื่อสารกับ Service อื่น ๆ

ที่ Web Service Layer เราจะใช้ HTTP Verb (Method) หลัก ๆ ได้แก่ GET, PUT, POST, DELETE และ PATCH เพื่อกําหนดการดําเนินการที่เราสามารถทำได้กับ Resource ต่าง ๆ ขณะที่ใน Service Layer เราใช้การดําเนินการ CRUD (Create, Read, Update, Delete) ในการจัดการกับ Business Logic จริง ๆ

โดย GET จะถูกใช้เพื่อดึง Resource ต่าง ๆ และ PUT ถูกใช้สำหรับการ Update แบบแทนที่ Resource ทั้งหมด ขณะที่ PATCH จะถูกใช้สำหรับการ Update Resource บางส่วน

ส่วน POST จะถูกใช้สำหรับสร้าง Resource ใหม่ และ DELETE สำหรับลบ Resource ที่มีอยู่

GET
- สำหรับดึง Resource ต่าง ๆ
- ตัวอย่างเช่น GET /users/123
- ไม่มี Request Body
- มี Response Body เช่น

{
    "id": 123,
    "name": "สมชาย ใจดี",
    "email": "[email protected]",
    "phone": "081-234-5678",
    "address": {
        "street": "123 ถนนหลัก",
        "city": "กรุงเทพมหานคร",
        "zip": "10100"
    }
}

- สามารถ Cache ได้ (แต่ไม่ได้หมายความว่าทุก GET Request ควรถูก cache เสมอไป)
- ทำซ้ำได้โดยให้ผลลัพธ์เหมือนเดิม (Idempotent)
- Service Layer Method เช่น get() และ list()

PUT
- สำหรับการ Update แบบแทนที่ Resource ทั้งหมด
- ตัวอย่างเช่น PUT /users/123
- มี Request Body เช่น

{
    "id": 123,
    "name": "ณัฐโชติ พรหมฤทธิ์",
    "email": "[email protected]",
    "phone": "081-234-5678",
    "address": {
        "street": "123 ถนนหลัก",
        "city": "กรุงเทพมหานคร",
        "zip": "10100"
    }
}

- อาจมี Response Body
- ไม่สามารถ Cache ได้
- ทำซ้ำได้โดยให้ผลลัพธ์เหมือนเดิม (Idempotent)
- Service Layer Method เช่น update()

POST
- สำหรับสร้าง Resource ใหม่
- ตัวอย่างเช่น POST /users
- มี Request Body เช่น

{
    "name": "สัจจาภรณ์ ไวจรรยา",
    "email": "[email protected]",
    "phone": "095-277-5111",
    "address": {
        "street": "123 ถนนหลัก",
        "city": "กรุงเทพมหานคร",
        "state": "กรุงเทพ",
        "zip": "10100"
    }
}

- มี Response Body เช่น

{
    "id": 987,
    "name": "สมชาย ใจดี",
    "email": "[email protected]",
    "phone": "095-277-5111",
    "address": {
        "street": "123 ถนนหลัก",
        "city": "กรุงเทพมหานคร",
        "zip": "10100"
    },
    "created_at": "2024-08-28T16:00:00Z",
    "updated_at": "2024-08-28T16:00:00Z"
}

- ไม่สามารถ Cache ได้
- ทำซ้ำแล้วอาจให้ผลลัพธ์ต่างออกไป เนื่องจาก POST ถูกออกแบบมาเพื่อใช้ในการสร้าง Resource ใหม่ การทำคำขอซ้ำอาจสร้าง Resource ใหม่ที่มีรหัสไม่เหมือนเดิม
- Service Layer Method เช่น create()

DELETE
- สำหรับการลบ Resource
- ตัวอย่างเช่น DELETE /user/123
- อาจมี Request Body
- อาจมี Response Body
- ไม่สามารถ Cache ได้
- ทำซ้ำได้โดยให้ผลลัพธ์เหมือนเดิม (Idempotent)
- Service Layer Method เช่น delete()

PATCH
- สำหรับการ Update Resource บางส่วน
- ตัวอย่างเช่น PATCH /user/123
- มี Request Body

{
    "email": "[email protected]"
}

- มี Response Body

{
    "id": 123,
    "name": "ณัฐโชติ พรหมฤทธิ์",
    "email": "[email protected]",
    "created_at": "2023-01-01T12:00:00Z",
    "updated_at": "2024-08-28T15:00:00Z"
}

- ไม่สามารถ Cache ได้
- ทำซ้ำแล้วอาจให้ผลลัพธ์ต่างออกไป เพราะการ Update อาจขึ้นอยู่กับค่าปัจจุบันใน Resource นั้น เช่น การเพิ่มค่า (Increment) ใน Field จำนวน หรือการ Update Field ที่ต้องขึ้นอยู่กับเงื่อนไขเฉพาะ ถ้า Resource มีการเปลี่ยนแปลงในระหว่างการทำคำขอครั้งแรกและครั้งที่สองผลลัพธ์ที่ได้อาจแตกต่างกัน
- Service Layer Method เช่น update()

API Design Best Practices

1. API Versioning

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

หากเราเปลี่ยนชื่อ Field เดิมโดยไม่มีระบบ Version แล้ว Application ที่เรียกใช้ API อาจหยุดทํางานหรือมีการแสดงข้อผิดพลาด เพื่อรักษาความเข้ากันได้กับ Application ที่ใช้ API Version เก่า เราต้องขอให้นักพัฒนาเลือกเวลาที่จะ Update Application ให้สามารถทํางานกับ API Version ใหม่

เราอาจเพิ่มหมายเลข Version เป็นส่วนหนึ่งของ URL ได้ ซึ่งทำให้ง่ายต่อการตรวจสอบและแก้ไขข้อผิดพลาด เนื่องจากเราจะเห็น Version ได้ชัดเจนใน URL

ตำแหน่งของ Version อาจอยู่ก่อนชื่อ Resource หรือหลังชื่อ Resource เช่น

อยู่ก่อนชื่อ Resource เมื่อต้องการใช้ Version กับ API หลายตัวพร้อมกัน

https://myapi.com/api/v1/users

อยู่หลังชื่อ Resource เมื่อต้องการกำหนด Version เฉพาะสำหรับบาง Resource

https://myapi.com/api/users/v1

ระบบการกำหนด Version ของ API อีกแบบหนึ่งคือ Semantic Versioning Specification (SemVer) ประกอบด้วย 3 ส่วนหลัก ตัวอย่างเช่น Version 3.0.0

Major คือ Version หลัก
Minor คือ Version รอง
Patch คือ Version แก้ไขย่อย

การ Update Major Version ใช้เมื่อมีการเปลี่ยนแปลงที่ทำให้เกิดความไม่เข้ากันกับเวอร์ชันเก่า (Breaking Change) เช่นจาก 3.0.0 เป็น 4.0.0

การ Update Minor Version ใช้เมื่อเราเพิ่ม Feature ใหม่ที่ยังคงมีความเข้ากันได้กับ Version เก่า (Backward Compatible Feature) เช่นจาก 3.0.0 เป็น 3.1.0

การ Update Patch ใช้สำหรับการแก้ไขข้อบกพร่องที่ยังคงความเข้ากันได้กับ Version เก่า (Backward Compatible Defect Fix) เช่นจาก 3.1.0 เป็น 3.1.1

หมายเหตุ SemVer ถูกออกแบบมาสำหรับการทำ Version ของ Software Library และ OS มากกว่า API โดยทั่วไป การใช้ Major Version เช่น v1 และ v2 ก็อาจเพียงพอและง่ายกว่าในการใช้งาน

2. Pagination

ในการออกแบบ API การแบ่งหน้า (Pagination) เป็นสิ่งสำคัญสำหรับจัดการกับ Resource ขนาดใหญ่

ตัวอย่างเทคนิคการแบ่งหน้า 3 แบบ ได้แก่ Offset-based, Cursor-based และ Page-based

Offset-based
ใช้ Parameter "offset" และ "limit" เพื่อกำหนดจุดเริ่มต้นและจำนวนรายการที่ต้องการ เช่น GET /api/v1/products?offset=0&limit=2

offset คือ ตำแหน่งเริ่มต้นของ Resource ที่ได้จากการ Query ที่ต้องการ (offset=0 หมายถึงเริ่มจากรายการแรก)
limit คือ จำนวนรายการที่ต้องการ

ตัวอย่างข้อมูลสินค้า
id 1001, name "โน้ตบุ๊ก A", price 30000
id 1002, name "สมาร์ทโฟน B", price 15000
id 1003, name "แท็บเล็ต C", price 20000
id 1004, name "หูฟัง D", price 5000
id 1005, name "กล้อง E", ราคา: 25000

เรียกครั้งแรก GET /api/v1/products?offset=0&limit=2

{
  "products": [
    {"id": 1001, "name": "โน้ตบุ๊ก A", "price": 30000},
    {"id": 1002, "name": "สมาร์ทโฟน B", "price": 15000}
  ],
  "total": 5,
  "next_offset": 2
}

เรียกครั้งที่สอง GET /api/v1/products?offset=2&limit=2

{
  "products": [
    {"id": 1003, "name": "แท็บเล็ต C", "price": 20000},
    {"id": 1004, "name": "หูฟัง D", "price": 5000}
  ],
  "total": 5,
  "next_offset": 4
}

เรียกครั้งที่สาม GET /api/v1/products?offset=4&limit=2

{
  "products": [
    {"id": 1005, "name": "กล้อง E", "price": 25000}
  ],
  "total": 5,
  "next_offset": null
}

ข้อดี ง่ายต่อการเข้าใจและใช้งาน
ข้อเสีย อาจไม่มีประสิทธิภาพสำหรับ offset ขนาดใหญ่

Cursor-based
ใช้ตัวชี้ (cursor) เป็นตัวระบุตำแหน่งใน Resource มักเป็นค่าที่ไม่ซ้ำกัน เช่น ID ของรายการสุดท้ายในหน้าปัจจุบัน เช่น GET /api/v1/products?limit=2&cursor=1004

limit คือ จำนวนรายการต่อหน้า
cursor คือ ตำแหน่งใน Resource มักเป็นค่าที่ไม่ซ้ำกัน

เรียกครั้งแรก GET /api/v1/products?limit=2

{
  "products": [
    {"id": 1001, "name": "โน้ตบุ๊ก A", "price": 30000},
    {"id": 1002, "name": "สมาร์ทโฟน B", "price": 15000}
  ],
  "next_cursor": "1002"
}

เรียกครั้งที่สอง: GET /api/v1/products?limit=2&cursor=1002

{
  "products": [
    {"id": 1003, "name": "แท็บเล็ต C", "price": 20000},
    {"id": 1004, "name": "หูฟัง D", "price": 5000}
  ],
  "next_cursor": "1004"
}

เรียกครั้งที่สาม: GET /api/v1/products?limit=2&cursor=1004

{
  "products": [
    {"id": 1005, "name": "กล้อง E", "price": 25000}
  ],
  "next_cursor": null
}

ข้อดี มีประสิทธิภาพสำหรับชุดข้อมูลขนาดใหญ่
ข้อเสีย ซับซ้อนกว่าในการใช้งานและพัฒนา

Page-based
ใช้การระบุหมายเลขหน้าและขนาดของแต่ละหน้า เช่น GET /api/v1/products?page=1&pageSize=2

page คือ หมายเลขหน้าที่ต้องการ (เริ่มจาก 1)
pageSize คือ จำนวนรายการต่อหน้า

เรียกหน้าแรก: GET /api/v1/products?page=1&pageSize=2

{
  "products": [
    {"id": 1001, "name": "โน้ตบุ๊ก A", "price": 30000},
    {"id": 1002, "name": "สมาร์ทโฟน B", "price": 15000}
  ],
  "currentPage": 1,
  "totalPages": 3,
  "totalItems": 5
}

เรียกหน้าที่สอง: GET /api/v1/products?page=2&pageSize=2

{
  "products": [
    {"id": 1003, "name": "แท็บเล็ต C", "price": 20000},
    {"id": 1004, "name": "หูฟัง D", "price": 5000}
  ],
  "currentPage": 2,
  "totalPages": 3,
  "totalItems": 5
}

เรียกหน้าสุดท้าย: GET /api/v1/products?page=3&pageSize=2

{
  "products": [
    {"id": 1005, "name": "กล้อง E", "price": 25000}
  ],
  "currentPage": 3,
  "totalPages": 3,
  "totalItems": 5
}

ยกตัวอย่างการคำนวน totalPages
totalItems = 5 (จำนวนสินค้าทั้งหมด)
pageSize = 2 (จำนวนรายการต่อหน้า)

totalPages = Math.ceil(5/2) = Math.ceil(2.5) = 3

ข้อดี ง่ายต่อการใช้งานและพัฒนา
ข้อเสีย อาจมีปัญหาประสิทธิภาพสำหรับหมายเลขหน้าที่สูง

3. Filtering

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

GET /api/v1/products?category=electronics&price_min=1000&price_max=5000&location=bangkok

4. Sorting

เช่นเดียวกับการทำ Filtering เราควรใช้การ Sorting เพื่อให้ผู้ใช้สามารถจัดเรียงข้อมูลตามเงื่อนไขที่กำหนด เช่น

GET /api/v1/products?sort_by=price&order=desc

ผลลัพธ์จากการใช้ Sorting

{
  "products": [
    {"id": 1001, "name": "โน้ตบุ๊ก A", "price": 30000},
    {"id": 1005, "name": "กล้อง E", "price": 25000},
    {"id": 1003, "name": "แท็บเล็ต C", "price": 20000},
    {"id": 1002, "name": "สมาร์ทโฟน B", "price": 15000},
    {"id": 1004, "name": "หูฟัง D", "price": 5000}
  ]
}

5. Error Handling

การจัดการข้อผิดพลาด (Error Handling) เป็นส่วนสำคัญในการสร้าง API ที่มีความปลอดภัยและให้ประสบการณ์ผู้ใช้ที่ดี การจัดการข้อผิดพลาดที่เหมาะสมจะช่วยให้นักพัฒนาเข้าใจปัญหาได้ดีขึ้นโดยไม่เปิดเผยข้อมูลที่อาจเป็นอันตราย

เราควรส่งคืนข้อความข้อผิดพลาดด้วย HTTP Status Code และรายละเอียดเพิ่มเติม เช่น

HTTP Status Code
400 : Bad Reques ใช้เมื่อข้อมูลที่ส่งมามีรูปแบบไม่ถูกต้องหรือมีข้อมูลที่ไม่ครบถ้วน
401 : Unauthorized ใช้เมื่อผู้ใช้ยังไม่ได้มีการ Authen ที่ถูกต้อง
403 : Forbidden ใช้เมื่อผู้ใช้ไม่มีสิทธิ์ในการเข้าถึง Resource ที่ร้องขอ
404 : Not Found เมื่อไม่พบ Resource ที่ร้องขอ
500 : Internal Server Error ใช้เมื่อเกิดข้อผิดพลาดภายใน Server

กลุ่มของ HTTP Status Code ที่มักใช้กันบ่อย ๆ

2xx - Acknowledge and Success ใช้เมื่อคำขอประสบความสำเร็จ

3xx - Redirection ใช้เมื่อต้องมีการดำเนินการเพิ่มเติมเพื่อให้คำขอสมบูรณ์

4xx - Client Error ใช้เมื่อคำขอมีข้อผิดพลาดหรือไม่สามารถดำเนินการได้เนื่องจากปัญหาจากฝั่ง Client

5xx - Server Error ใช้เมื่อ Server ไม่สามารถดำเนินการตามคำขอที่มาอย่างถูกต้องได้

รายละเอียดเพิ่มเติมที่ส่งไปพร้อมกับ HTTP Status Code
เราควรให้ข้อมูลที่ชัดเจนเพียงพอให้นักพัฒนาสามารถแก้ไขปัญหาได้ แต่ไม่ควรเปิดเผยข้อมูลภายใน ที่อาจเสี่ยงต่อความมั่นคงปลอดภัย เช่น แทนที่จะบอกว่า "Internal Server Error" ควรใช้ข้อความเช่น "Access denied. Check credentials and permissions." ซึ่งให้ข้อมูลเพียงพอแก่นักพัฒนาโดยไม่เปิดเผยข้อมูลภายในระบบ

รวมทั้งไม่ควรแสดงรายละเอียดที่อาจทำให้ระบบถูกโจมตีได้ เช่นไม่ควรบอกว่า "SQL query failed due to malformed input containing a DROP TABLE command" แต่ควรใช้ข้อความทั่วไปเช่น "Invalid input provided. Please review and try again."

6. API Spec

API Spec คือ เอกสารที่อธิบายรายละเอียดของ API อย่างเป็นทางการ API Spec ช่วยให้นักพัฒนาเข้าใจวิธีการใช้งาน API ได้อย่างถูกต้อง ทำให้การพัฒนาและการทดสอบมีประสิทธิภาพมากขึ้น โดยเฉพาะ Project ที่มีทีมพัฒนาหลายทีม หรือ Project ที่ต้องมีการจ้างทีม Outsource

Post-test (25 ข้อ) ขอให้สนุกกับการทำ Post-test นะครับ!