Spring Boot API Mastery: Validation, Global Exceptions, Responses and Security

This comprehensive guide walks you through building robust Spring Boot backend APIs, covering environment setup, parameter validation with Validator and groups, custom validation, global exception handling, unified response structures, optional response wrapping, API version control via path and header, and essential security measures such as token authentication, timestamp checks, URL signing, replay protection, and HTTPS usage.

Architect's Guide
Architect's Guide
Architect's Guide
Spring Boot API Mastery: Validation, Global Exceptions, Responses and Security

Introduction

A backend API typically consists of four parts: URL, request method, request data, and response data. While there is no universal standard, consistency and proper validation are essential.

Environment Setup

Include the necessary dependencies in your project:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
  <groupId>com.github.xiaoymin</groupId>
  <artifactId>knife4j-spring-boot-starter</artifactId>
  <version>2.0.2</version>
</dependency>

<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <optional>true</optional>
</dependency>

Parameter Validation

1. Introduction

Three common validation approaches are used; the tutorial focuses on the most concise third method.

Business‑layer validation

Validator + BindingResult validation

Validator + automatic exception throwing

2. Validator + BindingResult

Example controller method:

@PostMapping("/addUser")
public String addUser(@RequestBody @Validated User user, BindingResult bindingResult) {
    List<ObjectError> allErrors = bindingResult.getAllErrors();
    if (!allErrors.isEmpty()) {
        return allErrors.stream()
                         .map(o -> o.getDefaultMessage())
                         .collect(Collectors.toList()).toString();
    }
    return validationService.addUser(user);
}

3. Validator + Automatic Exception

Annotate the request object with validation constraints and add @Valid on the parameter. Spring will automatically throw MethodArgumentNotValidException when validation fails.

@Data
public class User {
    @NotNull(message = "User ID cannot be null")
    private Long id;

    @NotNull(message = "Account cannot be null")
    @Size(min = 6, max = 11, message = "Account length must be 6‑11 characters")
    private String account;

    @NotNull(message = "Password cannot be null")
    @Size(min = 6, max = 16, message = "Password length must be 6‑16 characters")
    private String password;

    @NotNull(message = "Email cannot be null")
    @Email(message = "Invalid email format")
    private String email;
}

Controller method:

@RestController
@RequestMapping("user")
public class ValidationController {
    @Autowired
    private ValidationService validationService;

    @PostMapping("/addUser")
    public String addUser(@RequestBody @Validated User user) {
        return validationService.addUser(user);
    }
}

4. Group Validation and Recursive Validation

Define a group interface, apply it to constraint annotations, and specify the group in @Validated on the controller method.

public interface Update extends Default {}

@Data
public class User {
    @NotNull(message = "User ID cannot be null", groups = Update.class)
    private Long id;
    // ... other fields
}
@PostMapping("/update")
public String update(@Validated({Update.class}) User user) {
    return "success";
}

For nested objects, simply add @Valid on the field.

5. Custom Validation

Create a custom annotation and its validator:

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = HaveNoBlankValidator.class)
public @interface HaveNoBlank {
    String message() default "String cannot contain spaces";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlank, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) return true;
        return !value.contains(" ");
    }
}

Global Exception Handling

1. Basic Usage

Create a class annotated with @RestControllerAdvice (or @ControllerAdvice) and define methods with @ExceptionHandler for specific exceptions.

