Skip to content

บทที่ 1 — REST Design ลึก

← บทที่ 0 | สารบัญ | บทที่ 2 →

หลังจบบท คุณจะ:

  • ออกแบบ URL ตามมาตรฐาน
  • เลือก HTTP method + status code ที่ถูก
  • จัดการ nested resource, sub-resource
  • ทำ action ที่ไม่ใช่ CRUD
  • เข้าใจ idempotent + safety + HATEOAS

1. Resource Modeling

REST เริ่มจากคำถาม: "app เรามีอะไรเป็น 'resource'?"

E-commerce:
- User
- Product
- Order
- OrderItem
- Cart
- Review

Blog:
- Post
- Comment
- Author
- Tag
- Category

แต่ละ resource → endpoint


2. URL Convention

กฎทอง

✅ /users                  noun + plural
❌ /user                   singular
❌ /getUsers               verb
❌ /User                   PascalCase
❌ /Users                  PascalCase
✅ /users                  lowercase
❌ /Users-List             multiple words mixed
✅ /user-profiles          kebab-case (multi-word)
❌ /user_profiles          snake_case ใน URL
✅ /api/v1/users           prefix + version

Query parameter ใช้ camelCase หรือ snake_case ก็ได้ — แต่ต้อง consistent ทั้ง API (เช่นถ้า body ใช้ camelCase → query ก็ camelCase)

ลำดับ Hierarchy

/users                          # all users
/users/{id}                     # specific user
/users/{id}/orders              # orders ของ user นี้
/users/{id}/orders/{orderId}    # specific order ของ user นี้
/users/{id}/orders/{orderId}/items   # items ใน order

URL = "เส้นทาง" ที่อ่านแล้วเข้าใจ

Nesting — อย่าลึกเกิน 2 ระดับ

✅ /users/{id}/orders
❌ /users/{id}/orders/{oid}/items/{iid}/refunds/{rid}

หากลึกเกิน → ทำ top-level:

GET /refunds?orderItemId={iid}
GET /orders/{oid}/items

3. HTTP Method — Map กับ CRUD

List all:     GET    /users
Get one:      GET    /users/{id}
Create:       POST   /users
Update full:  PUT    /users/{id}
Update part:  PATCH  /users/{id}
Delete:       DELETE /users/{id}
Verbภาษาไทยคำอ่านIdempotent?Safe?
GETดึงข้อมูลเก็ท
POSTสร้างใหม่โพสต์❌ (สร้างซ้ำได้)
PUTแทนที่ทั้งหมดพุท
PATCHแก้บางส่วนแพทช์⚠️ ไม่จำเป็น (แล้วแต่ implement)
DELETEลบดีลีท✅ (ลบซ้ำ = ลบไปแล้ว)

Idempotent (อ่าน ไอ-เด็ม-โพ-เทน-ท์) = เรียกซ้ำกี่ครั้งผลก็เหมือนเดิม. Safe = ไม่เปลี่ยน state บน server. PUT idempotent ตามนิยาม แต่ PATCH ไม่จำเป็น ต้อง idempotent (ถ้า PATCH body เป็น "เพิ่ม 1" เรียก 3 ครั้ง = เพิ่ม 3) — RFC 9110

PUT vs PATCH — ต่างยังไง?

PUT = แทนที่ทั้ง resource

http
PUT /users/1
{ "name": "Anna", "email": "anna@x.com", "age": 25 }

→ field ที่ไม่ส่ง = ถูกเซ็ตเป็น default/null

⚠️ PUT ตามมาตรฐานต้อง replace ทุก field — ถ้า body ไม่ส่ง field มาด้วย ต้อง reset เป็น default/null. ใน Spring @RequestBody User ส่วนมาก ไม่ null ออก field ที่หายไป (มัน deserialize ทับเฉพาะที่ส่ง) ทำให้ PUT พฤติกรรมกลายเป็น PATCH โดยไม่ตั้งใจ — ถ้าไม่แน่ใจ ใช้ PATCH ปลอดภัยกว่า

PATCH = แก้บางส่วน

http
PATCH /users/1
{ "age": 26 }

→ field อื่นไม่กระทบ

ในงานจริง: ใช้ PATCH ส่วนใหญ่ — PUT ใช้กับ replace ทั้ง entity จริง ๆ

