กลับไปที่บทความ
Architecture Backend API Microservices

REST API Communication เทคโนโลยีการสื่อสารระหว่างเซิร์ฟเวอร์

พลากร วรมงคล
15 เมษายน 2568 13 นาที

“คู่มือที่ครอบคลุมเกี่ยวกับ REST API สำหรับการสื่อสารระหว่างเซิร์ฟเวอร์ — ครอบคลุมหลักการออกแบบ API, HTTP semantics, ลวดจำนวน authentication, การจัดการข้อผิดพลาด, rate limiting, circuit breakers, และ production best practices”

การศึกษาลึกเรื่อง: REST API Communication

การสื่อสาร REST API ระหว่างเซิร์ฟเวอร์เป็นกระดูกสันหลังของระบบแจกจ่ายสมัยใหม่ ไม่ว่าคุณกำลังจัดการระบบไมโครเซอร์วิส รวมบริการของบุคคลที่สาม หรือสร้างสถาปัตยกรรมแบ็กเอนด์ที่ปรับขนาดได้ การเข้าใจหลักการ REST และลวดจำนวน implementation patterns เป็นสิ่งจำเป็นสำหรับการสร้างระบบที่แข็งแกร่งและสามารถบำรุงรักษาได้

คู่มือนี้ครอบคลุมทุกอย่างตั้งแต่พื้นฐาน REST จนถึงลวดจำนวน production deployment patterns โดยมีตัวอย่าง TypeScript ที่ใช้งานได้จริงซึ่งคุณสามารถใช้ได้ทันที

REST คืออะไร

REST (Representational State Transfer) เป็นรูปแบบสถาปัตยกรรมสำหรับการออกแบบระบบแจกจ่ายผ่าน HTTP สิ่งสำคัญคือต้องเข้าใจว่า REST ไม่ใช่โปรโตคอล—เป็นชุดข้อจำกัดที่เมื่อนำไปใช้กับ HTTP จะสร้าง APIs ที่สามารถคาดการณ์ได้และปรับขนาดได้

REST ถูกกำหนดรูปแบบอย่างเป็นทางการโดย Roy Fielding ในปี 2000 และสร้างจากข้อจำกัดหกประการ:

  1. Client-Server Architecture — การแยกแยะความเกี่ยวข้องระหว่างไคลเอนต์และเซิร์ฟเวอร์
  2. Statelessness — แต่ละคำขอมีข้อมูลทั้งหมดที่จำเป็นในการทำความเข้าใจและประมวลผล
  3. Uniform Interface — วิธีการสื่อสารที่สอดคล้องกับทรัพยากร
  4. Cacheability — การตอบสนองควรกำหนดตัวเองว่า cacheable หรือไม่
  5. Layered System — ไคลเอนต์ไม่สามารถสมมติว่ามีการเชื่อมต่อโดยตรงกับเซิร์ฟเวอร์สิ้นสุด
  6. Code on Demand (ตัวเลือก) — เซิร์ฟเวอร์สามารถขยายฟังก์ชันการทำงานของไคลเอนต์ได้

Richardson Maturity Model ให้กรอบงานสำหรับ REST maturity:

  • Level 0: POX (Plain Old XML) — endpoint เดียว RPC-style
  • Level 1: Resources — endpoints หลายตัว แต่ HTTP methods ถูกละเว้น
  • Level 2: HTTP Verbs — การใช้ GET, POST, PUT, DELETE อย่างถูกต้อง
  • Level 3: HATEOAS — hypermedia as engine of application state

APIs ในการผลิตส่วนใหญ่มีเป้าหมายที่ Level 2 พร้อมการตัดสินใจทางสถาปัตยกรรมอย่างระมัดระวังเกี่ยวกับ Level 3

A Typical Server-to-Server REST Exchange

นี่คือวิธีที่บริการสองแห่งสื่อสารผ่าน REST:

sequenceDiagram
    participant ServiceA as Service A (Client)
    participant Network as HTTP
    participant ServiceB as Service B (Server)
    
    ServiceA->>ServiceA: Prepare request<br/>POST /api/v1/orders<br/>Authorization: Bearer token<br/>Content-Type: application/json
    ServiceA->>Network: Send HTTP request
    Network->>ServiceB: Route to endpoint handler
    ServiceB->>ServiceB: Authenticate<br/>Validate request<br/>Process business logic
    ServiceB->>ServiceB: Generate response<br/>200 OK<br/>Content-Type: application/json
    ServiceB->>Network: Send HTTP response
    Network->>ServiceA: Deliver response body<br/>{ id, status, timestamp }
    ServiceA->>ServiceA: Parse JSON<br/>Handle response<br/>Update state

Core Principles

  • Resources Over RPC — จำลอง API ของคุณรอบคำนาม (users, orders, payments) ไม่ใช่คำกริยา (getUser, createOrder)
  • HTTP Methods as Verbs — GET ดึงข้อมูล POST สร้าง PUT อัพเดต DELETE ลบ; สิ่งเหล่านี้ไม่ใช่ข้อเสนอแนะ
  • Statelessness — เซิร์ฟเวอร์ไม่รักษาสถานะเซสชัน; แต่ละคำขอเป็นอิสระและสมบูรณ์
  • Idempotency — การทำซ้ำคำขอควรให้ผลลัพธ์เดียวกัน; ปลอดภัยสำหรับการลองใหม่โดยไม่มีผลข้างเคียง
  • Proper Status Codes — ใช้ 2xx สำหรับความสำเร็จ 3xx สำหรับการเปลี่ยนเส้นทาง 4xx สำหรับข้อผิดพลาดของไคลเอนต์ 5xx สำหรับข้อผิดพลาดของเซิร์ฟเวอร์
  • Content Negotiation — รองรับรูปแบบหลายรูปแบบ (JSON, XML) ผ่าน Accept headers เมื่อเหมาะสม
  • Versioning — ออกแบบสำหรับ API evolution ตั้งแต่วันแรก

HTTP Method Semantics and Status Codes

การใช้ HTTP methods อย่างถูกต้องเป็นพื้นฐานของ REST แต่ละวิธีมี semantics เฉพาะและผลข้างเคียงที่คาดหวัง

GET — Safe and Idempotent

app.get('/api/v1/orders/:orderId', (req, res) => {
  try {
    const order = orderService.getOrder(req.params.orderId);
    
    if (!order) {
      return res.status(404).json({
        type: 'https://api.example.com/errors/not-found',
        title: 'Order Not Found',
        status: 404,
        detail: `Order ${req.params.orderId} does not exist`,
      });
    }
    
    res.set('Cache-Control', 'public, max-age=300');
    res.status(200).json(order);
  } catch (error) {
    res.status(500).json(createErrorResponse(error));
  }
});
@GetMapping("/api/v1/orders/{orderId}")
public ResponseEntity<?> getOrder(@PathVariable String orderId) {
    try {
        Order order = orderService.getOrder(orderId);
        if (order == null) {
            return ResponseEntity.status(404).body(Map.of(
                "type", "https://api.example.com/errors/not-found",
                "title", "Order Not Found",
                "status", 404,
                "detail", "Order " + orderId + " does not exist"
            ));
        }
        return ResponseEntity.ok()
            .header("Cache-Control", "public, max-age=300")
            .body(order);
    } catch (Exception e) {
        return ResponseEntity.status(500).body(createErrorResponse(e));
    }
}
from fastapi import APIRouter
from fastapi.responses import JSONResponse

router = APIRouter()

@router.get("/api/v1/orders/{order_id}")
async def get_order(order_id: str):
    try:
        order = order_service.get_order(order_id)
        if order is None:
            return JSONResponse(status_code=404, content={
                "type": "https://api.example.com/errors/not-found",
                "title": "Order Not Found",
                "status": 404,
                "detail": f"Order {order_id} does not exist",
            })
        return JSONResponse(
            status_code=200,
            content=order.dict(),
            headers={"Cache-Control": "public, max-age=300"},
        )
    except Exception as e:
        return JSONResponse(status_code=500, content=create_error_response(e))
[HttpGet("api/v1/orders/{orderId}")]
public async Task<IActionResult> GetOrder(string orderId)
{
    try
    {
        var order = await _orderService.GetOrderAsync(orderId);
        if (order == null)
        {
            return NotFound(new
            {
                type = "https://api.example.com/errors/not-found",
                title = "Order Not Found",
                status = 404,
                detail = $"Order {orderId} does not exist"
            });
        }
        Response.Headers.CacheControl = "public, max-age=300";
        return Ok(order);
    }
    catch (Exception e)
    {
        return StatusCode(500, CreateErrorResponse(e));
    }
}

Characteristics: Safe (ไม่มีผลข้างเคียง), idempotent, cacheable, ไม่ควรมี request body

Common Status Codes:

  • 200 OK — การดึงข้อมูลสำเร็จ
  • 304 Not Modified — ทรัพยากรไม่เปลี่ยนแปลงตั้งแต่ header If-Modified-Since
  • 404 Not Found — ทรัพยากรไม่มีอยู่
  • 410 Gone — ทรัพยากรถูกลบออกถาวร

POST — Creates Resources

app.post('/api/v1/orders', (req, res) => {
  try {
    const { userId, items, shippingAddress } = req.body;
    
    // Validate request
    const validation = validateOrderRequest({ userId, items, shippingAddress });
    if (!validation.valid) {
      return res.status(400).json({
        type: 'https://api.example.com/errors/validation-error',
        title: 'Validation Failed',
        status: 400,
        detail: validation.errors.join('; '),
        instance: req.path,
      });
    }
    
    // Create order (may fail with conflict)
    const newOrder = await orderService.createOrder(userId, items, shippingAddress);
    
    // Return 201 Created with Location header
    res
      .status(201)
      .set('Location', `/api/v1/orders/${newOrder.id}`)
      .json(newOrder);
  } catch (error) {
    if (error instanceof ConflictError) {
      return res.status(409).json({
        type: 'https://api.example.com/errors/conflict',
        title: 'Conflict',
        status: 409,
        detail: error.message,
      });
    }
    res.status(500).json(createErrorResponse(error));
  }
});
@PostMapping("/api/v1/orders")
public ResponseEntity<?> createOrder(@RequestBody CreateOrderRequest body) {
    try {
        // Validate request
        ValidationResult validation = validateOrderRequest(body);
        if (!validation.isValid()) {
            return ResponseEntity.status(400).body(Map.of(
                "type", "https://api.example.com/errors/validation-error",
                "title", "Validation Failed",
                "status", 400,
                "detail", String.join("; ", validation.getErrors())
            ));
        }

        Order newOrder = orderService.createOrder(
            body.getUserId(), body.getItems(), body.getShippingAddress());

        return ResponseEntity.status(201)
            .header("Location", "/api/v1/orders/" + newOrder.getId())
            .body(newOrder);
    } catch (ConflictException e) {
        return ResponseEntity.status(409).body(Map.of(
            "type", "https://api.example.com/errors/conflict",
            "title", "Conflict",
            "status", 409,
            "detail", e.getMessage()
        ));
    } catch (Exception e) {
        return ResponseEntity.status(500).body(createErrorResponse(e));
    }
}
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel

router = APIRouter()

class CreateOrderRequest(BaseModel):
    user_id: str
    items: list
    shipping_address: dict

@router.post("/api/v1/orders")
async def create_order(body: CreateOrderRequest, request: Request):
    try:
        validation = validate_order_request(body)
        if not validation.valid:
            return JSONResponse(status_code=400, content={
                "type": "https://api.example.com/errors/validation-error",
                "title": "Validation Failed",
                "status": 400,
                "detail": "; ".join(validation.errors),
                "instance": str(request.url.path),
            })

        new_order = await order_service.create_order(
            body.user_id, body.items, body.shipping_address)

        return JSONResponse(
            status_code=201,
            content=new_order.dict(),
            headers={"Location": f"/api/v1/orders/{new_order.id}"},
        )
    except ConflictError as e:
        return JSONResponse(status_code=409, content={
            "type": "https://api.example.com/errors/conflict",
            "title": "Conflict",
            "status": 409,
            "detail": str(e),
        })
    except Exception as e:
        return JSONResponse(status_code=500, content=create_error_response(e))
