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.

Java Captain
Java Captain
Java Captain
Building a Robust Backend API with Spring Boot: Validation, Global Exception Handling, and Unified Response

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<T> {
    /** 状态码,比如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<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());
}

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<String> APIExceptionHandler(APIException e) {
    return new ResultVO<>(ResultCode.FAILED, e.getMsg());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> 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<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // Skip if already a ResultVO
        return !returnType.getGenericParameterType().equals(ResultVO.class);
    }

    @Override
    public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaBackend DevelopmentSpring BootParameter Validationglobal exception handlingUnified response
Java Captain
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.