19 - Response Format
เป้าหมายของบท
Section titled “เป้าหมายของบท”บทนี้จะเพิ่ม response wrapper ชื่อ ApiResponse<T> เพื่อให้ success response ของระบบมีรูปแบบสม่ำเสมอ
หลังจบบทนี้ผู้อ่านควรเข้าใจ:
- response format กลางช่วยอะไร
- ควรใช้ wrapper เมื่อไหร่
- วิธีสร้าง generic record
ApiResponse<T> - วิธีใช้
ResponseEntityคู่กับApiResponse - ข้อควรระวังของการห่อ response ทุก endpoint
ปัญหาของ response ที่ไม่สม่ำเสมอ
Section titled “ปัญหาของ response ที่ไม่สม่ำเสมอ”ถ้าแต่ละ endpoint ตอบไม่เหมือนกัน client จะเขียนโค้ดยาก
ตัวอย่าง response แบบไม่สม่ำเสมอ:
[ { "id": 1, "email": "john@example.com" }]อีก endpoint:
{ "message": "User created", "user": { "id": 1, "email": "john@example.com" }}อีก endpoint:
{ "data": { "id": 1, "email": "john@example.com" }}ถ้า response shape เปลี่ยนไปเรื่อย ๆ frontend ต้องเช็กหลายรูปแบบ ทำให้ bug ง่าย
สร้าง ApiResponse
Section titled “สร้าง ApiResponse”สร้างไฟล์:
src/main/java/com/example/backendapi/common/ApiResponse.javapackage com.example.backendapi.common;
import java.time.LocalDateTime;
public record ApiResponse<T>( boolean success, String message, T data, LocalDateTime timestamp) { public static <T> ApiResponse<T> ok(String message, T data) { return new ApiResponse<>(true, message, data, LocalDateTime.now()); }}<T> ทำให้ data เป็น type อะไรก็ได้ เช่น:
UserResponseList<UserResponse>PageResponse<UserResponse>ใช้กับ GET endpoint
Section titled “ใช้กับ GET endpoint”@GetMapping("/{id}")public ApiResponse<UserResponse> findById(@PathVariable Long id) { return ApiResponse.ok("User found", userService.findById(id));}response:
{ "success": true, "message": "User found", "data": { "id": 1, "username": "john", "email": "john@example.com", "role": "USER", "status": "ACTIVE" }, "timestamp": "2026-06-05T15:30:00"}ใช้กับ POST endpoint
Section titled “ใช้กับ POST endpoint”กรณี create ควรใช้ ResponseEntity เพื่อกำหนด status 201 Created:
@PostMappingpublic ResponseEntity<ApiResponse<UserResponse>> create( @Valid @RequestBody CreateUserRequest request) { UserResponse user = userService.create(request); URI location = URI.create("/api/v1/users/" + user.id());
return ResponseEntity.created(location) .body(ApiResponse.ok("User created", user));}จุดสำคัญ:
- HTTP status เป็น
201 Created - header
Locationชี้ไปยัง resource ใหม่ - body ใช้ format กลาง
ใช้กับ DELETE endpoint
Section titled “ใช้กับ DELETE endpoint”สำหรับ delete มี 2 แนวทางที่ใช้ได้
แนวทางแรก ตอบ 204 No Content และไม่มี body:
@DeleteMapping("/{id}")public ResponseEntity<Void> deleteById(@PathVariable Long id) { userService.deleteById(id); return ResponseEntity.noContent().build();}แนวทางที่สอง ตอบ 200 OK พร้อม message:
@DeleteMapping("/{id}")public ApiResponse<Void> deleteById(@PathVariable Long id) { userService.deleteById(id); return ApiResponse.ok("User deleted", null);}ในหนังสือนี้จะใช้ 204 No Content ต่อ เพราะเป็น REST convention ที่ชัดเจน และไม่จำเป็นต้องมี body
Error response ควรรวมกับ ApiResponse ไหม
Section titled “Error response ควรรวมกับ ApiResponse ไหม”ทำได้ แต่ในหนังสือนี้จะแยก:
- success response ใช้
ApiResponse<T> - error response ใช้
ErrorResponse
เหตุผลคือ error มักมี field เฉพาะ เช่น status, path, errors ส่วน success response เน้น message และ data
อย่าออกแบบ wrapper ซับซ้อนเกินไป
Section titled “อย่าออกแบบ wrapper ซับซ้อนเกินไป”มือใหม่มักอยากใส่ field เยอะ เช่น:
successcodemessagedatametalinksdebugtracerequestIdtimestampversionเริ่มจากน้อยก่อน:
successmessagedatatimestampแล้วเพิ่มเมื่อมีความต้องการจริง
แบบฝึกหัดท้ายบท
Section titled “แบบฝึกหัดท้ายบท”- สร้าง
ApiResponse<T> - ปรับ
GET /api/v1/users/{id}ให้ใช้ApiResponse - ปรับ
POST /api/v1/usersให้ตอบ201 Createdพร้อมApiResponse - ตัดสินใจว่าจะให้
DELETEตอบ204หรือApiResponse - เทียบ response ก่อนและหลังเพิ่ม wrapper