[HttpPost("api/v1/orders")]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest body)
{
    try
    {
        var validation = ValidateOrderRequest(body);
        if (!validation.Valid)
        {
            return BadRequest(new
            {
                type = "https://api.example.com/errors/validation-error",
                title = "Validation Failed",
                status = 400,
                detail = string.Join("; ", validation.Errors),
                instance = Request.Path.Value
            });
        }

        var newOrder = await _orderService.CreateOrderAsync(
            body.UserId, body.Items, body.ShippingAddress);

        Response.Headers.Location = $"/api/v1/orders/{newOrder.Id}";
        return StatusCode(201, newOrder);
    }
    catch (ConflictException e)
    {
        return Conflict(new
        {
            type = "https://api.example.com/errors/conflict",
            title = "Conflict",
            status = 409,
            detail = e.Message
        });
    }
    catch (Exception e)
    {
        return StatusCode(500, CreateErrorResponse(e));
    }
}

Characteristics: ไม่ปลอดภัย (สร้างผลข้างเคียง), ไม่ idempotent (แต่ละคำขอสร้างทรัพยากรใหม่), โดยทั่วไปมี request body

Common Status Codes:

  • 201 Created — ทรัพยากรถูกสร้างสำเร็จ รวม Location header พร้อม URI ทรัพยากรใหม่
  • 202 Accepted — คำขอได้รับการยอมรับสำหรับการประมวลผล async คืน status URI
  • 400 Bad Request — การตรวจสอบล้มเหลว
  • 409 Conflict — คำขอขัดแย้งกับสถานะปัจจุบัน (duplicate constraint violation)

PUT — Full Replacement

app.put('/api/v1/orders/:orderId', (req, res) => {
  try {
    const { items, shippingAddress, status } = req.body;
    
    // Validate complete resource
    const validation = validateCompleteOrder({ items, shippingAddress, status });
    if (!validation.valid) {
      return res.status(400).json({
        type: 'https://api.example.com/errors/validation-error',
        title: 'Invalid Order',
        status: 400,
        detail: validation.errors.join('; '),
      });
    }
    
    // PUT replaces entire resource; fail if missing required fields
    const updated = await orderService.replaceOrder(req.params.orderId, {
      items,
      shippingAddress,
      status,
    });
    
    if (!updated) {
      return res.status(404).json({
        type: 'https://api.example.com/errors/not-found',
        title: 'Order Not Found',
        status: 404,
        detail: `Order ${req.params.orderId} not found`,
      });
    }
    
    res.status(200).json(updated);
  } catch (error) {
    res.status(500).json(createErrorResponse(error));
  }
});
@PutMapping("/api/v1/orders/{orderId}")
public ResponseEntity<?> replaceOrder(
        @PathVariable String orderId,
        @RequestBody ReplaceOrderRequest body) {
    try {
        ValidationResult validation = validateCompleteOrder(body);
        if (!validation.isValid()) {
            return ResponseEntity.status(400).body(Map.of(
                "type", "https://api.example.com/errors/validation-error",
                "title", "Invalid Order",
                "status", 400,
                "detail", String.join("; ", validation.getErrors())
            ));
        }

        Order updated = orderService.replaceOrder(orderId, body);
        if (updated == null) {
            return ResponseEntity.status(404).body(Map.of(
                "type", "https://api.example.com/errors/not-found",
                "title", "Order Not Found",
                "status", 404,
                "detail", "Order " + orderId + " not found"
            ));
        }

        return ResponseEntity.ok(updated);
    } catch (Exception e) {
        return ResponseEntity.status(500).body(createErrorResponse(e));
    }
}
from fastapi import APIRouter
from fastapi.responses import JSONResponse
from pydantic import BaseModel

router = APIRouter()

class ReplaceOrderRequest(BaseModel):
    items: list
    shipping_address: dict
    status: str

@router.put("/api/v1/orders/{order_id}")
async def replace_order(order_id: str, body: ReplaceOrderRequest):
    try:
        validation = validate_complete_order(body)
        if not validation.valid:
            return JSONResponse(status_code=400, content={
                "type": "https://api.example.com/errors/validation-error",
                "title": "Invalid Order",
                "status": 400,
                "detail": "; ".join(validation.errors),
            })

        updated = await order_service.replace_order(order_id, body)
        if updated is None:
            return JSONResponse(status_code=404, content={
                "type": "https://api.example.com/errors/not-found",
                "title": "Order Not Found",
                "status": 404,
                "detail": f"Order {order_id} not found",
            })

        return JSONResponse(status_code=200, content=updated.dict())
    except Exception as e:
        return JSONResponse(status_code=500, content=create_error_response(e))
[HttpPut("api/v1/orders/{orderId}")]
public async Task<IActionResult> ReplaceOrder(
    string orderId, [FromBody] ReplaceOrderRequest body)
{
    try
    {
        var validation = ValidateCompleteOrder(body);
        if (!validation.Valid)
        {
            return BadRequest(new
            {
                type = "https://api.example.com/errors/validation-error",
                title = "Invalid Order",
                status = 400,
                detail = string.Join("; ", validation.Errors)
            });
        }

        var updated = await _orderService.ReplaceOrderAsync(orderId, body);
        if (updated == null)
        {
            return NotFound(new
            {
                type = "https://api.example.com/errors/not-found",
                title = "Order Not Found",
                status = 404,
                detail = $"Order {orderId} not found"
            });
        }

        return Ok(updated);
    }
    catch (Exception e)
    {
        return StatusCode(500, CreateErrorResponse(e));
    }
}

Characteristics: ไม่ปลอดภัย, idempotent (คำขอเดียวกันให้ผลลัพธ์เดียวกัน), ต้องมีทรัพยากรทั้งหมดใน body

Common Status Codes:

  • 200 OK — อัพเดตสำเร็จ
  • 201 Created — อัพเดตสร้างทรัพยากรใหม่ (ถ้า URI ไม่มีอยู่)
  • 204 No Content — อัพเดตสำเร็จ แต่ไม่มี response body
  • 400 Bad Request — ทรัพยากรที่ไม่สมบูรณ์หรือไม่ถูกต้อง

PATCH — Partial Update

app.patch('/api/v1/orders/:orderId', (req, res) => {
  try {
    const updates = req.body; // Only provided fields
    
    // Validate only the fields being updated
    const validation = validateOrderUpdates(updates);
    if (!validation.valid) {
      return res.status(400).json({
        type: 'https://api.example.com/errors/validation-error',
        title: 'Invalid Update',
        status: 400,
        detail: validation.errors.join('; '),
      });
    }
    
    // Apply partial updates
    const updated = await orderService.patchOrder(req.params.orderId, updates);
    
    if (!updated) {
      return res.status(404).json({
        type: 'https://api.example.com/errors/not-found',
        title: 'Order Not Found',
        status: 404,
        detail: `Order ${req.params.orderId} not found`,
      });
    }
    
    res.status(200).json(updated);
  } catch (error) {
    res.status(500).json(createErrorResponse(error));
  }
});
@PatchMapping("/api/v1/orders/{orderId}")
public ResponseEntity<?> patchOrder(
        @PathVariable String orderId,
        @RequestBody Map<String, Object> updates) {
    try {
        ValidationResult validation = validateOrderUpdates(updates);
        if (!validation.isValid()) {
            return ResponseEntity.status(400).body(Map.of(
                "type", "https://api.example.com/errors/validation-error",
                "title", "Invalid Update",
                "status", 400,
                "detail", String.join("; ", validation.getErrors())
            ));
        }

        Order updated = orderService.patchOrder(orderId, updates);
        if (updated == null) {
            return ResponseEntity.status(404).body(Map.of(
                "type", "https://api.example.com/errors/not-found",
                "title", "Order Not Found",
                "status", 404,
                "detail", "Order " + orderId + " not found"
            ));
        }

        return ResponseEntity.ok(updated);
    } catch (Exception e) {
        return ResponseEntity.status(500).body(createErrorResponse(e));
    }
}
from fastapi import APIRouter
from fastapi.responses import JSONResponse

router = APIRouter()

@router.patch("/api/v1/orders/{order_id}")
async def patch_order(order_id: str, updates: dict):
    try:
        validation = validate_order_updates(updates)
        if not validation.valid:
            return JSONResponse(status_code=400, content={
                "type": "https://api.example.com/errors/validation-error",
                "title": "Invalid Update",
                "status": 400,
                "detail": "; ".join(validation.errors),
            })

        updated = await order_service.patch_order(order_id, updates)
        if updated is None:
            return JSONResponse(status_code=404, content={
                "type": "https://api.example.com/errors/not-found",
                "title": "Order Not Found",
                "status": 404,
                "detail": f"Order {order_id} not found",
            })

        return JSONResponse(status_code=200, content=updated.dict())
    except Exception as e:
        return JSONResponse(status_code=500, content=create_error_response(e))
[HttpPatch("api/v1/orders/{orderId}")]
public async Task<IActionResult> PatchOrder(
    string orderId, [FromBody] JsonElement updates)
{
    try
    {
        var updateDict = System.Text.Json.JsonSerializer
            .Deserialize<Dictionary<string, object>>(updates.GetRawText())!;

        var validation = ValidateOrderUpdates(updateDict);
        if (!validation.Valid)
        {
            return BadRequest(new
            {
                type = "https://api.example.com/errors/validation-error",
                title = "Invalid Update",
                status = 400,
                detail = string.Join("; ", validation.Errors)
            });
        }

        var updated = await _orderService.PatchOrderAsync(orderId, updateDict);
        if (updated == null)
        {
            return NotFound(new
            {
                type = "https://api.example.com/errors/not-found",
                title = "Order Not Found",
                status = 404,
                detail = $"Order {orderId} not found"
            });
        }

        return Ok(updated);
    }
    catch (Exception e)
    {
        return StatusCode(500, CreateErrorResponse(e));
    }
}

Characteristics: ไม่ปลอดภัย อาจ idempotent หรือไม่ (ขึ้นอยู่กับการดำเนินการ), ทรัพยากรบางส่วนใน body

Common Status Codes:

  • 200 OK — patch สำเร็จ คืนทรัพยากรที่อัพเดต
  • 204 No Content — patch สำเร็จ ไม่มี body
  • 400 Bad Request — patch operations ไม่ถูกต้อง
  • 422 Unprocessable Entity — patch ไม่ถูกต้องทางความหมาย

DELETE — Resource Removal

app.delete('/api/v1/orders/:orderId', (req, res) => {
  try {
    const deleted = await orderService.deleteOrder(req.params.orderId);
    
    if (!deleted) {
      return res.status(404).json({
        type: 'https://api.example.com/errors/not-found',
        title: 'Order Not Found',
        status: 404,
        detail: `Order ${req.params.orderId} not found`,
      });
    }
    
    // Common pattern: return 204 No Content or 200 with deleted resource
    res.status(204).send();
  } catch (error) {
    res.status(500).json(createErrorResponse(error));
  }
});
@DeleteMapping("/api/v1/orders/{orderId}")
public ResponseEntity<?> deleteOrder(@PathVariable String orderId) {
    try {
        boolean deleted = orderService.deleteOrder(orderId);
        if (!deleted) {
            return ResponseEntity.status(404).body(Map.of(
                "type", "https://api.example.com/errors/not-found",
                "title", "Order Not Found",
                "status", 404,
                "detail", "Order " + orderId + " not found"
            ));
        }
        return ResponseEntity.noContent().build(); // 204
    } catch (Exception e) {
        return ResponseEntity.status(500).body(createErrorResponse(e));
    }
}
from fastapi import APIRouter
from fastapi.responses import JSONResponse, Response

router = APIRouter()

@router.delete("/api/v1/orders/{order_id}")
async def delete_order(order_id: str):
    try:
        deleted = await order_service.delete_order(order_id)
        if not deleted:
            return JSONResponse(status_code=404, content={
                "type": "https://api.example.com/errors/not-found",
                "title": "Order Not Found",
                "status": 404,
                "detail": f"Order {order_id} not found",
            })
        return Response(status_code=204)
    except Exception as e:
        return JSONResponse(status_code=500, content=create_error_response(e))
