ข้ามไปยังเนื้อหา
Tayakorn
System Design Handbook

System Design Handbook

การออกแบบ API — สัญญาที่ดีระหว่างระบบ

Resource Modeling · HTTP Methods · Pagination · Versioning · ▶ ในคลิป 0:29:50 – 0:46:06

API คือ สัญญา (contract) ระหว่างระบบของเรากับโลกภายนอก — และสัญญาที่ปล่อยออกไปแล้ว แก้ยากที่สุด เพราะมี client นับพันยึดตามอยู่ API ที่ดีจึงต้อง "เดาได้" (predictable): นักพัฒนาที่เห็น endpoint หนึ่งตัว ควรเดา endpoint ที่เหลือได้ถูกเกือบหมด

4.1 คิดเป็น Resource ไม่ใช่ Action

หัวใจของ REST-style API: URL แทน "สิ่งของ" (คำนาม) ส่วน "การกระทำ" (กริยา) ให้ HTTP method เป็นคนบอก — อย่าฝังกริยาลงใน URL

GET https://api.shop.com/v1/users/42/orders?status=paid&limit=20MethodการกระทำVersionกันสัญญาพังCollectionคำนามพหูพจน์Resource IDตัวระบุชิ้นเดียวNested resourceของที่อยู่ใต้ user 42Query paramsกรอง / แบ่งหน้า / เรียงลำดับ✗ ที่ไม่ควรทำ: /getUserOrders, /createOrder, /order_delete?id=42 — กริยาอยู่ผิดที่ และรูปแบบเดาไม่ได้
FIG 4.1 กายวิภาคของ URL ที่ดี: version → collection (พหูพจน์) → id → nested resource → query สำหรับการกรอง

4.2 HTTP Methods และคุณสมบัติ Idempotency

Idempotent = เรียกซ้ำกี่ครั้งผลลัพธ์สุดท้ายเหมือนเรียกครั้งเดียว — สำคัญมากเพราะ network ล้มเหลวได้เสมอ และ client ต้องรู้ว่า method ไหน "retry ได้อย่างปลอดภัย"

MethodความหมายIdempotent?ตัวอย่าง
GETอ่านข้อมูล ไม่เปลี่ยนแปลงอะไร✓ (และ safe)GET /orders/90
POSTสร้างใหม่ / สั่งประมวลผล✗ — เรียกซ้ำ = สร้างซ้ำPOST /orders
PUTแทนที่ทั้งก้อนด้วยข้อมูลใหม่PUT /users/42
PATCHแก้บางฟิลด์ขึ้นกับการออกแบบPATCH /users/42
DELETEลบ✓ (ลบซ้ำ = ก็ยังถูกลบอยู่ดี)DELETE /orders/90

🧭 Idempotency Key — เทคนิคระดับ production — POST ไม่ idempotent โดยธรรมชาติ แต่ระบบจ่ายเงินยอม "ตัดบัตรซ้ำ" ไม่ได้ — ทางแก้คือให้ client แนบ Idempotency-Key: <uuid> มากับ request ฝั่งเซิร์ฟเวอร์จำ key ที่เคยประมวลผลแล้ว ถ้าเจอซ้ำ (เช่น client retry เพราะ timeout) ให้ตอบผลลัพธ์เดิมแทนการทำงานซ้ำ — Stripe ใช้แพตเทิร์นนี้กับทุก API ที่สร้างธุรกรรม

4.3 Status Codes — ตอบให้ตรงความหมาย

  • 2xx สำเร็จ200 OK, 201 Created (สร้างสำเร็จ + ส่ง location กลับ), 204 No Content (ลบสำเร็จ ไม่มี body)
  • 4xx client ผิด400 Bad Request (payload ผิดรูป), 401 Unauthorized (ยังไม่พิสูจน์ตัวตน), 403 Forbidden (ตัวตนชัดแต่ไม่มีสิทธิ์), 404 Not Found, 429 Too Many Requests (โดน rate limit — บทที่ 8)
  • 5xx server ผิด500 Internal Server Error, 503 Service Unavailable (ล่มชั่วคราว/ปิดปรับปรุง)