@RestControllerAdvice
public class ExceptionControllerAdvice {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleValidation(MethodArgumentNotValidException e) {
        ObjectError error = e.getBindingResult().getAllErrors().get(0);
        return error.getDefaultMessage();
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResultVO<?> handleUnexpected(Exception ex) {
        log.error("System error:", ex);
        return new ResultVO<>(GlobalMsgEnum.ERROR);
    }
}

2. Custom Exception

Define a custom runtime exception carrying a code and message:

@Getter
public class APIException extends RuntimeException {
    private int code;
    private String msg;
    public APIException() { this(1001, "Interface error"); }
    public APIException(String msg) { this(1001, msg); }
    public APIException(int code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }
}

Handle it in the advice:

@ExceptionHandler(APIException.class)
public String handleAPI(APIException e) {
    return e.getMsg();
}

Unified Response Structure

Define an enum for result codes and a generic response wrapper.

@Getter
public enum ResultCode {
    SUCCESS(1000, "Operation successful"),
    FAILED(1001, "Response failed"),
    VALIDATE_FAILED(1002, "Parameter validation failed"),
    ERROR(5000, "Unknown error");
    private int code;
    private String msg;
    ResultCode(int code, String msg) { this.code = code; this.msg = msg; }
}
@Getter
public class ResultVO<T> {
    private int code;
    private String msg;
    private T data;
    public ResultVO(T data) { this(ResultCode.SUCCESS, data); }
    public ResultVO(ResultCode rc, T data) {
        this.code = rc.getCode();
        this.msg = rc.getMsg();
        this.data = data;
    }
}

Controllers can now return ResultVO objects, ensuring a consistent response format.

Optional Global Response Wrapping

Create a marker annotation to skip wrapping:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface NotResponseBody {}

Implement ResponseBodyAdvice to wrap responses unless the method is annotated with NotResponseBody or already returns ResultVO:

@RestControllerAdvice(basePackages = {"com.csdn.demo1.controller"})
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return !(returnType.getParameterType().equals(ResultVO.class) ||
                 returnType.hasMethodAnnotation(NotResponseBody.class));
    }
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                ServerHttpRequest request, ServerHttpResponse response) {
        if (returnType.getGenericParameterType().equals(String.class)) {
            try {
                return new ObjectMapper().writeValueAsString(new ResultVO<>(body));
            } catch (JsonProcessingException e) {
                throw new APIException("String response error");
            }
        }
        return new ResultVO<>(body);
    }
}

API Version Control

Path‑Based Versioning

Define an annotation and a custom request condition:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    String value() default "1.0";
}

public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
    private static final Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+\\.\\d+)");
    private final String version;
    public ApiVersionCondition(String version) { this.version = version; }
    @Override
    public ApiVersionCondition combine(ApiVersionCondition other) { return new ApiVersionCondition(other.version); }
    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI());
        if (m.find() && Objects.equals(m.group(1), version)) {
            return this;
        }
        return null;
    }
    @Override
    public int compareTo(ApiVersionCondition other, HttpServletRequest request) { return 0; }
    public String getApiVersion() { return version; }
}

Register a custom RequestMappingHandlerMapping that reads the annotation:

public class PathVersionHandlerMapping extends RequestMappingHandlerMapping {
    @Override
    protected boolean isHandler(Class<?> beanType) {
        return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
    }
    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        ApiVersion av = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
        return av == null ? null : new ApiVersionCondition(av.value());
    }
    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        ApiVersion av = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        return av == null ? null : new ApiVersionCondition(av.value());
    }
}

Enable it via WebMvcRegistrations:

@Configuration
public class WebMvcConfiguration implements WebMvcRegistrations {
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new PathVersionHandlerMapping();
    }
}

Controller example:

@RestController
@ApiVersion
@RequestMapping("/{version}/test")
public class TestController {
    @GetMapping("one")
    public String query() { return "test api default"; }

    @GetMapping("one")
    @ApiVersion("1.1")
    public String queryV11() { return "test api v1.1"; }

    @GetMapping("one")
    @ApiVersion("3.1")
    public String queryV31() { return "test api v3.1"; }
}

Header‑Based Versioning

Use a similar condition that reads a custom header (e.g., X-VERSION) instead of the URL path.

public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
    private static final String X_VERSION = "X-VERSION";
    private final String version;
    // combine, compareTo same as before
    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        String header = request.getHeader(X_VERSION);
        return Objects.equals(version, header) ? this : null;
    }
}

API Security Measures

Token Authentication

After a successful login, issue a token (e.g., md5(userId + timestamp + serverSecret)) and store it in Redis. Subsequent requests must include the token, which is validated against the cache.

Timestamp Validation

Clients send a timestamp parameter; the server rejects requests where the timestamp differs from the current time by more than a configured threshold (e.g., 1 minute), mitigating replay and DoS attacks.

URL Signing

Sort all request parameters (including the URL) alphabetically, concatenate them with &, prepend or append a secret key, and compute an MD5 hash. The resulting sign is sent with the request and verified server‑side.

Replay Protection

Store each generated sign in Redis with the same expiration as the timestamp. If a request arrives with a previously seen sign, reject it as a replay.

HTTPS

All communication should be over HTTPS to encrypt data in transit and prevent eavesdropping.

Section header image
Section header image
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.

API VersioningSpring BootAPI Validationglobal exception handlingUnified response
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

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.