[HttpDelete("api/v1/orders/{orderId}")]
public async Task<IActionResult> DeleteOrder(string orderId)
{
    try
    {
        bool deleted = await _orderService.DeleteOrderAsync(orderId);
        if (!deleted)
        {
            return NotFound(new
            {
                type = "https://api.example.com/errors/not-found",
                title = "Order Not Found",
                status = 404,
                detail = $"Order {orderId} not found"
            });
        }
        return NoContent(); // 204
    }
    catch (Exception e)
    {
        return StatusCode(500, CreateErrorResponse(e));
    }
}

Characteristics: ไม่ปลอดภัย, idempotent (การลบทรัพยากรที่ถูกลบแล้วให้ผลลัพธ์เดียวกัน), โดยทั่วไปไม่มี request body

Common Status Codes:

  • 204 No Content — ลบสำเร็จ ไม่มี body
  • 200 OK — ลบสำเร็จ อาจคืนทรัพยากรที่ถูกลบ
  • 404 Not Found — ทรัพยากรไม่มีอยู่ (มีการถกเถียงบ้างเกี่ยวกับ idempotency ที่นี่)

API Versioning Strategies

Versioning เป็นสิ่งจำเป็นสำหรับการจัดการ API evolution โดยไม่ทำลายไคลเอนต์ มีสามกลยุทธ์หลัก:

1. URL Path Versioning

ชัดเจนและมองเห็นได้มากที่สุด ปรับเส้นทางและแคชอย่างแตกต่างกันตามเวอร์ชัน

app.get('/api/v1/users/:id', v1UserHandler);
app.get('/api/v2/users/:id', v2UserHandler);

// Allows deprecating old versions gradually
app.use('/api/v1/*', (req, res, next) => {
  res.set('Sunset', new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toUTCString());
  res.set('Deprecation', 'true');
  next();
});
// Spring Boot — separate controllers per version
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
    @GetMapping("/{id}")
    public ResponseEntity<UserV1> getUser(@PathVariable String id) {
        return ResponseEntity.ok(userServiceV1.getUser(id));
    }
}

@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
    @GetMapping("/{id}")
    public ResponseEntity<UserV2> getUser(@PathVariable String id) {
        return ResponseEntity.ok(userServiceV2.getUser(id));
    }
}

// Deprecation filter for v1
@Component
@Order(1)
public class DeprecationFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        if (request.getRequestURI().startsWith("/api/v1/")) {
            long sunsetMs = System.currentTimeMillis() + 30L * 24 * 3600 * 1000;
            response.setHeader("Sunset", new Date(sunsetMs).toInstant().toString());
            response.setHeader("Deprecation", "true");
        }
        chain.doFilter(req, res);
    }
}
from fastapi import APIRouter, Response
from datetime import datetime, timedelta, timezone

router_v1 = APIRouter(prefix="/api/v1")
router_v2 = APIRouter(prefix="/api/v2")

@router_v1.get("/users/{user_id}")
async def get_user_v1(user_id: str, response: Response):
    # Mark v1 as deprecated
    sunset = datetime.now(timezone.utc) + timedelta(days=30)
    response.headers["Sunset"] = sunset.strftime("%a, %d %b %Y %H:%M:%S GMT")
    response.headers["Deprecation"] = "true"
    return user_service_v1.get_user(user_id)

@router_v2.get("/users/{user_id}")
async def get_user_v2(user_id: str):
    return user_service_v2.get_user(user_id)
// ASP.NET Core — Asp.Versioning package
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/users")]
public class UserControllerV1 : ControllerBase
{
    [HttpGet("{id}")]
    [Obsolete]
    public IActionResult GetUser(string id)
    {
        var sunset = DateTime.UtcNow.AddDays(30).ToString("R");
        Response.Headers["Sunset"] = sunset;
        Response.Headers["Deprecation"] = "true";
        return Ok(_userServiceV1.GetUser(id));
    }
}

[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/users")]
public class UserControllerV2 : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetUser(string id) => Ok(_userServiceV2.GetUser(id));
}

Pros: Versioning ชัดเจน แคชง่าย ปรับเส้นทางแบบง่าย Cons: Codebases หลายตัวหรือ branching ที่สำคัญ โค้ดที่ซ้ำกัน

2. Header-Based Versioning

มองเห็นได้น้อยกว่าใน URLs โครงสร้าง endpoint ที่สะอาด

const apiVersion = (req: Request): string => {
  return req.get('API-Version') || req.get('Accept-Version') || '1.0';
};

app.get('/api/users/:id', (req, res) => {
  const version = apiVersion(req);
  
  if (version.startsWith('1')) {
    return userHandlerV1(req, res);
  } else if (version.startsWith('2')) {
    return userHandlerV2(req, res);
  }
  
  res.status(400).json({
    type: 'https://api.example.com/errors/unsupported-version',
    title: 'Unsupported API Version',
    status: 400,
    detail: `API version ${version} is not supported`,
  });
});
@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    public ResponseEntity<?> getUser(
            @PathVariable String id,
            @RequestHeader(value = "API-Version", defaultValue = "1.0") String apiVersion) {

        if (apiVersion.startsWith("1")) {
            return ResponseEntity.ok(userServiceV1.getUser(id));
        } else if (apiVersion.startsWith("2")) {
            return ResponseEntity.ok(userServiceV2.getUser(id));
        }

        return ResponseEntity.status(400).body(Map.of(
            "type", "https://api.example.com/errors/unsupported-version",
            "title", "Unsupported API Version",
            "status", 400,
            "detail", "API version " + apiVersion + " is not supported"
        ));
    }
}
from fastapi import APIRouter, Header
from fastapi.responses import JSONResponse

router = APIRouter()

@router.get("/api/users/{user_id}")
async def get_user(
    user_id: str,
    api_version: str = Header(default="1.0", alias="API-Version"),
):
    if api_version.startswith("1"):
        return user_service_v1.get_user(user_id)
    elif api_version.startswith("2"):
        return user_service_v2.get_user(user_id)

    return JSONResponse(status_code=400, content={
        "type": "https://api.example.com/errors/unsupported-version",
        "title": "Unsupported API Version",
        "status": 400,
        "detail": f"API version {api_version} is not supported",
    })
[ApiController]
[Route("api/users")]
public class UserController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetUser(
        string id,
        [FromHeader(Name = "API-Version")] string apiVersion = "1.0")
    {
        if (apiVersion.StartsWith("1"))
            return Ok(_userServiceV1.GetUser(id));
        if (apiVersion.StartsWith("2"))
            return Ok(_userServiceV2.GetUser(id));

        return BadRequest(new
        {
            type = "https://api.example.com/errors/unsupported-version",
            title = "Unsupported API Version",
            status = 400,
            detail = $"API version {apiVersion} is not supported"
        });
    }
}

Pros: โครงสร้าง URL เดียว API surface ไม่วุ่นวาย Cons: มองเห็นได้น้อย แคชยากขึ้น ต้องรู้ header ของไคลเอนต์

3. Query Parameter Versioning

แนะนำอย่างน้อย แต่มีอยู่

app.get('/api/users/:id', (req, res) => {
  const version = req.query.v || '1.0';
  // Version handling...
});
@GetMapping("/api/users/{id}")
public ResponseEntity<?> getUser(
        @PathVariable String id,
        @RequestParam(defaultValue = "1.0") String v) {
    // Version handling...
    return ResponseEntity.ok(userService.getUser(id, v));
}
from fastapi import APIRouter, Query

router = APIRouter()

@router.get("/api/users/{user_id}")
async def get_user(user_id: str, v: str = Query(default="1.0")):
    # Version handling...
    return user_service.get_user(user_id, v)
[HttpGet("api/users/{id}")]
public IActionResult GetUser(string id, [FromQuery] string v = "1.0")
{
    // Version handling...
    return Ok(_userService.GetUser(id, v));
}

Pros: ง่าย backward compatible default Cons: ปัญหาแคช ไม่ใช่ RESTful ผสมความกังวลในพารามิเตอร์คิวรี

Recommendation: ใช้ URL path versioning สำหรับเวอร์ชันหลักพร้อมกำหนดเวลา deprecation ที่ชัดเจน วางแผนสนับสนุนเป็นเวลาอย่างน้อย 2-3 ปี

Authentication Between Services

การสื่อสารระหว่างเซิร์ฟเวอร์ต้องใช้ authentication ที่แข็งแกร่ง การเลือกขึ้นอยู่กับโครงสร้างพื้นฐานและข้อกำหนดด้านความปลอดภัย

API Keys (Simple but Limited)

interface ApiKeyCredential {
  keyId: string;
  secret: string;
}

