Trong bài viết này, chúng ta sẽ cùng tìm hiểu cách xây dựng một hệ thống phản hồi API thống nhất, giúp định dạng dữ liệu trả về từ server luôn nhất quán, từ đó hỗ trợ bảo trì và phát triển dễ dàng hơn.
1. Lớp thực thể phản hồi chung
Trước tiên, bạn cần định nghĩa một lớp thực thể chung cho tất cả các phản hồi API. Điều này giúp đảm bảo mọi endpoint đều trả về cấu trúc dữ liệu giống nhau, bao gồm ba thành phần chính:
- Mã trạng thái (status code)
- Thông báo (message)
- Dữ liệu (data)
Ví dụ về lớp ApiResult:
@AllArgsConstructor
@Data
public class ApiResult<T> {
/**
* Mã trạng thái thành công
*/
public static final int OK = HttpStatus.HTTP_OK;
/**
* Mã phản hồi
*/
private int code;
/**
* Thông báo phản hồi
*/
private String message;
/**
* Dữ liệu trả về
*/
private T data;
}
2. Lớp tiện ích tạo phản hồi
Để việc khởi tạo đối tượng ApiResult trở nên gọn gàng hơn, chúng ta tạo một lớp tiện ích chuyên xử lý các trường hợp thành công và thất bại:
public class ApiResultUtil {
private ApiResultUtil() {
// Ngăn khởi tạo instance
}
/**
* Phản hồi thành công kèm dữ liệu
*/
public static <T> ApiResult<T> success(T data) {
return new ApiResult<>(ApiResult.OK, null, data);
}
/**
* Phản hồi thành công không kèm dữ liệu
*/
public static <T> ApiResult<T> success() {
return success(null);
}
/**
* Phản hồi thất bại
*/
public static <T> ApiResult<T> error(int code, String message) {
return new ApiResult<>(code, message, null);
}
}
Lớp này cung cấp các phương thức tĩnh, giúp đơn giản hóa việc tạo đối tượng phản hồi – tương tự như cách nhiều thư viện JDK áp dụng.
3. Xử lý ngoại lệ nghiệp vụ
Trong thực tế, có hai loại ngoại lệ cần quan tâm:
- Ngoại lệ hệ thống (ví dụ lỗi 500)
- Ngoại lệ nghiệp vụ (ví dụ tên người dùng bị trùng)
Để phân biệt rõ ràng, hãy tạo lớp BusinessException kế thừa RuntimeException:
@AllArgsConstructor
@Data
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = -6735897190745766939L;
/**
* Mã lỗi nghiệp vụ
*/
private int code;
/**
* Thông báo lỗi
*/
private String message;
public BusinessException() {
super();
}
}
Ví dụ: Khi người dùng đăng ký với tên đã tồn tại, bạn có thể ném ra BusinessException với mã lỗi và thông báo cụ thể như "Tên người dùng đã tồn tại".
4. Bắt ngoại lệ toàn cục
Sử dụng @RestControllerAdvice trong Spring MVC để bắt mọi ngoại lệ và trả về định dạng thống nhất:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Throwable.class)
public ApiResult handleException(Throwable e) {
if (e instanceof BusinessException) {
BusinessException be = (BusinessException) e;
log.info("Ngoại lệ nghiệp vụ: ", e);
return ApiResultUtil.error(be.getCode(), be.getMessage());
}
log.error("Ngoại lệ hệ thống: ", e);
return ApiResultUtil.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Lỗi máy chủ nội bộ");
}
}
Phương thức handleException kiểm tra loại ngoại lệ: nếu là BusinessException, nó trả về mã lỗi và thông báo tương ứng; nếu không, nó trả về lỗi 500 mặc định.
5. Đóng gói phản hồi thành công
Để tự động đóng gói kết quả trả về từ các controller, ta triển khai interface ResponseBodyAdvice:
@ControllerAdvice
public class GlobalApiResultHandler implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sra.getRequest();
String uri = request.getRequestURI();
return matchUrl(uri);
}
private boolean matchUrl(String uri) {
if (uri == null || uri.isBlank()) {
return false;
}
return uri.contains("/v1");
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof ApiResult) {
return body;
}
return ApiResultUtil.success(body);
}
}
Ở đây, supports giới hạn chỉ áp dụng với các request có URI chứa "/v1" (tránh ảnh hưởng đến các endpoint không phải API). beforeBodyWrite sẽ tự động bọc kết quả trả về vào ApiResult nếu chưa được bọc.
6. Kiểm tra kết quả
Giả sử bạn chạy ứng dụng tại http://localhost:8011 và dùng Swagger để kiểm tra:
Trường hợp thành công: Gọi API lấy danh sách người dùng, kết quả trả về có cấu trúc {code, message, data} như mong đợi.
Trường hợp lỗi: Gọi API thêm người dùng với username đã tồn tại, hệ thống trả về mã lỗi và thông báo "Tên người dùng đã tồn tại" thay vì lỗi 500 chung chung.
Như vậy, giải pháp trên không chỉ giúp chuẩn hóa phản hồi API mà còn tách biệt rõ ràng giữa lỗi hệ thống và lỗi nghiệp vụ – một thiết kế có tính ứng dụng cao trong các dự án thực tế.