ข้อผิดพลาดควรมี body ที่ machine-readable และบอกวิธีแก้ ไม่ใช่แค่ข้อความลอย ๆ:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "email ไม่ใช่รูปแบบที่ถูกต้อง",
    "field": "email",
    "request_id": "req_8f3ka"   // ไว้ตามรอยใน log
  }
}

4.4 Pagination — อย่าส่งข้อมูลล้านแถวในครั้งเดียว

ทุก endpoint ที่คืน list ต้องแบ่งหน้า มีสองแนวทางหลักที่ trade-off ต่างกันชัดเจน:

Offset-basedGET /orders?offset=20&limit=10แถว 1–10 (ข้าม)แถว 11–20 (ข้าม)แถว 21–30 ← ส่งหน้านี้+ เข้าใจง่าย กระโดดไปหน้าไหนก็ได้− ช้าลงเรื่อย ๆ เมื่อ offset ลึก (DB ต้องนับข้ามจริง)− แถวถูกแทรก/ลบระหว่างเปิดหน้า → ข้อมูลซ้ำ/หายCursor-basedGET /orders?after=ord_120&limit=10cursor: ord_120 ← จุดอ้างอิง10 แถวถัดจาก ord_120response แนบ next_cursor มาให้+ เร็วคงที่ทุกความลึก (กระโดดด้วย index)+ ทนต่อข้อมูลที่เปลี่ยนระหว่างเลื่อน — เหมาะ infinite scroll− กระโดดไป “หน้า 50” ตรง ๆ ไม่ได้กติกาเลือก: หน้าจอแบบ “เลขหน้า” (admin table) → offset · feed/timeline ที่เลื่อนไม่รู้จบ → cursor
FIG 4.2 Offset นับข้ามจากหัวตาราง ส่วน cursor กระโดดจากจุดอ้างอิงล่าสุด — Twitter/Facebook feed ล้วนใช้ cursor

4.5 Versioning — เผื่อทางให้อนาคต

สักวันเราต้องเปลี่ยนสัญญาแบบที่ client เก่าพัง (breaking change) เช่น เปลี่ยนชื่อฟิลด์ หรือโครงสร้าง response — versioning คือทางหนีไฟ มี 3 สไตล์:

วิธีหน้าตาข้อสังเกต
URI path/v1/orders/v2/ordersชัดเจน เห็นด้วยตา cache ง่าย — นิยมที่สุด
HeaderAccept: application/vnd.api+json;version=2URL สะอาด แต่ debug ยากกว่า มองไม่เห็นใน browser
Query param/orders?version=2ง่ายสุดแต่ปนกับ param อื่น ไม่ค่อยแนะนำ

กฎที่สำคัญกว่าการเลือกวิธี: เปลี่ยนแบบไม่พังได้ ไม่ต้องออกเวอร์ชันใหม่ — การ "เพิ่มฟิลด์ใหม่" ใน response ไม่ใช่ breaking change (client ที่ดีต้องเมินฟิลด์ที่ไม่รู้จัก) ออก v2 เฉพาะเมื่อจำเป็นจริง เพราะทุกเวอร์ชันที่เปิดอยู่คือภาระดูแล ×2

สรุปบทที่ 4 — URL คือคำนาม method คือกริยา · จำตาราง idempotency ให้ขึ้นใจ — มันคือเหตุผลว่าอะไร retry ได้ · ใช้ status code ให้ตรงความหมาย + error body ที่บอกวิธีแก้ · list ทุกตัวต้องแบ่งหน้า: เลขหน้า→offset, feed→cursor · เพิ่มฟิลด์ได้เสมอ แต่เปลี่ยน/ลบต้องออกเวอร์ชัน

อ่านแบบเต็มเล่ม