async function validateApiKey(keyId: string, signature: string, body: string): Promise<boolean> {
  const credential = await credentialStore.get(keyId);
  if (!credential) return false;
  
  // Create signature: HMAC-SHA256(secret, method + path + timestamp + body)
  const expectedSignature = crypto
    .createHmac('sha256', credential.secret)
    .update(body)
    .digest('hex');
  
  // Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

app.use('/api/v1/*', async (req, res, next) => {
  const auth = req.get('Authorization');
  if (!auth?.startsWith('ApiKey ')) {
    return res.status(401).json({
      type: 'https://api.example.com/errors/unauthorized',
      title: 'Unauthorized',
      status: 401,
      detail: 'Missing or invalid authorization',
    });
  }
  
  const [keyId, signature] = auth.slice(7).split(':');
  const body = req.body ? JSON.stringify(req.body) : '';
  
  const valid = await validateApiKey(keyId, signature, body);
  if (!valid) {
    return res.status(401).json({
      type: 'https://api.example.com/errors/unauthorized',
      title: 'Unauthorized',
      status: 401,
      detail: 'Invalid credentials',
    });
  }
  
  req.user = { keyId, authenticated: true };
  next();
});
@Component
public class ApiKeyFilter extends OncePerRequestFilter {

    private final CredentialStore credentialStore;

    public ApiKeyFilter(CredentialStore credentialStore) {
        this.credentialStore = credentialStore;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws ServletException, IOException {

        String auth = request.getHeader("Authorization");
        if (auth == null || !auth.startsWith("ApiKey ")) {
            sendUnauthorized(response, "Missing or invalid authorization");
            return;
        }

        String[] parts = auth.substring(7).split(":", 2);
        if (parts.length != 2) { sendUnauthorized(response, "Invalid authorization format"); return; }

        String keyId = parts[0];
        String signature = parts[1];
        String body = new String(request.getInputStream().readAllBytes(), StandardCharsets.UTF_8);

        boolean valid = validateApiKey(keyId, signature, body);
        if (!valid) { sendUnauthorized(response, "Invalid credentials"); return; }

        SecurityContextHolder.getContext().setAuthentication(
            new ApiKeyAuthenticationToken(keyId));
        chain.doFilter(request, response);
    }

    private boolean validateApiKey(String keyId, String signature, String body) {
        ApiKeyCredential credential = credentialStore.get(keyId);
        if (credential == null) return false;

        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(credential.getSecret().getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            String expected = HexFormat.of().formatHex(mac.doFinal(body.getBytes(StandardCharsets.UTF_8)));
            return MessageDigest.isEqual(signature.getBytes(), expected.getBytes());
        } catch (Exception e) { return false; }
    }

    private void sendUnauthorized(HttpServletResponse response, String detail) throws IOException {
        response.setStatus(401);
        response.setContentType("application/json");
        response.getWriter().write(String.format(
            "{\"type\":\"https://api.example.com/errors/unauthorized\",\"title\":\"Unauthorized\",\"status\":401,\"detail\":\"%s\"}", detail));
    }
}
import hmac, hashlib
from fastapi import Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware

class ApiKeyMiddleware(BaseHTTPMiddleware):
    def __init__(self, app, credential_store):
        super().__init__(app)
        self.credential_store = credential_store

    async def dispatch(self, request: Request, call_next):
        if not request.url.path.startswith("/api/v1/"):
            return await call_next(request)

        auth = request.headers.get("Authorization", "")
        if not auth.startswith("ApiKey "):
            return JSONResponse(status_code=401, content={
                "type": "https://api.example.com/errors/unauthorized",
                "title": "Unauthorized",
                "status": 401,
                "detail": "Missing or invalid authorization",
            })

        parts = auth[7:].split(":", 1)
        if len(parts) != 2:
            return JSONResponse(status_code=401, content={"detail": "Invalid format"})

        key_id, signature = parts
        body = (await request.body()).decode()

        credential = self.credential_store.get(key_id)
        if not credential:
            return JSONResponse(status_code=401, content={"detail": "Invalid credentials"})

        expected = hmac.new(
            credential["secret"].encode(), body.encode(), hashlib.sha256
        ).hexdigest()

        if not hmac.compare_digest(signature, expected):
            return JSONResponse(status_code=401, content={
                "type": "https://api.example.com/errors/unauthorized",
                "title": "Unauthorized",
                "status": 401,
                "detail": "Invalid credentials",
            })

        request.state.key_id = key_id
        return await call_next(request)
public class ApiKeyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ICredentialStore _credentialStore;

    public ApiKeyMiddleware(RequestDelegate next, ICredentialStore credentialStore)
    {
        _next = next;
        _credentialStore = credentialStore;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (!context.Request.Path.StartsWithSegments("/api/v1"))
        {
            await _next(context);
            return;
        }

        var auth = context.Request.Headers.Authorization.ToString();
        if (!auth.StartsWith("ApiKey "))
        {
            await WriteUnauthorized(context, "Missing or invalid authorization");
            return;
        }

        var parts = auth[7..].Split(':', 2);
        if (parts.Length != 2) { await WriteUnauthorized(context, "Invalid format"); return; }

        var (keyId, signature) = (parts[0], parts[1]);
        context.Request.EnableBuffering();
        var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
        context.Request.Body.Position = 0;

        var credential = await _credentialStore.GetAsync(keyId);
        if (credential == null) { await WriteUnauthorized(context, "Invalid credentials"); return; }

        var keyBytes = Encoding.UTF8.GetBytes(credential.Secret);
        var expected = Convert.ToHexString(
            HMACSHA256.HashData(keyBytes, Encoding.UTF8.GetBytes(body))).ToLower();

        if (!CryptographicOperations.FixedTimeEquals(
                Encoding.UTF8.GetBytes(signature), Encoding.UTF8.GetBytes(expected)))
        {
            await WriteUnauthorized(context, "Invalid credentials");
            return;
        }

        context.Items["keyId"] = keyId;
        await _next(context);
    }

    private static async Task WriteUnauthorized(HttpContext context, string detail)
    {
        context.Response.StatusCode = 401;
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsJsonAsync(new
        {
            type = "https://api.example.com/errors/unauthorized",
            title = "Unauthorized",
            status = 401,
            detail
        });
    }
}

Pros: ใช้งานง่าย หมุนเวียนง่าย Cons: Key ใช้เป็นข้อความธรรมชาติ (ใช้ HTTPS เสมอ) ไม่มีการหมดอายุ ขอบเขตจำกัด

mTLS (Mutual TLS)

ปลอดภัยที่สุดสำหรับ service-to-service โดยเฉพาะในสภาแวดล้อม Kubernetes/container

import https from 'https';
import fs from 'fs';
import { createSecureClient } from './http-client';

// Server setup
const serverOptions = {
  key: fs.readFileSync('./certs/server.key'),
  cert: fs.readFileSync('./certs/server.crt'),
  ca: fs.readFileSync('./certs/ca.crt'),
  requestCert: true,
  rejectUnauthorized: true,
};

https.createServer(serverOptions, app).listen(3000);

// Client setup for calling other services
export const createMtlsClient = () => {
  const tlsOptions = {
    key: fs.readFileSync('./certs/client.key'),
    cert: fs.readFileSync('./certs/client.crt'),
    ca: fs.readFileSync('./certs/ca.crt'),
    rejectUnauthorized: true,
  };
  
  const agent = new https.Agent(tlsOptions);
  
  return async (url: string, options: RequestInit = {}) => {
    return fetch(url, {
      ...options,
      agent,
    });
  };
};
// Spring Boot — configure via application.properties or programmatically
// application.properties:
// server.ssl.key-store=classpath:certs/server.p12
// server.ssl.key-store-password=changeit
// server.ssl.trust-store=classpath:certs/ca.p12
// server.ssl.client-auth=need

// mTLS HTTP client using Apache HttpClient 5
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import javax.net.ssl.*;

@Bean
public CloseableHttpClient mtlsHttpClient() throws Exception {
    KeyStore keyStore = KeyStore.getInstance("PKCS12");
    try (var is = new FileInputStream("certs/client.p12")) {
        keyStore.load(is, "changeit".toCharArray());
    }

    KeyStore trustStore = KeyStore.getInstance("PKCS12");
    try (var is = new FileInputStream("certs/ca.p12")) {
        trustStore.load(is, "changeit".toCharArray());
    }

    SSLContext sslContext = SSLContextBuilder.create()
        .loadKeyMaterial(keyStore, "changeit".toCharArray())
        .loadTrustMaterial(trustStore, null)
        .build();

    return HttpClients.custom()
        .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create()
            .setSSLSocketFactory(new SSLConnectionSocketFactory(sslContext))
            .build())
        .build();
}
# FastAPI server with mTLS via uvicorn SSL options
# Run: uvicorn main:app --ssl-keyfile certs/server.key --ssl-certfile certs/server.crt --ssl-ca-certs certs/ca.crt

# mTLS HTTP client using httpx
import httpx
import ssl

def create_mtls_client() -> httpx.AsyncClient:
    ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile="certs/ca.crt")
    ssl_context.load_cert_chain(certfile="certs/client.crt", keyfile="certs/client.key")
    ssl_context.verify_mode = ssl.CERT_REQUIRED
    return httpx.AsyncClient(verify=ssl_context)

# Usage
async def call_service(url: str):
    async with create_mtls_client() as client:
        response = await client.get(url)
        return response.json()
// Server: configure in Program.cs
builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        httpsOptions.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
        httpsOptions.ServerCertificate = new X509Certificate2("certs/server.pfx", "changeit");
    });
});

// Client: HttpClient with mTLS
public static HttpClient CreateMtlsClient()
{
    var clientCert = new X509Certificate2("certs/client.pfx", "changeit");
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(clientCert);
    handler.ServerCertificateCustomValidationCallback = (_, cert, _, _) =>
    {
        // Validate against CA
        var chain = new X509Chain();
        chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
        chain.ChainPolicy.CustomTrustStore.Add(new X509Certificate2("certs/ca.crt"));
        return chain.Build(cert!);
    };
    return new HttpClient(handler);
}

Pros: ปลอดภัยทางการเข้ารหัส การตรวจสอบซึ่งกันและกัน ไม่มีความลับที่ใช้ร่วมกัน Cons: ค่าใช้จ่ายในการจัดการใบรับรอง ไม่มีความยืดหยุ่นสำหรับบุคคลที่สาม การดีบัก ยากขึ้น

JWT Service Tokens

Token-based เหมาะสำหรับ microservices พร้อม central auth server

interface ServiceToken {
  iss: string; // issuer
  sub: string; // subject (requesting service)
  aud: string; // audience (target service)
  iat: number; // issued at
  exp: number; // expiration
  scope: string[]; // permissions
}

function createServiceToken(requestingService: string, targetService: string): string {
  const payload: ServiceToken = {
    iss: 'auth.internal',
    sub: requestingService,
    aud: targetService,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour
    scope: ['read:users', 'write:orders'],
  };
  
  return jwt.sign(payload, process.env.SERVICE_TOKEN_SECRET, {
    algorithm: 'HS256',
  });
}

// Client side
async function callServiceWithJwt(targetService: string, path: string) {
  const token = createServiceToken('user-service', targetService);
  
  return fetch(`https://${targetService}/api/v1${path}`, {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });
}

// Server side verification
app.use('/api/v1/*', (req, res, next) => {
  const auth = req.get('Authorization');
  if (!auth?.startsWith('Bearer ')) {
    return res.status(401).json({
      type: 'https://api.example.com/errors/unauthorized',
      title: 'Unauthorized',
      status: 401,
      detail: 'Missing bearer token',
    });
  }
  
  try {
    const token = jwt.verify(auth.slice(7), process.env.SERVICE_TOKEN_SECRET, {
      algorithms: ['HS256'],
      audience: 'user-service',
    }) as ServiceToken;
    
    req.user = { serviceId: token.sub, scopes: token.scope };
    next();
  } catch (error) {
    res.status(401).json({
      type: 'https://api.example.com/errors/unauthorized',
      title: 'Unauthorized',
      status: 401,
      detail: 'Invalid or expired token',
    });
  }
});
// Spring Boot + io.jsonwebtoken (JJWT)
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;

@Service
public class ServiceTokenService {

    private final SecretKey key = Keys.hmacShaKeyFor(
        System.getenv("SERVICE_TOKEN_SECRET").getBytes(StandardCharsets.UTF_8));

    public String createServiceToken(String requestingService, String targetService) {
        return Jwts.builder()
            .issuer("auth.internal")
            .subject(requestingService)
            .audience().add(targetService).and()
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + 3_600_000)) // 1 hour
            .claim("scope", List.of("read:users", "write:orders"))
            .signWith(key)
            .compact();
    }

    public Claims verifyServiceToken(String token, String expectedAudience) {
        return Jwts.parser()
            .verifyWith(key)
            .requireAudience(expectedAudience)
            .build()
            .parseSignedClaims(token)
            .getPayload();
    }
}

// Client side
public ResponseEntity<?> callServiceWithJwt(String targetService, String path) {
    String token = serviceTokenService.createServiceToken("user-service", targetService);
    return restTemplate.exchange(
        "https://" + targetService + "/api/v1" + path,
        HttpMethod.GET,
        new HttpEntity<>(new HttpHeaders() {{ setBearerAuth(token); }}),
        Object.class
    );
}

// Server-side verification filter
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws ServletException, IOException {
        String auth = request.getHeader("Authorization");
        if (auth == null || !auth.startsWith("Bearer ")) {
            response.sendError(401, "Missing bearer token");
            return;
        }
        try {
            Claims claims = serviceTokenService.verifyServiceToken(
                auth.substring(7), "user-service");
            SecurityContextHolder.getContext().setAuthentication(
                new JwtAuthenticationToken(claims));
            chain.doFilter(request, response);
        } catch (JwtException e) {
            response.sendError(401, "Invalid or expired token");
        }
    }
}
# FastAPI + PyJWT
import jwt as pyjwt
from datetime import datetime, timedelta, timezone
from fastapi import Request, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

SECRET = os.environ["SERVICE_TOKEN_SECRET"]
security = HTTPBearer()

def create_service_token(requesting_service: str, target_service: str) -> str:
    now = datetime.now(timezone.utc)
    payload = {
        "iss": "auth.internal",
        "sub": requesting_service,
        "aud": target_service,
        "iat": now,
        "exp": now + timedelta(hours=1),
        "scope": ["read:users", "write:orders"],
    }
    return pyjwt.encode(payload, SECRET, algorithm="HS256")

async def call_service_with_jwt(target_service: str, path: str):
    import httpx
    token = create_service_token("user-service", target_service)
    async with httpx.AsyncClient() as client:
        return await client.get(
            f"https://{target_service}/api/v1{path}",
            headers={"Authorization": f"Bearer {token}"},
        )

# Server-side verification dependency
def verify_service_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    try:
        payload = pyjwt.decode(
            credentials.credentials,
            SECRET,
            algorithms=["HS256"],
            audience="user-service",
        )
        return {"service_id": payload["sub"], "scopes": payload.get("scope", [])}
    except pyjwt.PyJWTError:
        raise HTTPException(status_code=401, detail="Invalid or expired token")
// ASP.NET Core + System.IdentityModel.Tokens.Jwt
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;

public class ServiceTokenService
{
    private readonly SymmetricSecurityKey _key = new(
        Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SERVICE_TOKEN_SECRET")!));

    public string CreateServiceToken(string requestingService, string targetService)
    {
        var now = DateTime.UtcNow;
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Iss, "auth.internal"),
            new Claim(JwtRegisteredClaimNames.Sub, requestingService),
            new Claim(JwtRegisteredClaimNames.Aud, targetService),
            new Claim("scope", "read:users write:orders"),
        };

        var token = new JwtSecurityToken(
            claims: claims,
            notBefore: now,
            expires: now.AddHours(1),
            signingCredentials: new SigningCredentials(_key, SecurityAlgorithms.HmacSha256)
        );
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

// Client side
public async Task<HttpResponseMessage> CallServiceWithJwtAsync(
    string targetService, string path)
{
    var token = _serviceTokenService.CreateServiceToken("user-service", targetService);
    _httpClient.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", token);
    return await _httpClient.GetAsync($"https://{targetService}/api/v1{path}");
}

// Server-side: configure JWT middleware in Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SERVICE_TOKEN_SECRET")!)),
            ValidateIssuer = true,
            ValidIssuer = "auth.internal",
            ValidateAudience = true,
            ValidAudience = "user-service",
            ValidateLifetime = true,
        };
    });