⚠️ PATCH ต้องประกาศ Content-Type ที่ยอมรับapplication/merge-patch+json (RFC 7396) หรือ application/json-patch+json (RFC 6902) สอง format มี semantic ต่างกันสิ้นเชิง. ถ้าใช้ application/json เฉย ๆ จะกำกวม client ไม่รู้ว่า server ตีความ body แบบไหน

PATCH formats

RFC = เอกสารมาตรฐานสากลของอินเทอร์เน็ต เลขข้างหลังคือหมายเลขเอกสาร (ไม่ต้องจำ — รู้แค่ว่ามันคือ "มาตรฐานที่มีคนกำหนดไว้แล้ว")

  1. JSON Merge Patch (RFC 7396) — simple

    json
    { "age": 26, "phone": null }  // null = remove

    💡 Mnemonic จำง่ายMerge = "รวม" body กับ resource เดิม (ง่าย) / Patch = ระบุ operation ทีละขั้น (ละเอียด)

  2. JSON Patch (RFC 6902) — operation-based

    🚀 ขั้นสูง — กางดู (มือใหม่ข้ามได้)
    json
    [
      { "op": "replace", "path": "/age", "value": 26 },
      { "op": "remove", "path": "/phone" }
    ]

    ใช้เมื่อต้องการ atomic operation หลายขั้น (add/remove/replace/move/copy/test) ส่วนใหญ่ใช้ใน config-as-code หรือ collaborative editing

มือใหม่ใช้ Merge Patch — ง่ายกว่า


4. Status Code — เลือกให้ถูก

HTTP status code คือภาษาที่ API บอก client ว่าเกิดอะไรขึ้น — เลือกให้ตรงความหมาย (201 สร้างสำเร็จ, 204 ลบแล้วไม่มี body, 404 ไม่เจอ, 409 ขัดแย้ง) ช่วยให้ client จัดการผลลัพธ์ได้ถูกโดยไม่ต้อง parse body ส่วนนี้รวมเคสที่ใช้บ่อยพร้อมตัวอย่าง:

Create

http
POST /users → 201 Created
Location: https://api.example.com/v1/users/123
Body: { "id": 123, "name": "..." }

