การศึกษาลึกเรื่อง: 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 และสร้างจากข้อจำกัดหกประการ:
- Client-Server Architecture — การแยกแยะความเกี่ยวข้องระหว่างไคลเอนต์และเซิร์ฟเวอร์
- Statelessness — แต่ละคำขอมีข้อมูลทั้งหมดที่จำเป็นในการทำความเข้าใจและประมวลผล
- Uniform Interface — วิธีการสื่อสารที่สอดคล้องกับทรัพยากร
- Cacheability — การตอบสนองควรกำหนดตัวเองว่า cacheable หรือไม่
- Layered System — ไคลเอนต์ไม่สามารถสมมติว่ามีการเชื่อมต่อโดยตรงกับเซิร์ฟเวอร์สิ้นสุด
- 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-Since404 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 URI400 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 body400 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 สำเร็จ ไม่มี body400 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— ลบสำเร็จ ไม่มี body200 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
จัดการชุดผลลัพธ์ขนาดใหญ่อย่างมีประสิทธิภาพโดยไม่โหลดคอลเลกชันทั้งหมดลงในหน่วยความจำ
Cursor-Based Pagination (Recommended)
มีประสิทธิภาพมากขึ้น เสถียรเมื่อข้อมูลเปลี่ยนแปลงในระหว่าง 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:
- HTTP semantics matter — GET vs. POST vs. PUT ไม่ใช่ตัวเลือกรูปแบบ พวกมันอธิบาย API contract ของคุณ
- Plan for failure — Retry logic, circuit breakers, และ graceful degradation เป็นสิ่งจำเป็นใน production
- Secure from the start — Authentication และ authorization ควรพิจารณาระหว่าง design ไม่ใช่ bolted on later
- Monitor everything — คุณไม่สามารถดีบัก สิ่งที่คุณไม่วัด structured logging และ metrics เป็นสิ่งจำเป็น
- Version deliberately — API changes เกิดขึ้น วางแผนสำหรับสนับสนุน versions หลาย version gracefully
นำ patterns เหล่านี้ไปใช้อย่างสอดคล้องกัน และคุณจะสร้าง APIs ที่ปรับขนาด บำรุงรักษา และ evolve โดยไม่ต้องดับไฟอย่างต่อเนื่อง
สำหรับข้อมูลเพิ่มเติมเกี่ยวกับหัวข้อที่เกี่ยวข้อง โปรดดู: /th/blog/server-to-server-communication-technologies