Pros: Stateless tokens หมดอายุ scoped permissions สามารถรวม claims Cons: ต้องใช้ shared secret หรือ public key infrastructure ความเสี่ยงต่อการรั่วไหล token

Request and Response Patterns

ลวดจำนวน ที่สอดคล้องกันทำให้ APIs สามารถคาดการณ์ได้และบำรุงรักษาง่ายขึ้น

Request Structure

interface ApiRequest<T = any> {
  // Standard headers
  'Content-Type': 'application/json';
  'User-Agent': 'ServiceName/1.0';
  'X-Request-Id': string; // For tracing
  'Authorization': string;
  'Idempotency-Key'?: string; // For safe retries
  
  // Body
  body: T;
}

// Making a request
async function createOrder(userId: string, items: OrderItem[]) {
  const requestId = generateUUID();
  const idempotencyKey = generateUUID();
  
  const response = await fetch('https://orders-service/api/v1/orders', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Request-Id': requestId,
      'Idempotency-Key': idempotencyKey,
      'Authorization': `Bearer ${serviceToken}`,
    },
    body: JSON.stringify({
      userId,
      items,
      metadata: {
        requestId,
        timestamp: new Date().toISOString(),
      },
    }),
  });
  
  return response.json();
}
// Spring Boot WebClient (reactive) for service-to-service calls
import org.springframework.web.reactive.function.client.WebClient;
import java.util.UUID;

@Service
public class OrderClient {

    private final WebClient webClient;
    private final ServiceTokenService tokenService;

    public OrderClient(WebClient.Builder builder, ServiceTokenService tokenService) {
        this.webClient = builder.baseUrl("https://orders-service").build();
        this.tokenService = tokenService;
    }

    public Mono<Order> createOrder(String userId, List<OrderItem> items) {
        String requestId = UUID.randomUUID().toString();
        String idempotencyKey = UUID.randomUUID().toString();
        String token = tokenService.createServiceToken("user-service", "orders-service");

        return webClient.post()
            .uri("/api/v1/orders")
            .header("Content-Type", "application/json")
            .header("X-Request-Id", requestId)
            .header("Idempotency-Key", idempotencyKey)
            .header("Authorization", "Bearer " + token)
            .bodyValue(Map.of(
                "userId", userId,
                "items", items,
                "metadata", Map.of(
                    "requestId", requestId,
                    "timestamp", Instant.now().toString()
                )
            ))
            .retrieve()
            .bodyToMono(Order.class);
    }
}
import httpx
import uuid
from datetime import datetime, timezone

async def create_order(user_id: str, items: list) -> dict:
    request_id = str(uuid.uuid4())
    idempotency_key = str(uuid.uuid4())
    token = create_service_token("user-service", "orders-service")

    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://orders-service/api/v1/orders",
            headers={
                "Content-Type": "application/json",
                "X-Request-Id": request_id,
                "Idempotency-Key": idempotency_key,
                "Authorization": f"Bearer {token}",
            },
            json={
                "user_id": user_id,
                "items": items,
                "metadata": {
                    "request_id": request_id,
                    "timestamp": datetime.now(timezone.utc).isoformat(),
                },
            },
        )
        response.raise_for_status()
        return response.json()
public class OrderClient
{
    private readonly HttpClient _httpClient;
    private readonly ServiceTokenService _tokenService;

    public OrderClient(HttpClient httpClient, ServiceTokenService tokenService)
    {
        _httpClient = httpClient;
        _tokenService = tokenService;
    }

    public async Task<Order?> CreateOrderAsync(string userId, List<OrderItem> items)
    {
        var requestId = Guid.NewGuid().ToString();
        var idempotencyKey = Guid.NewGuid().ToString();
        var token = _tokenService.CreateServiceToken("user-service", "orders-service");

        var request = new HttpRequestMessage(HttpMethod.Post,
            "https://orders-service/api/v1/orders")
        {
            Headers =
            {
                { "X-Request-Id", requestId },
                { "Idempotency-Key", idempotencyKey },
                Authorization = new AuthenticationHeaderValue("Bearer", token),
            },
            Content = JsonContent.Create(new
            {
                userId,
                items,
                metadata = new { requestId, timestamp = DateTime.UtcNow }
            })
        };

        var response = await _httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<Order>();
    }
}

Response Structure

interface ApiResponse<T = any> {
  // 2xx Success
  status: number; // 200, 201, 204
  headers: {
    'Content-Type': 'application/json';
    'Cache-Control': string;
    'X-Request-Id': string; // Echo request ID for tracing
    'X-RateLimit-Limit': string;
    'X-RateLimit-Remaining': string;
    'X-RateLimit-Reset': string;
  };
  body: T;
}

// 2xx response handling
interface Order {
  id: string;
  userId: string;
  items: OrderItem[];
  status: 'pending' | 'processing' | 'shipped' | 'delivered';
  createdAt: string;
  updatedAt: string;
}

const response = await fetch('https://orders-service/api/v1/orders/123', {
  headers: { 'Authorization': `Bearer ${token}` },
});

if (response.ok) {
  const order: Order = await response.json();
  console.log(`Order ${order.id} status: ${order.status}`);
}
// Spring Boot — consume and handle a REST response
public Order fetchOrder(String orderId) {
    ResponseEntity<Order> response = restTemplate.exchange(
        "https://orders-service/api/v1/orders/" + orderId,
        HttpMethod.GET,
        new HttpEntity<>(new HttpHeaders() {{ setBearerAuth(serviceToken); }}),
        Order.class
    );
    if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
        Order order = response.getBody();
        System.out.println("Order " + order.getId() + " status: " + order.getStatus());
        return order;
    }
    throw new RuntimeException("Unexpected status: " + response.getStatusCode());
}
import httpx

async def fetch_order(order_id: str, token: str) -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"https://orders-service/api/v1/orders/{order_id}",
            headers={"Authorization": f"Bearer {token}"},
        )
        if response.is_success:
            order = response.json()
            print(f"Order {order['id']} status: {order['status']}")
            return order
        response.raise_for_status()
public async Task<Order?> FetchOrderAsync(string orderId, string token)
{
    _httpClient.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", token);

    var response = await _httpClient.GetAsync(
        $"https://orders-service/api/v1/orders/{orderId}");

    if (response.IsSuccessStatusCode)
    {
        var order = await response.Content.ReadFromJsonAsync<Order>();
        Console.WriteLine($"Order {order!.Id} status: {order.Status}");
        return order;
    }
    response.EnsureSuccessStatusCode();
    return null;
}

Error Handling and RFC 7807 Problem Details

การตอบสนองข้อผิดพลาดมาตรฐานทำให้การจัดการข้อผิดพลาดของไคลเอนต์สอดคล้องและสามารถคาดการณ์ได้

// RFC 7807 Problem Details for HTTP APIs
interface ProblemDetail {
  type: string; // URI reference to error type documentation
  title: string; // Short, human-readable summary
  status: number; // HTTP status code
  detail?: string; // Human-readable explanation
  instance?: string; // URI of the specific problem occurrence
  [key: string]: any; // Extension properties for specific error types
}

// Error type examples
const ErrorTypes = {
  NOT_FOUND: 'https://api.example.com/errors/not-found',
  VALIDATION_ERROR: 'https://api.example.com/errors/validation-error',
  CONFLICT: 'https://api.example.com/errors/conflict',
  RATE_LIMIT: 'https://api.example.com/errors/rate-limit-exceeded',
  SERVICE_UNAVAILABLE: 'https://api.example.com/errors/service-unavailable',
  INTERNAL_ERROR: 'https://api.example.com/errors/internal-error',
};

function createProblemResponse(status: number, type: string, title: string, detail?: string): ProblemDetail {
  return {
    type,
    title,
    status,
    detail,
    instance: `${new Date().toISOString()}`,
  };
}

// Usage in error handling
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error('Unhandled error:', err);
  
  // Log with request context for debugging
  logger.error({
    error: err.message,
    stack: err.stack,
    requestId: req.get('X-Request-Id'),
    path: req.path,
    method: req.method,
  });
  
  // Return standardized error response
  const problem = createProblemResponse(
    500,
    ErrorTypes.INTERNAL_ERROR,
    'Internal Server Error',
    process.env.NODE_ENV === 'development' ? err.message : 'An unexpected error occurred'
  );
  
  res.status(500).json(problem);
});

// Specific error responses
class ValidationError extends Error {
  constructor(public errors: string[]) {
    super('Validation failed');
  }
}

app.post('/api/v1/orders', (req, res) => {
  try {
    const validation = validateOrderRequest(req.body);
    if (!validation.valid) {
      throw new ValidationError(validation.errors);
    }
    // ... process
  } catch (err) {
    if (err instanceof ValidationError) {
      return res.status(400).json({
        type: ErrorTypes.VALIDATION_ERROR,
        title: 'Validation Failed',
        status: 400,
        detail: 'Request validation failed',
        errors: err.errors,
        instance: req.path,
      });
    }
    next(err);
  }
});
// Spring Boot — RFC 7807 via ProblemDetail (Spring 6+)
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ProblemDetail> handleGeneral(
            Exception ex, HttpServletRequest request) {
        log.error("Unhandled error: path={}, method={}",
            request.getRequestURI(), request.getMethod(), ex);

        ProblemDetail problem = ProblemDetail.forStatus(500);
        problem.setType(URI.create("https://api.example.com/errors/internal-error"));
        problem.setTitle("Internal Server Error");
        String env = System.getenv("SPRING_PROFILES_ACTIVE");
        problem.setDetail("development".equals(env) ? ex.getMessage()
            : "An unexpected error occurred");
        problem.setInstance(URI.create(request.getRequestURI()));
        return ResponseEntity.status(500).body(problem);
    }

    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ProblemDetail> handleValidation(
            ValidationException ex, HttpServletRequest request) {
        ProblemDetail problem = ProblemDetail.forStatus(400);
        problem.setType(URI.create("https://api.example.com/errors/validation-error"));
        problem.setTitle("Validation Failed");
        problem.setDetail("Request validation failed");
        problem.setProperty("errors", ex.getErrors());
        problem.setInstance(URI.create(request.getRequestURI()));
        return ResponseEntity.status(400).body(problem);
    }
}

// Usage in a controller
@PostMapping("/api/v1/orders")
public ResponseEntity<?> createOrder(@RequestBody CreateOrderRequest body,
                                     HttpServletRequest request) {
    ValidationResult validation = validateOrderRequest(body);
    if (!validation.isValid()) {
        throw new ValidationException(validation.getErrors());
    }
    // ... process
    return ResponseEntity.status(201).body(newOrder);
}
# FastAPI — RFC 7807 problem details via exception handlers
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
import logging

logger = logging.getLogger(__name__)
app = FastAPI()

class ValidationError(Exception):
    def __init__(self, errors: list[str]):
        self.errors = errors

@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
    logger.error("Unhandled error: path=%s, method=%s", request.url.path, request.method, exc_info=exc)
    import os
    detail = str(exc) if os.getenv("ENV") == "development" else "An unexpected error occurred"
    return JSONResponse(status_code=500, content={
        "type": "https://api.example.com/errors/internal-error",
        "title": "Internal Server Error",
        "status": 500,
        "detail": detail,
        "instance": str(request.url.path),
    })

@app.exception_handler(ValidationError)
async def validation_exception_handler(request: Request, exc: ValidationError):
    return JSONResponse(status_code=400, content={
        "type": "https://api.example.com/errors/validation-error",
        "title": "Validation Failed",
        "status": 400,
        "detail": "Request validation failed",
        "errors": exc.errors,
        "instance": str(request.url.path),
    })

@app.post("/api/v1/orders")
async def create_order(body: CreateOrderRequest, request: Request):
    validation = validate_order_request(body)
    if not validation.valid:
        raise ValidationError(validation.errors)
    # ... process
// ASP.NET Core — RFC 7807 via ProblemDetails middleware (.NET 7+)
// Program.cs
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

