Building a Robust Backend API with Spring Boot: Validation, Global Exception Handling, and Unified Response
This article demonstrates step‑by‑step how to construct a clean and standardized backend API in Spring Boot by introducing required dependencies, using Validator for request parameter checks, applying global exception handling, defining custom exceptions, and implementing a unified response format for both success and error cases.
Introduction
A backend API typically consists of four parts: URL, HTTP method, request data, and response data. While standards vary across companies, a well‑structured API is essential for maintainability and readability.
Required Dependencies
The project uses Spring Boot with only the spring-boot-starter-web dependency for the web layer. Swagger and Lombok are optional.
<!-- web dependency, essential for web applications -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>Parameter Validation
Business‑Layer Validation
Traditional validation is performed manually in the service layer, resulting in verbose 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";
}This approach is functional but cumbersome.
Validator + BindingResult
Spring's Validator and Hibernate Validator allow declarative validation via annotations.
@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;
}Controller method with @Valid and BindingResult :
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user, BindingResult bindingResult) {
// If validation fails, errors are collected in BindingResult
for (ObjectError error : bindingResult.getAllErrors()) {
return error.getDefaultMessage();
}
return userService.addUser(user);
}
}When validation fails, the method returns the first error message without executing business logic.
Validator + Automatic Exception
By removing BindingResult , Spring automatically throws MethodArgumentNotValidException on validation failure.
@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user) {
return userService.addUser(user);
}The exception can be handled globally.
Global Exception Handling
Create a class annotated with @RestControllerAdvice (or @ControllerAdvice ) and define methods with @ExceptionHandler for specific exception types.
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// Extract the first validation error message
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
return objectError.getDefaultMessage();
}
}To customize error handling further, define a custom exception:
@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;
}
}Handle the custom exception globally:
@ExceptionHandler(APIException.class)
public String APIExceptionHandler(APIException e) {
return e.getMsg();
}Unified Response Structure
Define a generic response wrapper ResultVO<T> containing code , msg , and data .
@Getter
public class ResultVO
{
/** 状态码,比如1000代表响应成功 */
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;
}
}Update global exception handlers to return ResultVO :
@ExceptionHandler(APIException.class)
public ResultVO
APIExceptionHandler(APIException e) {
return new ResultVO<>(1001, "响应失败", e.getMsg());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO
MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
return new ResultVO<>(1001, "参数校验失败", objectError.getDefaultMessage());
}Response Code Enum
Standardize response codes using an enum:
@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;
}
}Modify ResultVO constructors to accept ResultCode :
public ResultVO(T data) {
this(ResultCode.SUCCESS, data);
}
public ResultVO(ResultCode resultCode, T data) {
this.code = resultCode.getCode();
this.msg = resultCode.getMsg();
this.data = data;
}Update exception handlers accordingly:
@ExceptionHandler(APIException.class)
public ResultVO
APIExceptionHandler(APIException e) {
return new ResultVO<>(ResultCode.FAILED, e.getMsg());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO
MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());
}Global Response Body Advice
To avoid manually wrapping every controller return value, implement ResponseBodyAdvice :
@RestControllerAdvice(basePackages = {"com.rudecrab.demo.controller"})
public class ResponseControllerAdvice implements ResponseBodyAdvice
{
@Override
public boolean supports(MethodParameter returnType, Class
> converterType) {
// Skip if already a ResultVO
return !returnType.getGenericParameterType().equals(ResultVO.class);
}
@Override
public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType,
Class
> converterType,
ServerHttpRequest request, ServerHttpResponse response) {
// Special handling for String type
if (returnType.getGenericParameterType().equals(String.class)) {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(new ResultVO<>(data));
} catch (JsonProcessingException e) {
throw new APIException("返回String类型错误");
}
}
// Wrap other types directly
return new ResultVO<>(data);
}
}Now controller methods can simply return domain objects:
@GetMapping("/getUser")
public User getUser() {
User user = new User();
user.setId(1L);
user.setAccount("12345678");
user.setPassword("12345678");
user.setEmail("[email protected]");
return user; // ResponseBodyAdvice will wrap it into ResultVO
}Summary
Use Validator with automatic exception throwing for concise parameter validation.
Apply global exception handling and custom exceptions to standardize error responses.
Introduce a unified response wrapper and response‑code enum to keep API contracts consistent.
Leverage ResponseBodyAdvice to automatically wrap successful responses, reducing boilerplate.
The presented approach is a guideline; teams can adapt parts to fit their own project conventions.
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
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.