Mastering Spring Boot: Build Clean, Validated, and Unified Backend APIs
Learn how to construct robust Spring Boot backend APIs by integrating essential dependencies, applying concise parameter validation with Validator and BindResult, handling exceptions globally, creating custom error types, and standardizing responses through a unified ResultVO, all while simplifying code and improving maintainability.
Preface
A backend interface consists of URL, request method, request data, and response data. While standards vary across companies, the key to a good API is consistency. This article demonstrates step‑by‑step how to build a well‑structured backend API system.
Required Dependencies
Using a Spring Boot project, only
spring-boot-starter-webis required for the API. Swagger and Lombok are optional.
<code><!--web依赖包,web应用必备-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</code>Parameter Validation
Validating request parameters is essential. The article first shows a verbose manual validation implementation, then introduces Spring Validator and Hibernate Validator to simplify the process.
Business‑Layer Validation (Manual)
<code>public String addUser(User user) {
if (user == null || user.getId() == null || user.getAccount() == null
|| user.getPassword() == null || user.getEmail() == null) {
return "对象或者对象字段不能为空";
}
if (StringUtils.isEmpty(user.getAccount()) || StringUtils.isEmpty(user.getPassword())
|| StringUtils.isEmpty(user.getEmail())) {
return "不能输入空字符串";
}
if (user.getAccount().length() < 6 || user.getAccount().length() > 11) {
return "账号长度必须是6-11个字符";
}
if (user.getPassword().length() < 6 || user.getPassword().length() > 16) {
return "密码长度必须是6-16个字符";
}
if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
return "邮箱格式不正确";
}
//业务逻辑
return "success";
}
</code>Validator + BindResult
Annotate fields with validation constraints and add
@Validand
BindingResultto the controller method.
<code>@Data
public class User {
@NotNull(message = "用户id不能为空")
private Long id;
@NotNull(message = "用户账号不能为空")
@Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")
private String account;
@NotNull(message = "用户密码不能为空")
@Size(min = 6, max = 16, message = "密码长度必须是6-16个字符")
private String password;
@NotNull(message = "用户邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}
</code> <code>@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user, BindingResult bindingResult) {
for (ObjectError error : bindingResult.getAllErrors()) {
return error.getDefaultMessage();
}
return userService.addUser(user);
}
}
</code>When validation fails, the controller returns the first error message and the business logic is not executed.
Validator + Automatic Exception
Removing
BindingResultlets Spring throw
MethodArgumentNotValidExceptionautomatically.
<code>@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user) {
return userService.addUser(user);
}
</code>The exception produces a detailed error object, which is not user‑friendly.
Global Exception Handling
Use
@RestControllerAdvice(or
@ControllerAdvice) with
@ExceptionHandlerto process exceptions centrally.
<code>@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
return objectError.getDefaultMessage();
}
}
</code>The response now contains only the custom error message.
Custom Exception
Define a custom exception class to carry an error code and message.
<code>@Getter
public class APIException extends RuntimeException {
private int code;
private String msg;
public APIException() {
this(1001, "接口错误");
}
public APIException(String msg) {
this(1001, msg);
}
public APIException(int code, String msg) {
super(msg);
this.code = code;
this.msg = msg;
}
}
</code>Add handling for
APIExceptionin the advice class.
<code>@ExceptionHandler(APIException.class)
public String APIExceptionHandler(APIException e) {
return e.getMsg();
}
</code>Unified Response Structure
Create a generic
ResultVOclass containing
code,
msg, and
data. Controllers return
ResultVOinstead of raw data.
<code>@Getter
public class ResultVO<T> {
private int code;
private String msg;
private T data;
public ResultVO(T data) {
this(1000, "success", data);
}
public ResultVO(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}
</code>Update exception handlers to return
ResultVOwith appropriate codes.
<code>@ExceptionHandler(APIException.class)
public ResultVO<String> APIExceptionHandler(APIException e) {
return new ResultVO<>(1001, "响应失败", e.getMsg());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
return new ResultVO<>(1001, "参数校验失败", objectError.getDefaultMessage());
}
</code>Response Code Enum
Define an enum
ResultCodeto restrict possible codes and messages.
<code>@Getter
public enum ResultCode {
SUCCESS(1000, "操作成功"),
FAILED(1001, "响应失败"),
VALIDATE_FAILED(1002, "参数校验失败"),
ERROR(5000, "未知错误");
private int code;
private String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
</code>Modify
ResultVOconstructors to accept
ResultCode.
Global Response Advice
Implement
ResponseBodyAdviceto wrap any controller return value (except
ResultVO) automatically.
<code>@RestControllerAdvice(basePackages = {"com.rudecrab.demo.controller"})
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return !returnType.getParameterType().equals(ResultVO.class);
}
@Override
public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (returnType.getGenericParameterType().equals(String.class)) {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(new ResultVO<>(data));
} catch (JsonProcessingException e) {
throw new APIException("返回String类型错误");
}
}
return new ResultVO<>(data);
}
}
</code>Now even plain objects like
Userare automatically wrapped in the unified format.
Conclusion
Use Validator with automatic exception throwing for concise parameter checks.
Apply global exception handling and custom exceptions for consistent error processing.
Standardize all responses with a unified ResultVO and response‑code enum.
Leverage ResponseBodyAdvice to eliminate repetitive wrapping code.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.