Backend Development 27 min read

Comprehensive Guide to Backend API Design with Spring Boot: Validation, Global Exception Handling, Unified Responses, Versioning, and Security

This article provides a step‑by‑step tutorial on building robust Spring Boot backend APIs, covering environment setup, parameter validation techniques, global exception handling, unified response structures, API version control, and security measures such as token authentication, timestamp checks, URL signing, replay protection, and HTTPS.

Architect
Architect
Architect
Comprehensive Guide to Backend API Design with Spring Boot: Validation, Global Exception Handling, Unified Responses, Versioning, and Security

1. Introduction

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

2. Environment Setup

Include the necessary Spring Boot starters and validation dependencies:

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

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

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

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

3. Parameter Validation

Three common validation approaches are demonstrated, with the most concise being Validator + Automatic Exception Throwing :

3.1 Basic Validation (Business Layer)

Manual checks in the service layer are verbose and error‑prone.

3.2 Validator + BindingResult

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

3.3 Validator + Automatic Exception

Annotate the request object with validation constraints and add @Valid on the controller method. Spring will automatically throw MethodArgumentNotValidException on failure.

@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:

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

3.4 Group and Recursive Validation

Define a validation group interface and apply it via groups attribute:

public interface Update extends Default {}

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

Controller usage:

@PostMapping("update")
public String update(@Validated({Update.class}) User user) {
    return "success";
}

3.5 Custom Validation Annotation

@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
[] payload() default {};
}

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

4. Global Exception Handling

Use @RestControllerAdvice (or @ControllerAdvice ) to centralize error responses.

@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
handleUnexpectedServer(Exception ex) {
        log.error("System error:", ex);
        return new ResultVO<>(GlobalMsgEnum.ERROR);
    }
}

4.1 Custom API Exception

@Getter
public class APIException extends RuntimeException {
    private int code;
    private String msg;
    public APIException() { this(1001, "API 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 ResultVO
handleAPI(APIException e) {
    return new ResultVO<>(ResultCode.FAILED, e.getMsg());
}

5. Unified Response Structure

Define a response enum and a generic wrapper:

@Getter
public enum ResultCode {
    SUCCESS(1000, "Operation successful"),
    FAILED(1001, "Operation failed"),
    VALIDATE_FAILED(1002, "Parameter validation failed"),
    ERROR(5000, "Unknown error");
    private final int code;
    private final String msg;
    ResultCode(int code, String msg) { this.code = code; this.msg = msg; }
}

@Getter
public class ResultVO
{
    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;
    }
}

6. Optional Global Response Wrapping

Implement ResponseBodyAdvice to automatically wrap non‑wrapped return values, with a switch annotation @NotResponseBody to opt‑out.

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

@RestControllerAdvice(basePackages = "com.csdn.demo1.controller")
public class ResponseControllerAdvice implements ResponseBodyAdvice
{
    @Override
    public boolean supports(MethodParameter returnType, Class
> converterType) {
        return !(returnType.getParameterType().equals(ResultVO.class) ||
                 returnType.hasMethodAnnotation(NotResponseBody.class));
    }
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                Class
> 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);
    }
}

7. API Version Control

Two strategies are provided: path‑based and header‑based versioning.

7.1 Path Versioning

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

public class ApiVersionCondition implements RequestCondition
{
    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; }
}

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());
    }
}

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

@RestController
@ApiVersion("1.0")
@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"; }
}

7.2 Header Versioning

Replace the URI check with a header lookup (e.g., X-VERSION ) in ApiVersionCondition .

8. API Security

Implement a multi‑layer security scheme:

Token authentication stored in Redis; token = md5(userId + timestamp + serverSecret).

Timestamp validation to reject stale requests.

URL signing (sorted parameters + secret → MD5) to detect tampering.

Replay protection by caching the signature in Redis for the same duration as the timestamp.

Enforce HTTPS to prevent plaintext interception.

Typical request format:

http://api.example.com/getInfo?id=1&timestamp=1661061696&sign=abcdef1234567890

9. Summary

The article demonstrates a complete backend API framework that combines concise validation (Validator + automatic exception), centralized error handling, consistent response payloads, optional global wrapping, flexible versioning, and robust security measures, enabling developers to focus on business logic while maintaining high code quality and safety.

Exception HandlingSpring BootsecurityAPI designParameter ValidationVersioning
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.