// GlobalExceptionHandler.cs
public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;
    private readonly IHostEnvironment _env;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger, IHostEnvironment env)
    {
        _logger = logger;
        _env = env;
    }

    public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception,
        CancellationToken ct)
    {
        _logger.LogError(exception, "Unhandled error: {Path} {Method}",
            context.Request.Path, context.Request.Method);

        var problem = new ProblemDetails
        {
            Type = "https://api.example.com/errors/internal-error",
            Title = "Internal Server Error",
            Status = 500,
            Detail = _env.IsDevelopment() ? exception.Message : "An unexpected error occurred",
            Instance = context.Request.Path
        };

        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(problem, ct);
        return true;
    }
}

// Validation error in controller
[HttpPost("api/v1/orders")]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest body)
{
    var validation = ValidateOrderRequest(body);
    if (!validation.Valid)
    {
        return ValidationProblem(new ValidationProblemDetails(
            validation.Errors.ToDictionary(e => "error", e => new[] { e }))
        {
            Type = "https://api.example.com/errors/validation-error",
            Title = "Validation Failed",
            Status = 400,
            Detail = "Request validation failed",
            Instance = Request.Path
        });
    }
    // ... process
    return StatusCode(201, newOrder);
}

Retry Logic with Exponential Backoff and Jitter

ความล้มเหลวของเครือข่ายหลีกเลี่ยงไม่ได้ นำ intelligent retries ไปใช้เพื่อจัดการความล้มเหลวชั่วคราวอย่างสง่างาม

interface RetryConfig {
  maxAttempts: number;
  initialDelayMs: number;
  maxDelayMs: number;
  backoffMultiplier: number;
}

const DEFAULT_RETRY_CONFIG: RetryConfig = {
  maxAttempts: 3,
  initialDelayMs: 100,
  maxDelayMs: 10000,
  backoffMultiplier: 2,
};

// Idempotent status codes that are safe to retry
const IDEMPOTENT_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);

// Methods that are safe to retry
const IDEMPOTENT_METHODS = new Set(['GET', 'PUT', 'DELETE', 'HEAD', 'OPTIONS']);

async function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function calculateBackoffDelay(attempt: number, config: RetryConfig): number {
  // Exponential backoff with jitter to avoid thundering herd
  const exponentialDelay = Math.min(
    config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt),
    config.maxDelayMs
  );
  
  // Add random jitter (±25% of exponential delay)
  const jitter = exponentialDelay * 0.5 * Math.random();
  return exponentialDelay + jitter;
}

async function fetchWithRetry(
  url: string,
  options: RequestInit = {},
  config: RetryConfig = DEFAULT_RETRY_CONFIG
): Promise<Response> {
  let lastError: Error | null = null;
  
  for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
    try {
      const response = await fetch(url, {
        ...options,
        // Add timeout to prevent hanging requests
        signal: AbortSignal.timeout(30000),
      });
      
      // Check if response is retryable
      if (IDEMPOTENT_STATUS_CODES.has(response.status) && 
          IDEMPOTENT_METHODS.has((options.method || 'GET').toUpperCase())) {
        if (attempt < config.maxAttempts - 1) {
          const delay = calculateBackoffDelay(attempt, config);
          console.warn(
            `Request failed with ${response.status}. Retrying after ${delay}ms (attempt ${attempt + 1}/${config.maxAttempts})`
          );
          await sleep(delay);
          continue;
        }
      }
      
      // Success or non-retryable error
      return response;
    } catch (error) {
      lastError = error as Error;
      
      // Only retry on network errors and timeouts
      const isNetworkError = error instanceof TypeError || 
                            (error instanceof DOMException && error.name === 'AbortError');
      
      if (isNetworkError && attempt < config.maxAttempts - 1) {
        const delay = calculateBackoffDelay(attempt, config);
        console.warn(
          `Network error: ${(error as Error).message}. Retrying after ${delay}ms (attempt ${attempt + 1}/${config.maxAttempts})`
        );
        await sleep(delay);
        continue;
      }
      
      throw error;
    }
  }
  
  throw lastError || new Error('Max retry attempts exceeded');
}

// Usage
async function createOrderWithRetry(userId: string, items: OrderItem[]) {
  return fetchWithRetry(
    'https://orders-service/api/v1/orders',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Idempotency-Key': generateUUID(),
        'Authorization': `Bearer ${serviceToken}`,
      },
      body: JSON.stringify({ userId, items }),
    },
    {
      maxAttempts: 3,
      initialDelayMs: 100,
      maxDelayMs: 5000,
      backoffMultiplier: 2,
    }
  );
}
// Spring Boot + Resilience4j retry
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import java.time.Duration;
import java.util.Set;

@Service
public class RetryableOrderClient {

    private static final Set<Integer> RETRYABLE_STATUSES = Set.of(408, 429, 500, 502, 503, 504);
    private static final Set<String> IDEMPOTENT_METHODS = Set.of("GET", "PUT", "DELETE", "HEAD");

    private final RestTemplate restTemplate;

    private final Retry retry = Retry.of("orderService", RetryConfig.custom()
        .maxAttempts(3)
        .waitDuration(Duration.ofMillis(100))
        .intervalFunction(attempt -> {
            // Exponential backoff with jitter
            long base = (long)(100 * Math.pow(2, attempt - 1));
            long capped = Math.min(base, 5000);
            long jitter = (long)(capped * 0.5 * Math.random());
            return capped + jitter;
        })
        .retryOnException(ex -> ex instanceof IOException || ex instanceof TimeoutException)
        .build());

    public Order createOrderWithRetry(String userId, List<OrderItem> items) {
        return Retry.decorateSupplier(retry, () -> {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.set("Idempotency-Key", UUID.randomUUID().toString());
            headers.setBearerAuth(serviceToken);

            ResponseEntity<Order> response = restTemplate.exchange(
                "https://orders-service/api/v1/orders",
                HttpMethod.POST,
                new HttpEntity<>(Map.of("userId", userId, "items", items), headers),
                Order.class
            );
            return response.getBody();
        }).get();
    }
}
# FastAPI client + tenacity retry library
import asyncio
import random
import httpx
from tenacity import (
    retry, stop_after_attempt, wait_exponential_jitter,
    retry_if_exception_type, retry_if_result,
)

RETRYABLE_STATUSES = {408, 429, 500, 502, 503, 504}
IDEMPOTENT_METHODS = {"GET", "PUT", "DELETE", "HEAD"}

def is_retryable_response(response: httpx.Response) -> bool:
    return response.status_code in RETRYABLE_STATUSES

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential_jitter(initial=0.1, max=5.0, jitter=0.5),
    retry=(
        retry_if_exception_type((httpx.NetworkError, httpx.TimeoutException))
        | retry_if_result(is_retryable_response)
    ),
)
async def fetch_with_retry(
    method: str,
    url: str,
    **kwargs,
) -> httpx.Response:
    async with httpx.AsyncClient(timeout=30.0) as client:
        return await client.request(method, url, **kwargs)

# Usage
async def create_order_with_retry(user_id: str, items: list) -> dict:
    response = await fetch_with_retry(
        "POST",
        "https://orders-service/api/v1/orders",
        headers={
            "Content-Type": "application/json",
            "Idempotency-Key": str(uuid.uuid4()),
            "Authorization": f"Bearer {service_token}",
        },
        json={"user_id": user_id, "items": items},
    )
    response.raise_for_status()
    return response.json()
// ASP.NET Core + Polly retry
using Polly;
using Polly.Extensions.Http;
using System.Net;

// Program.cs — register HttpClient with Polly retry
builder.Services.AddHttpClient<OrderClient>()
    .AddPolicyHandler(GetRetryPolicy());

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    var retryableStatuses = new[] {
        HttpStatusCode.RequestTimeout,
        HttpStatusCode.TooManyRequests,
        HttpStatusCode.InternalServerError,
        HttpStatusCode.BadGateway,
        HttpStatusCode.ServiceUnavailable,
        HttpStatusCode.GatewayTimeout,
    };

    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(msg => retryableStatuses.Contains(msg.StatusCode))
        .WaitAndRetryAsync(
            retryCount: 3,
            sleepDurationProvider: (attempt, response, context) =>
            {
                // Exponential backoff with jitter
                double baseMs = 100 * Math.Pow(2, attempt - 1);
                double capped = Math.Min(baseMs, 5000);
                double jitter = capped * 0.5 * Random.Shared.NextDouble();
                return TimeSpan.FromMilliseconds(capped + jitter);
            },
            onRetryAsync: async (outcome, timespan, attempt, context) =>
            {
                Console.WriteLine($"Retry {attempt} after {timespan.TotalMilliseconds}ms");
                await Task.CompletedTask;
            }
        );
}

// Usage in OrderClient
public async Task<Order?> CreateOrderWithRetryAsync(string userId, List<OrderItem> items)
{
    var request = new HttpRequestMessage(HttpMethod.Post,
        "https://orders-service/api/v1/orders")
    {
        Headers = { { "Idempotency-Key", Guid.NewGuid().ToString() } },
        Content = JsonContent.Create(new { userId, items })
    };
    var response = await _httpClient.SendAsync(request);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadFromJsonAsync<Order>();
}

Circuit Breaker Pattern

ป้องกันความล้มเหลวแบบเป็นกำลัง โดยหยุดการส่งคำขอไปยังบริการที่ล้มเหลว และจะค่อยๆ ฟื้นตัว

type CircuitState = 'closed' | 'open' | 'half-open';

interface CircuitBreakerConfig {
  failureThreshold: number; // requests before opening
  successThreshold: number; // successes before closing (in half-open)
  timeout: number; // ms to wait before attempting half-open
}

interface CircuitBreakerMetrics {
  failures: number;
  successes: number;
  lastFailureTime?: number;
  totalRequests: number;
}

class CircuitBreaker {
  private state: CircuitState = 'closed';
  private metrics: CircuitBreakerMetrics = {
    failures: 0,
    successes: 0,
    totalRequests: 0,
  };
  private lastStateChangeTime = Date.now();

