Unify Spring Boot API Responses and Streamline Exception Handling
This article explains how to standardize API return formats with a generic Result object, define error codes and custom exceptions, implement a global exception handler, use ResponseBodyAdvice for automatic response wrapping, and package these utilities into a reusable Spring Boot starter for cleaner backend code.
In Domain‑Driven Design (DDD) the interface layer handles external requests and returns results.
1. Unified Return Format
1.1 Build Result object
To standardize responses a generic Result<T> class is introduced with fields code, message, data and timestamp.
@Data
@Accessors(chain = true)
public class Result<T> {
public static final String SUCCESS_CODE = "OK";
private String code;
private String message;
private T data;
private long timestamp;
}A helper ResultHelper provides static methods to create success or failure results.
@Slf4j
public class ResultHelper {
public static <T> Result<T> success(T data) {
return new Result<T>()
.setCode(SUCCESS_CODE)
.setData(data)
.setTimestamp(System.currentTimeMillis());
}
public static <T> Result<T> fail(String message) {
return new Result<T>()
.setCode(ErrorCode.SERVICE_ERROR.getCode())
.setMessage(message)
.setTimestamp(System.currentTimeMillis());
}
// ...
}1.2 Optimize DailyMart API
Example registration endpoint returns Result<UserRegistrationDTO> using the helper.
@PostMapping("/api/customer/register")
public Result<UserRegistrationDTO> register(@RequestBody @Valid UserRegistrationDTO customerDTO) {
try {
return ResultHelper.success(customerService.register(customerDTO));
} catch (Exception e) {
return ResultHelper.fail(e.getMessage());
}
}Successful response yields JSON with code "OK", message null, data object and timestamp; failure returns error code such as "B0001" and message.
2. Exception Control
Instead of throwing generic RuntimeException, custom exceptions derived from an abstract base provide error codes and messages.
2.1 Error Code Concept
Following Alibaba development guidelines, error codes consist of a source identifier (A‑user, B‑system, C‑third‑party) and a four‑digit number.
A indicates user‑related errors.
B indicates system errors.
C indicates third‑party service errors.
2.2 Define ErrorCode enum
public enum ErrorCode {
OK("00000","操作已成功"),
CLIENT_ERROR("A0001","客户端错误"),
USER_NOT_FOUND("A0010","用户不存在"),
USER_ALREADY_EXISTS("A0011","用户已存在"),
USERNAME_PASSWORD_INCORRECT("A0012","用户名或密码错误"),
VERIFICATION_CODE_EXPIRED("A0013","验证码已过期"),
BAD_CREDENTIALS_EXPIRED("A0014","用户认证异常"),
SERVICE_ERROR("B0001","系统内部错误"),
SERVICE_TIMEOUT_ERROR("B0010","系统执行超时"),
REMOTE_ERROR("C0001","第三方服务错误");
// fields, constructor, getters ...
}2.3 Create custom exception hierarchy
AbstractException stores code and message and extends RuntimeException. Specific exceptions such as ClientException, BusinessException, and RemoteException inherit from it.
@Getter
public abstract class AbstractException extends RuntimeException {
private final String code;
private final String message;
public AbstractException(ErrorCode errorCode, String message, Throwable throwable) {
super(message, throwable);
this.code = errorCode.getCode();
this.message = Optional.ofNullable(message).orElse(errorCode.getMessage());
}
}Example concrete exception:
public class ClientException extends AbstractException {
public ClientException() {
this(ErrorCode.CLIENT_ERROR, null, null);
}
public ClientException(String message) {
this(ErrorCode.CLIENT_ERROR, message, null);
}
// ... other constructors ...
}Specific UserNotFoundException extends ClientException and uses ErrorCode.USER_NOT_FOUND.
3. Global Exception Handling
Using @RestControllerAdvice a GlobalExceptionHandler handles validation errors, custom AbstractException, and any other Throwable, returning a unified Result object.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidException(HttpServletRequest request, MethodArgumentNotValidException ex) {
// extract first field error message
String msg = ...;
log.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), msg);
return ResultHelper.fail(ErrorCode.CLIENT_ERROR, msg);
}
@ExceptionHandler(AbstractException.class)
public Result<Void> handleAbstractException(HttpServletRequest request, AbstractException ex) {
log.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), ex.toString());
return ResultHelper.fail(ex);
}
@ExceptionHandler(Throwable.class)
public Result<Void> handleThrowable(HttpServletRequest request, Throwable throwable) {
log.error("[{}] {}", request.getMethod(), getUrl(request), throwable);
return ResultHelper.fail();
}
}4. Automatic Response Wrapping
Implementing ResponseBodyAdvice allows all controller return values to be automatically wrapped with ResultHelper.success(), eliminating repetitive code.
@Slf4j
@RestControllerAdvice
public class GlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof String) {
return objectMapper.writeValueAsString(ResultHelper.success(body));
}
if (body instanceof Result) {
return body;
}
return ResultHelper.success(body);
}
}5. Defining a Spring Boot Starter
To reuse the global exception handler and response‑body advice across services, they are packaged into a starter with an auto‑configuration class WebAutoConfiguration that registers the beans if they are missing.
@SpringBootConfiguration
@ConditionalOnWebApplication
public class WebAutoConfiguration {
@Bean
@ConditionalOnMissingBean(GlobalExceptionHandler.class)
public GlobalExceptionHandler dailyMartGlobalExceptionHandler() {
return new GlobalExceptionHandler();
}
@Bean
@ConditionalOnMissingBean(GlobalResponseBodyAdvice.class)
public GlobalResponseBodyAdvice dailyMartGlobalResponseBodyAdvice() {
return new GlobalResponseBodyAdvice();
}
}The starter also includes the required META-INF/spring/AutoConfiguration.imports file so that Spring Boot loads the configuration automatically.
Conclusion
The article demonstrates how to standardize API responses, define error codes, create custom exceptions, implement a global exception handler, use ResponseBodyAdvice for automatic wrapping, and package these utilities into a reusable Spring Boot starter, thereby simplifying controller code and improving maintainability.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
