Backend Development 20 min read

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
{
    /** 状态码,比如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.

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

login 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.