  constructor(private config: CircuitBreakerConfig) {}

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (this.shouldAttemptReset()) {
        this.state = 'half-open';
        this.metrics.successes = 0;
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess(): void {
    this.metrics.successes++;
    this.metrics.totalRequests++;

    if (this.state === 'half-open' && this.metrics.successes >= this.config.successThreshold) {
      this.state = 'closed';
      this.resetMetrics();
    }
  }

  private onFailure(): void {
    this.metrics.failures++;
    this.metrics.totalRequests++;
    this.metrics.lastFailureTime = Date.now();

    if (this.metrics.failures >= this.config.failureThreshold) {
      this.state = 'open';
      this.lastStateChangeTime = Date.now();
    }
  }

  private shouldAttemptReset(): boolean {
    const timeSinceLastChange = Date.now() - this.lastStateChangeTime;
    return timeSinceLastChange >= this.config.timeout;
  }

  private resetMetrics(): void {
    this.metrics = {
      failures: 0,
      successes: 0,
      totalRequests: 0,
    };
  }

  getState(): CircuitState {
    return this.state;
  }

  getMetrics(): CircuitBreakerMetrics {
    return { ...this.metrics };
  }
}

// Usage
const orderServiceBreaker = new CircuitBreaker({
  failureThreshold: 5,
  successThreshold: 2,
  timeout: 60000, // 1 minute before half-open attempt
});

async function createOrderWithCircuitBreaker(userId: string, items: OrderItem[]) {
  return orderServiceBreaker.execute(() =>
    fetchWithRetry('https://orders-service/api/v1/orders', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${serviceToken}`,
      },
      body: JSON.stringify({ userId, items }),
    })
  );
}

// Monitor circuit breaker health
setInterval(() => {
  const state = orderServiceBreaker.getState();
  const metrics = orderServiceBreaker.getMetrics();
  
  if (state !== 'closed') {
    console.warn(`Order service circuit breaker: ${state}`, metrics);
  }
}, 30000);
// Spring Boot + Resilience4j CircuitBreaker
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import java.time.Duration;

@Service
public class CircuitBreakerOrderClient {

    private final CircuitBreaker circuitBreaker;
    private final RestTemplate restTemplate;

    public CircuitBreakerOrderClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;

        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .failureRateThreshold(50)            // open after 50% failure rate
            .minimumNumberOfCalls(5)             // minimum calls before evaluating
            .waitDurationInOpenState(Duration.ofSeconds(60))
            .permittedNumberOfCallsInHalfOpenState(2)
            .slidingWindowSize(10)
            .build();

        this.circuitBreaker = CircuitBreakerRegistry.of(config)
            .circuitBreaker("orderService");

        // Monitor state changes
        circuitBreaker.getEventPublisher()
            .onStateTransition(event ->
                System.out.println("Circuit breaker state: " + event.getStateTransition()));
    }

    public Order createOrderWithCircuitBreaker(String userId, List<OrderItem> items) {
        return circuitBreaker.executeSupplier(() -> {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.setBearerAuth(serviceToken);

            return restTemplate.postForObject(
                "https://orders-service/api/v1/orders",
                new HttpEntity<>(Map.of("userId", userId, "items", items), headers),
                Order.class
            );
        });
    }
}
# FastAPI + pybreaker circuit breaker
import pybreaker
import httpx

order_breaker = pybreaker.CircuitBreaker(
    fail_max=5,
    reset_timeout=60,
    listeners=[pybreaker.CircuitBreakerListener()],
)

@order_breaker
async def _call_order_service(user_id: str, items: list) -> dict:
    async with httpx.AsyncClient(timeout=30.0) as client:
        response = await client.post(
            "https://orders-service/api/v1/orders",
            headers={"Authorization": f"Bearer {service_token}"},
            json={"user_id": user_id, "items": items},
        )
        response.raise_for_status()
        return response.json()

async def create_order_with_circuit_breaker(user_id: str, items: list) -> dict:
    try:
        return await _call_order_service(user_id, items)
    except pybreaker.CircuitBreakerError:
        raise RuntimeError("Circuit breaker is OPEN — order service unavailable")
// ASP.NET Core + Polly CircuitBreaker
using Polly;
using Polly.Extensions.Http;

// Program.cs — add circuit breaker to HttpClient
builder.Services.AddHttpClient<OrderClient>()
    .AddPolicyHandler(GetCircuitBreakerPolicy());

static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(
            handledEventsAllowedBeforeBreaking: 5,
            durationOfBreak: TimeSpan.FromSeconds(60),
            onBreak: (outcome, duration) =>
                Console.Warn($"Circuit breaker opened for {duration.TotalSeconds}s"),
            onReset: () =>
                Console.Info("Circuit breaker reset (closed)"),
            onHalfOpen: () =>
                Console.Info("Circuit breaker half-open — probing")
        );
}

// OrderClient usage — Polly middleware handles the circuit breaker automatically
public async Task<Order?> CreateOrderWithCircuitBreakerAsync(
    string userId, List<OrderItem> items)
{
    // If circuit is open, Polly throws BrokenCircuitException automatically
    var response = await _httpClient.PostAsJsonAsync(
        "https://orders-service/api/v1/orders",
        new { userId, items });
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadFromJsonAsync<Order>();
}

Rate Limiting

ป้องกันบริการของคุณจากการโหลดเกิน และบังคับใช้นโยบายการใช้งานที่ยุติธรรม

interface RateLimitConfig {
  maxRequests: number;
  windowSizeMs: number;
  keyExtractor: (req: Request) => string;
}

interface RateLimitMetrics {
  requests: number[];
  lastCleanup: number;
}

class SlidingWindowRateLimiter {
  private buckets = new Map<string, RateLimitMetrics>();

  constructor(private config: RateLimitConfig) {}

  private cleanOldRequests(metrics: RateLimitMetrics, now: number): void {
    // Remove requests outside the current window
    metrics.requests = metrics.requests.filter((timestamp) => now - timestamp < this.config.windowSizeMs);
    metrics.lastCleanup = now;
  }

  isAllowed(key: string): boolean {
    const now = Date.now();
    let metrics = this.buckets.get(key);

    if (!metrics) {
      metrics = { requests: [], lastCleanup: now };
      this.buckets.set(key, metrics);
    }

    // Clean old requests
    if (now - metrics.lastCleanup > this.config.windowSizeMs) {
      this.cleanOldRequests(metrics, now);
    }

    // Check if allowed
    if (metrics.requests.length < this.config.maxRequests) {
      metrics.requests.push(now);
      return true;
    }

    return false;
  }

  getRemainingRequests(key: string): number {
    const metrics = this.buckets.get(key);
    if (!metrics) return this.config.maxRequests;
    
    this.cleanOldRequests(metrics, Date.now());
    return Math.max(0, this.config.maxRequests - metrics.requests.length);
  }

  getResetTime(key: string): number {
    const metrics = this.buckets.get(key);
    if (!metrics || metrics.requests.length === 0) return Date.now();
    
    const oldestRequest = metrics.requests[0];
    return oldestRequest + this.config.windowSizeMs;
  }
}

// Create limiter: 1000 requests per minute per service
const rateLimiter = new SlidingWindowRateLimiter({
  maxRequests: 1000,
  windowSizeMs: 60000,
  keyExtractor: (req) => req.get('X-Service-Id') || req.ip || 'anonymous',
});

// Middleware
app.use('/api/v1/*', (req, res, next) => {
  const key = rateLimiter['config'].keyExtractor(req);
  const remaining = rateLimiter.getRemainingRequests(key);
  const resetTime = rateLimiter.getResetTime(key);

  // Set rate limit headers
  res.set('X-RateLimit-Limit', '1000');
  res.set('X-RateLimit-Remaining', String(remaining));
  res.set('X-RateLimit-Reset', String(Math.ceil(resetTime / 1000)));

  if (!rateLimiter.isAllowed(key)) {
    return res.status(429).json({
      type: 'https://api.example.com/errors/rate-limit-exceeded',
      title: 'Too Many Requests',
      status: 429,
      detail: `Rate limit exceeded. Retry after ${Math.ceil((resetTime - Date.now()) / 1000)} seconds`,
      retryAfter: Math.ceil((resetTime - Date.now()) / 1000),
    });
  }

  next();
});
// Spring Boot + Bucket4j sliding-window rate limiter
import io.github.bucket4j.*;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class RateLimitFilter extends OncePerRequestFilter {

    private final ConcurrentHashMap<String, Bucket> buckets = new ConcurrentHashMap<>();
    private static final int MAX_REQUESTS = 1000;
    private static final Duration WINDOW = Duration.ofMinutes(1);

    private Bucket getBucketForKey(String key) {
        return buckets.computeIfAbsent(key, k ->
            Bucket.builder()
                .addLimit(Bandwidth.classic(MAX_REQUESTS, Refill.greedy(MAX_REQUESTS, WINDOW)))
                .build()
        );
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws ServletException, IOException {
        if (!request.getRequestURI().startsWith("/api/v1/")) {
            chain.doFilter(request, response); return;
        }

        String serviceId = request.getHeader("X-Service-Id");
        String key = serviceId != null ? serviceId : request.getRemoteAddr();

        Bucket bucket = getBucketForKey(key);
        long remaining = bucket.getAvailableTokens();
        long resetEpoch = Instant.now().plus(WINDOW).getEpochSecond();

        response.setHeader("X-RateLimit-Limit", String.valueOf(MAX_REQUESTS));
        response.setHeader("X-RateLimit-Remaining", String.valueOf(remaining));
        response.setHeader("X-RateLimit-Reset", String.valueOf(resetEpoch));

        if (!bucket.tryConsume(1)) {
            response.setStatus(429);
            response.setContentType("application/json");
            long retryAfter = WINDOW.getSeconds();
            response.getWriter().write(String.format(
                "{\"type\":\"https://api.example.com/errors/rate-limit-exceeded\"," +
                "\"title\":\"Too Many Requests\",\"status\":429," +
                "\"detail\":\"Rate limit exceeded. Retry after %d seconds\"," +
                "\"retryAfter\":%d}", retryAfter, retryAfter));
            return;
        }
        chain.doFilter(request, response);
    }
}
# FastAPI + slowapi sliding-window rate limiter
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

limiter = Limiter(key_func=lambda request: (
    request.headers.get("X-Service-Id") or get_remote_address(request)
))
app = FastAPI()
app.state.limiter = limiter

@app.exception_handler(RateLimitExceeded)
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
    return JSONResponse(status_code=429, content={
        "type": "https://api.example.com/errors/rate-limit-exceeded",
        "title": "Too Many Requests",
        "status": 429,
        "detail": f"Rate limit exceeded. {exc.detail}",
    })

@app.get("/api/v1/orders/{order_id}")
@limiter.limit("1000/minute")
async def get_order(request: Request, order_id: str):
    return order_service.get_order(order_id)
// ASP.NET Core .NET 7+ built-in rate limiting
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;

// Program.cs
builder.Services.AddRateLimiter(options =>
{
    options.AddSlidingWindowLimiter("ApiPolicy", limiterOptions =>
    {
        limiterOptions.PermitLimit = 1000;
        limiterOptions.Window = TimeSpan.FromMinutes(1);
        limiterOptions.SegmentsPerWindow = 6; // 10-second segments
        limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        limiterOptions.QueueLimit = 0;
    });

    options.OnRejected = async (context, token) =>
    {
        context.HttpContext.Response.StatusCode = 429;
        context.HttpContext.Response.ContentType = "application/json";

        var retryAfter = context.Lease.TryGetMetadata(
            MetadataName.RetryAfter, out var retryAfterVal)
            ? (int)retryAfterVal.TotalSeconds : 60;

        context.HttpContext.Response.Headers["X-RateLimit-Limit"] = "1000";
        context.HttpContext.Response.Headers["X-RateLimit-Remaining"] = "0";
        context.HttpContext.Response.Headers["Retry-After"] = retryAfter.ToString();

        await context.HttpContext.Response.WriteAsJsonAsync(new
        {
            type = "https://api.example.com/errors/rate-limit-exceeded",
            title = "Too Many Requests",
            status = 429,
            detail = $"Rate limit exceeded. Retry after {retryAfter} seconds",
            retryAfter
        }, token);
    };
});

app.UseRateLimiter();

// Apply to controller
[EnableRateLimiting("ApiPolicy")]
[ApiController]
[Route("api/v1")]
public class OrdersController : ControllerBase { }

OpenAPI/Swagger Schema-First Development

กำหนด API contract ของคุณก่อน implementation นี่ช่วยให้สร้างโค้ดไคลเอนต์ เอกสาร และการตรวจสอบ

# openapi.yaml
openapi: 3.0.0
info:
  title: Order Service API
  version: 1.0.0
  description: Internal API for order management

servers:
  - url: https://orders-service/api/v1
    description: Production

paths:
  /orders:
    post:
      summary: Create a new order
      operationId: createOrder
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - userId
                - items
              properties:
                userId:
                  type: string
                  format: uuid
                items:
                  type: array
                  minItems: 1
                  items:
                    $ref: '#/components/schemas/OrderItem'
      responses:
        '201':
          description: Order created
          headers:
            Location:
              schema:
                type: string
                format: uri
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /orders/{orderId}:
    get:
      summary: Retrieve an order
      operationId: getOrder
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Order found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '404':
          $ref: '#/components/responses/NotFound'

components:
  schemas:
    Order:
      type: object
      required:
        - id
        - userId
        - items
        - status
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderItem'
        status:
          type: string
          enum: [pending, processing, shipped, delivered]
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    OrderItem:
      type: object
      required:
        - productId
        - quantity
        - price
      properties:
        productId:
          type: string
          format: uuid
        quantity:
          type: integer
          minimum: 1
        price:
          type: number
          format: double
          minimum: 0

  responses:
    ValidationError:
      description: Request validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ProblemDetail'

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ProblemDetail'

    Unauthorized:
      description: Authentication required or failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ProblemDetail'

    RateLimited:
      description: Too many requests
      headers:
        X-RateLimit-Limit:
          schema:
            type: integer
        X-RateLimit-Remaining:
          schema:
            type: integer
        X-RateLimit-Reset:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ProblemDetail'

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

Pagination Patterns

จัดการชุดผลลัพธ์ขนาดใหญ่อย่างมีประสิทธิภาพโดยไม่โหลดคอลเลกชันทั้งหมดลงในหน่วยความจำ

มีประสิทธิภาพมากขึ้น เสถียรเมื่อข้อมูลเปลี่ยนแปลงในระหว่าง pagination

interface CursorPaginationParams {
  limit: number; // 1-100, default 20
  cursor?: string; // opaque token
}

interface CursorPage<T> {
  items: T[];
  nextCursor?: string; // null if last page
  hasMore: boolean;
}

function createCursor(id: string, timestamp: number): string {
  const data = `${id}:${timestamp}`;
  return Buffer.from(data).toString('base64');
}

function decodeCursor(cursor: string): { id: string; timestamp: number } {
  const data = Buffer.from(cursor, 'base64').toString();
  const [id, timestamp] = data.split(':');
  return { id, timestamp: parseInt(timestamp) };
}

app.get('/api/v1/orders', async (req, res) => {
  const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
  const cursor = req.query.cursor as string | undefined;

  let query = db.orders.orderBy('createdAt', 'desc');

  if (cursor) {
    const { timestamp } = decodeCursor(cursor);
    query = query.where('createdAt', '<', new Date(timestamp));
  }

  // Fetch limit + 1 to determine if there are more results
  const orders = await query.limit(limit + 1).toArray();

  const hasMore = orders.length > limit;
  const items = orders.slice(0, limit);

  const response: CursorPage<Order> = {
    items,
    hasMore,
    nextCursor: hasMore
      ? createCursor(
          items[items.length - 1].id,
          items[items.length - 1].createdAt.getTime()
        )
      : undefined,
  };

  res.json(response);
});
@GetMapping("/api/v1/orders")
public ResponseEntity<CursorPage<Order>> getOrders(
        @RequestParam(defaultValue = "20") int limit,
        @RequestParam(required = false) String cursor) {

    limit = Math.min(limit, 100);
    Instant cursorTimestamp = null;

    if (cursor != null) {
        String decoded = new String(Base64.getDecoder().decode(cursor));
        String[] parts = decoded.split(":");
        cursorTimestamp = Instant.ofEpochMilli(Long.parseLong(parts[1]));
    }

    List<Order> orders;
    if (cursorTimestamp != null) {
        orders = orderRepo.findByCreatedAtBeforeOrderByCreatedAtDesc(
            cursorTimestamp, PageRequest.of(0, limit + 1));
    } else {
        orders = orderRepo.findAllByOrderByCreatedAtDesc(PageRequest.of(0, limit + 1));
    }

    boolean hasMore = orders.size() > limit;
    List<Order> items = orders.stream().limit(limit).toList();

    String nextCursor = null;
    if (hasMore) {
        Order last = items.get(items.size() - 1);
        String cursorData = last.getId() + ":" + last.getCreatedAt().toEpochMilli();
        nextCursor = Base64.getEncoder().encodeToString(cursorData.getBytes());
    }

    return ResponseEntity.ok(new CursorPage<>(items, nextCursor, hasMore));
}
from fastapi import APIRouter, Query
from typing import Optional
import base64

router = APIRouter()

@router.get("/api/v1/orders")
async def get_orders(
    limit: int = Query(default=20, ge=1, le=100),
    cursor: Optional[str] = None,
    db: Session = Depends(get_db),
):
    cursor_timestamp = None
    if cursor:
        decoded = base64.b64decode(cursor).decode()
        _, ts = decoded.split(":", 1)
        cursor_timestamp = datetime.fromtimestamp(int(ts) / 1000, tz=timezone.utc)

    query = db.query(Order).order_by(Order.created_at.desc())
    if cursor_timestamp:
        query = query.filter(Order.created_at < cursor_timestamp)

    # Fetch limit + 1 to check for more
    orders = query.limit(limit + 1).all()

    has_more = len(orders) > limit
    items = orders[:limit]

    next_cursor = None
    if has_more:
        last = items[-1]
        cursor_data = f"{last.id}:{int(last.created_at.timestamp() * 1000)}"
        next_cursor = base64.b64encode(cursor_data.encode()).decode()

    return {"items": items, "next_cursor": next_cursor, "has_more": has_more}
[HttpGet("api/v1/orders")]
public async Task<IActionResult> GetOrders(
    [FromQuery] int limit = 20,
    [FromQuery] string? cursor = null)
{
    limit = Math.Min(limit, 100);
    DateTime? cursorTimestamp = null;

    if (cursor != null)
    {
        var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(cursor));
        var parts = decoded.Split(':', 2);
        cursorTimestamp = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(parts[1])).UtcDateTime;
    }

    var query = _db.Orders.OrderByDescending(o => o.CreatedAt).AsQueryable();
    if (cursorTimestamp.HasValue)
        query = query.Where(o => o.CreatedAt < cursorTimestamp.Value);

    // Fetch limit + 1 to check for more
    var orders = await query.Take(limit + 1).ToListAsync();

    bool hasMore = orders.Count > limit;
    var items = orders.Take(limit).ToList();

    string? nextCursor = null;
    if (hasMore)
    {
        var last = items[^1];
        var cursorData = $"{last.Id}:{new DateTimeOffset(last.CreatedAt).ToUnixTimeMilliseconds()}";
        nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(cursorData));
    }

    return Ok(new { items, nextCursor, hasMore });
}

Offset-Based Pagination (Simple but Limited)

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

interface OffsetPaginationParams {
  limit: number; // 1-100, default 20
  offset: number; // 0-based
}

interface OffsetPage<T> {
  items: T[];
  total: number;
  limit: number;
  offset: number;
  hasMore: boolean;
}

app.get('/api/v1/users', async (req, res) => {
  const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
  const offset = Math.max(parseInt(req.query.offset as string) || 0, 0);

  const [items, total] = await Promise.all([
    db.users.orderBy('createdAt', 'desc').offset(offset).limit(limit).toArray(),
    db.users.count(),
  ]);

  const response: OffsetPage<User> = {
    items,
    total,
    limit,
    offset,
    hasMore: offset + limit < total,
  };

  res.json(response);
});
@GetMapping("/api/v1/users")
public ResponseEntity<Map<String, Object>> getUsers(
        @RequestParam(defaultValue = "20") int limit,
        @RequestParam(defaultValue = "0") int offset) {

    limit = Math.min(limit, 100);
    offset = Math.max(offset, 0);

    Pageable pageable = PageRequest.of(offset / limit, limit,
        Sort.by(Sort.Direction.DESC, "createdAt"));
    Page<User> page = userRepo.findAll(pageable);

    return ResponseEntity.ok(Map.of(
        "items", page.getContent(),
        "total", page.getTotalElements(),
        "limit", limit,
        "offset", offset,
        "hasMore", offset + limit < page.getTotalElements()
    ));
}
from fastapi import APIRouter, Query, Depends
from sqlalchemy.orm import Session

router = APIRouter()

@router.get("/api/v1/users")
async def get_users(
    limit: int = Query(default=20, ge=1, le=100),
    offset: int = Query(default=0, ge=0),
    db: Session = Depends(get_db),
):
    total = db.query(User).count()
    items = (
        db.query(User)
        .order_by(User.created_at.desc())
        .offset(offset)
        .limit(limit)
        .all()
    )
    return {
        "items": items,
        "total": total,
        "limit": limit,
        "offset": offset,
        "has_more": offset + limit < total,
    }
[HttpGet("api/v1/users")]
public async Task<IActionResult> GetUsers(
    [FromQuery] int limit = 20,
    [FromQuery] int offset = 0)
{
    limit = Math.Min(limit, 100);
    offset = Math.Max(offset, 0);

    var total = await _db.Users.CountAsync();
    var items = await _db.Users
        .OrderByDescending(u => u.CreatedAt)
        .Skip(offset)
        .Take(limit)
        .ToListAsync();

    return Ok(new
    {
        items,
        total,
        limit,
        offset,
        hasMore = offset + limit < total
    });
}

Production Checklist

ก่อน deploy REST API ของคุณไปยัง production:

Testing & Quality

  • Unit tests สำหรับ handlers ทั้งหมด (>80% coverage)
  • Integration tests สำหรับ critical paths (auth, payments, ฯลฯ)
  • Load testing เพื่อตรวจสอบ performance ภายใต้ stress
  • Chaos engineering tests สำหรับ failure scenarios
  • Security audit ของ authentication และ data handling
  • API contract tests ตาม OpenAPI schema

Observability

  • Structured logging พร้อม request IDs และ correlation IDs
  • Request/response time metrics ต่อ endpoint
  • Error rate monitoring และ alerting
  • Circuit breaker state changes logged
  • Rate limit violations tracked
  • Authentication failure logs สำหรับ investigation

Security

  • HTTPS/TLS สำหรับ endpoints ทั้งหมด
  • Request validation ตาม schema
  • SQL injection prevention (ใช้ parameterized queries)
  • Rate limiting บังคับใช้ต่อ service/client
  • Authentication verification ในทุก endpoint
  • Sensitive data ไม่ logged หรือ exposed ใน errors
  • CORS configured อย่างเหมาะสม
  • Input size limits บังคับใช้

Performance

  • Response time SLO กำหนด และ monitored (p99 < 500ms)
  • Database query optimization (indexes, explain plans)
  • Connection pooling configured
  • Caching strategy สำหรับ read-heavy endpoints
  • Pagination บังคับใช้สำหรับ list endpoints
  • Unused query parameters ignored safely

Reliability

  • Graceful shutdown handling (หยุดการยอมรับคำขอใหม่ finish in-flight)
  • Health check endpoint ที่ /health หรือ /healthz
  • Circuit breakers configured สำหรับ external service calls
  • Retry logic พร้อม backoff สำหรับ transient failures
  • Deadletter queue สำหรับ failed async operations
  • Database connection failover configured

Documentation

  • OpenAPI schema updated และ published
  • Runbook สำหรับ common operational tasks
  • Onboarding guide สำหรับ new service consumers
  • Architecture diagram ของ service dependencies
  • Deprecation timeline สำหรับ API versions
  • SLA และ rate limit documentation

Deployment

  • Feature flags สำหรับ gradual rollout
  • Canary deployment เพื่อ catch issues early
  • Rollback plan และ procedure
  • Blue-green deployment สำหรับ zero-downtime updates
  • Database migrations tested ก่อน production
  • Environment variables ทั้งหมด documented

Conclusion

REST APIs สำหรับ server-to-server communication มีความซับซ้อนที่มากกว่าที่ปรากฏ หลักการนั้นง่าย—ใช้ HTTP semantics อย่างถูกต้อง ออกแบบรอบทรัพยากร และคืน standardized responses—แต่การนำไปใช้อย่างสอดคล้องกันทั่วระบบแจกจ่ายจะกำหนดว่าสถาปัตยกรรมของคุณแข็งแกร่งหรือเปราะบาง

Key takeaways:

  1. HTTP semantics matter — GET vs. POST vs. PUT ไม่ใช่ตัวเลือกรูปแบบ พวกมันอธิบาย API contract ของคุณ
  2. Plan for failure — Retry logic, circuit breakers, และ graceful degradation เป็นสิ่งจำเป็นใน production
  3. Secure from the start — Authentication และ authorization ควรพิจารณาระหว่าง design ไม่ใช่ bolted on later
  4. Monitor everything — คุณไม่สามารถดีบัก สิ่งที่คุณไม่วัด structured logging และ metrics เป็นสิ่งจำเป็น
  5. Version deliberately — API changes เกิดขึ้น วางแผนสำหรับสนับสนุน versions หลาย version gracefully

นำ patterns เหล่านี้ไปใช้อย่างสอดคล้องกัน และคุณจะสร้าง APIs ที่ปรับขนาด บำรุงรักษา และ evolve โดยไม่ต้องดับไฟอย่างต่อเนื่อง


สำหรับข้อมูลเพิ่มเติมเกี่ยวกับหัวข้อที่เกี่ยวข้อง โปรดดู: /th/blog/server-to-server-communication-technologies

Comments powered by Giscus are not yet configured. Set PUBLIC_GISCUS_REPO_ID and PUBLIC_GISCUS_CATEGORY_ID in apps/web/.env to enable.

PV

เขียนโดย พลากร วรมงคล

Software Engineer Specialist ประสบการณ์กว่า 20 ปี เขียนเกี่ยวกับ Architecture, Performance และการสร้างระบบ Production

เพิ่มเติมเกี่ยวกับผม

บทความที่เกี่ยวข้อง