Location header = บอก client ว่า resource ที่เพิ่งสร้างอยู่ URL ไหน — client เอาไป fetch ต่อ/redirect ได้. RFC 9110 แนะนำใช้ absolute URL (https://api.example.com/v1/users/123) ถึงแม้ relative path (/users/123) client ส่วนใหญ่ยอมรับ

Update

http
PATCH /users/123 → 200 OK + updated body
หรือ
PATCH /users/123 → 204 No Content (ถ้าไม่ต้องการ body)

Delete

http
DELETE /users/123 → 204 No Content
หรือ
DELETE /users/123 → 200 OK + body (ระบุที่ลบ)

Not Found

http
GET /users/9999 → 404 Not Found

Validation Fail

http
POST /users { "email": "invalid" } → 422 Unprocessable Entity
Body: { "error": { "code": "VALIDATION", "details": [...] } }

⚠️ 400 vs 422 ไม่ใช่ของแทนกัน (RFC 9110):

  • 400 Bad Request = client ส่งมา ผิด syntax เช่น JSON parse ไม่ผ่าน, header หายไป
  • 422 Unprocessable Entity = syntax ถูก (JSON valid) แต่ fail business validation เช่น email format ไม่ตรง, age < 0

อย่าใช้สลับกัน — client log จะแยก error case ไม่ออก

Conflict

http
POST /users { "email": "exists@x.com" } → 409 Conflict
Body: { "error": "Email already in use" }

Unauthorized

http
GET /admin → 401 Unauthorized       (ไม่ได้ login)
GET /admin → 403 Forbidden          (login แล้วแต่ไม่มีสิทธิ์)

5. Resource Hierarchy Patterns

A. Sub-resource (Composition)

GET /users/{id}/orders           # orders ของ user นี้
POST /users/{id}/orders          # สร้าง order ให้ user นี้
GET /orders/{id}                 # ดู order ตรง ๆ ก็ได้

→ Order belongs to User — ไม่มี user = ไม่มี order

B. Independent Resource

POST /orders { "userId": 1, ... }   # สร้าง order ที่ระบุ user ใน body
GET /orders?userId=1                 # filter

→ Order อิสระ — relate กับ user ผ่าน FK

เลือกยังไง?

  • Sub-resource เมื่อ relation คงที่ + เห็นจาก parent
  • Independent เมื่อ relation flexible (เช่น many-to-many)

หรือทำ ทั้งคู่ — sub-resource สำหรับ list ตาม parent, independent สำหรับ direct access


6. Action ที่ไม่ใช่ CRUD

REST = "everything is a resource" — แต่บางอย่างก็ไม่ใช่ resource ตรง ๆ

ส่ง email reset password — ไม่ใช่ CRUD
ยกเลิก order — DELETE? ไม่ใช่ ลบจริง ๆ
Refund — POST resource ใหม่

Option 1: POST + verb ใน URL

POST /users/{id}/reset-password
POST /orders/{id}/cancel
POST /payments/{id}/refund
POST /users/{id}/promote-to-admin

💡 Action endpoint = ยกเว้นกฎ noun-only — ปกติ URL ควรเป็นคำนาม แต่ action ที่ไม่ใช่ CRUD ตรง ๆ (reset, cancel, refund) ใช้ verb ใน URL ได้ — เป็น pragmatic exception ที่ Stripe/GitHub ใช้กันอย่างกว้างขวาง

✅ Pragmatic — เห็นใน Stripe, GitHub ✅ ชัด action

Option 2: Action = Resource

POST /password-reset-requests { "email": "..." }   # request
POST /order-cancellations { "orderId": 1 }         # cancellation event
POST /refunds { "paymentId": 1, "amount": 50 }     # refund (มี resource จริง)

✅ ตามจริงแบบ REST ✅ track history ได้

Option 3: Update Status

PATCH /orders/{id} { "status": "CANCELLED" }

⚠️ แต่ status transition rule ซับซ้อน — controller มี business logic → Option 1 ชัดกว่า

กฎ: ถ้า action มี side effect / history → ทำเป็น resource. ถ้าเป็น state transition ง่าย ๆ → PATCH หรือ POST action


7. Query Parameter vs Path Parameter

ใช้ path หรือ query กันแน่? กฎง่าย ๆ คือ path parameter ใช้ "ระบุ resource" (/users/{id}) ส่วน query parameter ใช้ "ปรับผลลัพธ์" (filter, pagination, sort, projection) แยกให้ถูกทำให้ URL อ่านง่ายและสื่อความหมายชัด:

Path Parameter — บอก resource

/users/{id}                    # id = ส่วนของ identity
/users/123/orders/456

Query Parameter — modify ผลลัพธ์

/users?status=active           # filter
/users?page=2&size=20           # pagination
/users?sort=name                # sort
/users?fields=id,name           # sparse fieldset
/users?include=orders           # related data
ใช้ path → identify resource
ใช้ query → filter / paginate / project / sort

8. Response Format

Single Resource

json
{
    "id": 1,
    "email": "anna@example.com",
    "name": "Anna",
    "createdAt": "2026-05-18T10:00:00Z"
}

Wrapped Format

json
{
    "data": { "id": 1, ... },
    "meta": { "version": "v1", "requestId": "..." }
}

Collection

json
{
    "data": [
        { "id": 1, ... },
        { "id": 2, ... }
    ],
    "meta": {
        "page": 1,
        "size": 20,
        "total": 150,
        "hasNext": true
    }
}

หรือ unwrapped:

json
[
    { "id": 1, ... },
    { "id": 2, ... }
]

ทีมเลือกเอง — แค่ consistent ทั้ง API

Field naming

json
✅ camelCase
{
    "firstName": "Anna",
    "createdAt": "2026-05-18T10:00:00Z",
    "isActive": true
}

❌ snake_case (อันนี้ debate ได้ — Stripe ใช้ snake_case)
{ "first_name": "Anna" }

❌ PascalCase
{ "FirstName": "Anna" }

JS ecosystem เอนเอียง camelCase แต่ไม่ใช่กฎเหล็ก — Stripe (JS-heavy) ใช้ snake_case ทั้ง API ส่วน GitHub ใช้ผสม

ทีมเดียวกัน — เลือก 1 + consistent ทั้ง API


9. Date/Time Format

วันที่/เวลาเป็นจุดที่ API พังกันบ่อยเพราะแต่ละที่ format ต่างกัน — มาตรฐานคือ ISO 8601 + UTC (2026-05-18T10:00:00Z) เพราะ parser ทุกภาษารองรับและไม่กำกวมเรื่อง timezone หลีกเลี่ยง format อ่านง่ายแต่กำกวมอย่าง "18/05/2026":

✅ ISO 8601 + UTC + timezone
"2026-05-18T10:00:00Z"
"2026-05-18T10:00:00+07:00"

❌ "May 18, 2026"
❌ "18/05/2026"
❌ Unix timestamp (1747562400) — เว้นแต่ low-level

💡 Z vs +07:00Z (Zulu time) = UTC (เท่ากับ +00:00), ส่วน +07:00 = เร็วกว่า UTC 7 ชั่วโมง (เวลาประเทศไทย). ทั้งสองเป็น ISO 8601 ถูกต้อง — แต่ทีมส่วนใหญ่เก็บ UTC ลง DB (Z) แล้วให้ client แปลงเป็น timezone ของผู้ใช้เอง

ISO 8601 = สากล + parser ทุกภาษารองรับ


10. Field — Project / Sparse Fieldset

ลด data ที่ส่งเมื่อ client ต้องการแค่บางอย่าง:

GET /users?fields=id,name,email
json
[
    { "id": 1, "name": "Anna", "email": "anna@x.com" },
    ...
]

→ ไม่ส่ง field อื่น

→ ลด bandwidth + เร็วขึ้นบน mobile


client มักต้องการข้อมูลที่เกี่ยวข้องพร้อมกัน (order + user + items) — ถ้าให้เรียกแยกหลาย endpoint จะเปลือง round-trip โดยเฉพาะบน mobile การให้ ?include= ดึง related data มาด้วยในครั้งเดียวช่วยลด latency (แต่ต้องระวังไม่ให้ over-fetch):

GET /orders/123              → order
GET /orders/123?include=user,items   → order + user + items
json
{
    "id": 123,
    "total": 100,
    "user": { "id": 1, "name": "Anna" },
    "items": [{ "productId": 5, "quantity": 2 }]
}

→ ลด round-trip จาก client (ไม่ต้องเรียก /users/1 + /items แยก)


12. HATEOAS — Hypermedia

HATEOAS (อ่าน ฮา-ที-โอ-แอส หรือ เฮ-เท-โอ-แอส) = Hypermedia As The Engine Of Application State — แนวคิดที่ response บอก client ว่า "จะทำอะไรต่อได้บ้าง" ผ่าน link ใน body

REST แท้ — response บอกว่า "ทำอะไรต่อได้บ้าง":

🚀 โซนขั้นสูง — ส่วนใหญ่ไม่ทำ ข้ามได้รอบแรก API ส่วนใหญ่ใน 2026 (Stripe/GitHub/Twitter) ไม่ใช้ HATEOAS — ใช้ OpenAPI doc แทน. อ่านเอาแนวคิดพอ

json
{
    "id": 123,
    "status": "PENDING",
    "_links": {
        "self": { "href": "/orders/123" },
        "cancel": { "href": "/orders/123/cancel", "method": "POST" },
        "pay": { "href": "/orders/123/payment", "method": "POST" }
    }
}

ข้อดี:

  • Client ค้น API ได้เอง
  • Server บอกว่า action ไหนใช้ได้ตอนนี้ (ตาม status)

ความจริง:

  • ใช้น้อยมากในปี 2026
  • API doc (OpenAPI) แทน
  • Stripe, Twitter, GitHub ไม่ใช้

→ Optional — ทำเฉพาะเมื่อจำเป็น (เช่น api ที่ client ฉลาดน้อย, integration partner)


13. Idempotency

Idempotency (อ่าน ไอ-เด็ม-โพ-เทน-ซี่) = คุณสมบัติ "เรียกซ้ำกี่ครั้งผลก็เหมือนเดิม" — สำคัญมากตอน network ไม่เสถียร เพราะ client retry ได้โดยไม่กลัวสร้างซ้ำ

GET /users/1          → ผลเดิม (no side effect)
PUT /users/1 {...}    → set state เป็น ... (เรียก N ครั้ง = state เดิม)
DELETE /users/1       → ผลเดิม (ลบเหมือนเดิม + 404 ถ้าไม่มี)
POST /users {...}     → สร้างใหม่ทุกครั้ง (ไม่ idempotent)

ทำไมสำคัญ?

  • Retry ปลอดภัยขึ้น
  • Mobile/network ไม่เสถียร → app retry → ห้าม duplicate

Idempotency-Key (สำหรับ POST)

http
POST /payments
Idempotency-Key: 7e8f9-abc-def...
{ "amount": 100, ... }

Server เก็บ key + return cached response ถ้า retry ภายใน N ชั่วโมง

⚠️ Edge cases ที่ต้องคิด (ตามแนวทาง Stripe):

  • Same key + body ต่างกัน → return 422 Unprocessable Entity (key mismatch — ห้ามใช้ซ้ำกับ payload ใหม่)
  • Concurrent same key → ใช้ lock หรือ return 409 Conflict (request กำลังประมวลผลอยู่)
  • TTL ~24 ชั่วโมง (Stripe default) — เกินนั้น key หมดอายุ retry ไม่ได้
  • ต้อง cache error response ด้วย ไม่ใช่แค่ success — ถ้า request แรกได้ 400 retry ก็ต้องได้ 400 เหมือนกัน
java
@PostMapping("/payments")
public Payment create(
    @RequestHeader("Idempotency-Key") String key,
    @RequestBody PaymentRequest req
) {
    Optional<Payment> cached = idempotencyService.find(key);
    if (cached.isPresent()) return cached.get();
    
    Payment p = paymentService.create(req);
    idempotencyService.store(key, p, Duration.ofHours(24));
    return p;
}

Stripe + GitHub ใช้ pattern นี้


14. Bulk Operation

POST /users/bulk-delete { "ids": [1, 2, 3] }
POST /users/batch-create { "users": [...] }
PATCH /users/bulk-update { "filter": {...}, "set": {...} }

💡 POST สำหรับ bulk = pragmatic exception — RFC 9110 §9.3.5 บอกว่า DELETE มี body ได้ แต่ semantic undefined และ proxy/CDN บางตัว strip body ทิ้ง. ดังนั้น POST /resource/bulk-delete ปลอดภัยกว่า ถึงจะดู anti-RESTful ก็ตาม

หรือ:

POST /users  (one)
POST /users  (one) 
...

แต่ network call เยอะ — bulk operation ดีกว่า

⚠️ ระวัง:

  • Partial failure (5 ใน 10 fail) → tell client ทุก case
  • Transaction หรือ best-effort?
  • Limit: max 100 / 1000 ต่อ call

15. ตัวอย่างจริง — Twitter-like API

http
# Tweets
GET    /api/v1/tweets                          # timeline (your feed)
GET    /api/v1/tweets?author=alice              # filter
GET    /api/v1/tweets/{id}                      # detail
POST   /api/v1/tweets                            # create
DELETE /api/v1/tweets/{id}                       # delete (own tweets only)

# Interactions (actions)
POST   /api/v1/tweets/{id}/likes                 # like (idempotent toggle — ซ้ำ = 200, ไม่ใช่ 409)
DELETE /api/v1/tweets/{id}/likes                 # unlike
POST   /api/v1/tweets/{id}/retweets              # retweet
POST   /api/v1/tweets/{id}/replies               # reply

# Users
GET    /api/v1/users/{username}                  # profile
GET    /api/v1/users/{username}/tweets           # their tweets
POST   /api/v1/users/{username}/follows          # follow
DELETE /api/v1/users/{username}/follows          # unfollow
GET    /api/v1/users/{username}/followers
GET    /api/v1/users/{username}/following

# Me (current user)
GET    /api/v1/me
PATCH  /api/v1/me
GET    /api/v1/me/timeline
GET    /api/v1/me/notifications
POST   /api/v1/me/notifications/mark-read

# Search
GET    /api/v1/search/tweets?q=react
GET    /api/v1/search/users?q=anna

สังเกต:

  • Resource oriented (/tweets, /users)
  • Action ที่มี history → resource (/likes, /retweets)
  • Special path /me สำหรับ current user (ดีกว่าใช้ id ของตัวเอง)
  • Nested ลึกไม่เกิน 2 ระดับ

16. Spring Boot Implementation

มาดูว่าหลักการ REST ทั้งหมดในบทแปลงเป็นโค้ดจริงยังไง — controller Spring Boot ที่ map endpoint ตาม resource, ใช้ @ResponseStatus ให้ status code ตรง, แยก action (/likes) เป็น sub-resource และดึง current user จาก security context สังเกตว่าโครงสร้างตรงกับที่ออกแบบไว้:

📘 ถ้ายังไม่ได้อ่าน Spring Boot — ดูแค่ความสัมพันธ์ endpoint → method ก็พอ. ตัว annotation (@ResponseStatus, @AuthenticationPrincipal, @Valid, Pageable) มีบทแยกอยู่ในหนังสือ Java/Spring Boot — ตอนนี้รู้แค่ว่า "นี่คือวิธี Spring map HTTP เป็น method" พอ

java
@RestController
@RequestMapping("/api/v1/tweets")
@RequiredArgsConstructor
public class TweetController {
    
    private final TweetService tweetService;
    
    @GetMapping
    public PageResponse<TweetResponse> list(
        @RequestParam(required = false) String author,
        @PageableDefault Pageable pageable
    ) {
        return tweetService.list(author, pageable);
    }
    
    @GetMapping("/{id}")
    public TweetResponse get(@PathVariable Long id) {
        return tweetService.getById(id);
    }
    
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public TweetResponse create(
        @RequestBody @Valid CreateTweetRequest req,
        @AuthenticationPrincipal CustomUserDetails user
    ) {
        return tweetService.create(req, user.getId());
    }
    
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(
        @PathVariable Long id,
        @AuthenticationPrincipal CustomUserDetails user
    ) {
        tweetService.delete(id, user.getId());
    }
    
    // Like = idempotent toggle: ใช้ 204 No Content (ไม่มี representation)
    // ถ้าจะ return 201 Created ต้องมี Location header ชี้ไปที่ like resource
    // เช่น Location: /tweets/{id}/likes/{userId}
    @PostMapping("/{id}/likes")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void like(
        @PathVariable Long id,
        @AuthenticationPrincipal CustomUserDetails user
    ) {
        tweetService.like(id, user.getId());
    }
    
    @DeleteMapping("/{id}/likes")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void unlike(
        @PathVariable Long id,
        @AuthenticationPrincipal CustomUserDetails user
    ) {
        tweetService.unlike(id, user.getId());
    }
}

⚠️ 201 Created + void = anti-pattern — RFC 9110 บอกว่า 201 ควรมี Location header ชี้ไปที่ resource ใหม่ ถ้าไม่มี body ไม่มี Location → ใช้ 204 No Content ดีกว่า (เหมือนตัวอย่างด้านบน)


17. ⚠️ Common Pitfalls

GET /api/getUsersGET /api/users
GET /api/user/listGET /api/users
POST /api/users/delete/1DELETE /api/users/1
POST /api/users/1 (update)PATCH /api/users/1
GET ที่มี side effectuse POST/DELETE — GET = safe
Status 200 + { "error": ... }proper 4xx/5xx status
Status 500 ทุก error400 (validation), 404 (not found), 409 (conflict), ...
Mix conventions ใน API เดียวconsistent — เลือก 1
Nested URL ลึก > 2flatten
ลืม version (/api/users)/api/v1/users

18. Checkpoint

🛠️ Checkpoint 1.1 — Design API
ออกแบบ API สำหรับ "Online Bookstore":

  • Resource: book, author, customer, order, review, cart
  • 20+ endpoint ครบ CRUD + nested
  • Status code ที่ถูก
  • ระบุ idempotent / safe

🛠️ Checkpoint 1.2 — Refactor Bad API
จากตัวอย่าง bad API:

GET /getAllOrders
POST /orders/cancel/123
GET /api/orders?id=5
POST /api/users/{userid}/orders/getList

Refactor เป็น API ที่ดี

🛠️ Checkpoint 1.3 — Implement (Spring Boot)
เอา API ที่ออกแบบใน 1.1 — ทำ controller จริง 5 endpoint แรก
ตรวจ status code, response format


19. สรุปบท

URL = noun + plural + lowercase (/users ไม่ใช่ /getUser)
HTTP method ตรงกับ CRUD + idempotency rule
PATCH > PUT สำหรับ partial update (ใช้บ่อย)
✅ Status code: 200/201/204 success, 400/401/403/404/409/422 client error, 500 server error
✅ Sub-resource (/users/{id}/orders) แต่อย่าลึกเกิน 2 ระดับ
✅ Action: ใช้ POST /resource/{id}/verb หรือ ทำเป็น resource ใหม่
✅ Response format consistent + camelCase + ISO 8601 date
✅ Idempotency-Key header สำหรับ POST ที่อยาก retry safe
✅ HATEOAS = optional — OpenAPI doc แทนได้


← บทที่ 0 | บทที่ 2 → Pagination, Filter, Sort