Designing Clean Backend APIs: Unified Responses, Validation, and Exception Handling in Spring

The article explains how to refactor Spring controller code by introducing a unified response structure, using ResponseBodyAdvice for automatic wrapping, fixing String conversion issues through message‑converter ordering, applying JSR‑303 validation (including custom validators), and implementing custom exceptions with a global exception handler, resulting in concise, maintainable backend APIs.

java1234
java1234
java1234
Designing Clean Backend APIs: Unified Responses, Validation, and Exception Handling in Spring

In a typical three‑layer or COLA architecture, the Controller layer is indispensable for receiving requests, delegating to Service, handling exceptions, and returning responses, but it should not contain business logic.

Problems with a naïve Controller implementation

Parameter validation is tightly coupled with business code, violating the Single Responsibility Principle.

Identical exceptions may be thrown from multiple services, causing code duplication.

Inconsistent error and success response formats make API integration unfriendly.

Unified response structure

Define a common result interface and an enum of standard codes, then implement a generic Result<T> class that provides static factory methods for success and failure.

public interface IResult {
    Integer getCode();
    String getMessage();
}

public enum ResultEnum implements IResult {
    SUCCESS(2001, "接口调用成功"),
    VALIDATE_FAILED(2002, "参数校验失败"),
    COMMON_FAILED(2003, "接口调用失败"),
    FORBIDDEN(2004, "没有权限访问资源");
    private Integer code;
    private String message;
    // getters, constructor omitted
}

public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    public static <T> Result<T> success(T data) {
        return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
    }
    public static Result<?> failed() { ... }
    // other factory methods omitted
}

Automatic wrapping with ResponseBodyAdvice

Implement ResponseBodyAdvice<Object> to intercept the body before it is written. If the body is already a Result, return it unchanged; otherwise wrap it with Result.success(body). This centralises the response format without touching individual controllers.

@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true; // can add exclusion annotations here
    }
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof Result) {
            return body;
        }
        return Result.success(body);
    }
}

Fixing the "cannot be cast to java.lang.String" issue

When a controller returns a String, the StringHttpMessageConverter processes it before the advice, causing a ClassCastException. Two solutions are presented:

Detect String in beforeBodyWrite, convert the Result to JSON manually, and set the produces attribute to application/json.

Reorder the converters so that MappingJackson2HttpMessageConverter precedes StringHttpMessageConverter. This can be done by adding the Jackson converter at index 0 or swapping the positions in configureMessageConverters.

@Configuration
public class WebConfiguration implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(0, new MappingJackson2HttpMessageConverter());
    }
}

Parameter validation with JSR‑303

Spring MVC leverages the JSR‑303 validation‑api (commonly via Hibernate Validator). By annotating DTO fields with constraints such as @NotBlank, @Length, @Email, and placing @Validated on the controller, validation is performed automatically and failures raise MethodArgumentNotValidException or ConstraintViolationException.

@Data
public class TestDTO {
    @NotBlank
    private String userName;
    @NotBlank @Length(min = 6, max = 20)
    private String password;
    @NotNull @Email
    private String email;
}

@RestController
@RequestMapping("/pretty")
@Validated
public class TestController {
    @PostMapping("/test-validation")
    public void testValidation(@RequestBody @Validated TestDTO dto) {
        testService.save(dto);
    }
}

Custom validation annotation example

When built‑in constraints are insufficient, a custom annotation and validator can be created. The example defines @Mobile to validate Chinese mobile numbers.

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
    boolean required() default true;
    String message() default "不是一个手机号码格式";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class MobileValidator implements ConstraintValidator<Mobile, CharSequence> {
    private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$");
    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext ctx) {
        if (value == null) return !required; // skip when not required
        return pattern.matcher(value).matches();
    }
}

Custom exceptions and global exception handling

Define domain‑specific runtime exceptions (e.g., BusinessException, ForbiddenException) and handle them centrally with a @RestControllerAdvice. Each handler returns a Result with the appropriate ResultEnum, ensuring that even error responses follow the unified format and that HTTP status remains 200.

public class BusinessException extends RuntimeException { public BusinessException(String msg){ super(msg); } }
public class ForbiddenException extends RuntimeException { public ForbiddenException(String msg){ super(msg); } }

@RestControllerAdvice(basePackages = "com.example.demo")
public class ExceptionAdvice {
    @ExceptionHandler(BusinessException.class)
    public Result<?> handleBusinessException(BusinessException ex) {
        return Result.failed(ex.getMessage());
    }
    @ExceptionHandler(ForbiddenException.class)
    public Result<?> handleForbiddenException(ForbiddenException ex) {
        return Result.failed(ResultEnum.FORBIDDEN);
    }
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleValidationException(MethodArgumentNotValidException ex) {
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fe : ex.getBindingResult().getFieldErrors()) {
            sb.append(fe.getField()).append(":").append(fe.getDefaultMessage()).append(", ");
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), sb.toString());
    }
    @ExceptionHandler(Exception.class)
    public Result<?> handle(Exception ex) {
        return Result.failed(ex.getMessage());
    }
}

Conclusion

After applying the unified response wrapper, proper message‑converter ordering, JSR‑303 validation (including custom rules), and a global exception handler, controller code becomes dramatically shorter and clearer. Developers can focus on business logic while the framework guarantees consistent API contracts, error handling, and response formats.

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.

springvalidationControllerjsr303exceptionhandlingresponsebodyadviceunifiedresponsebackendapi
java1234
Written by

java1234

Former senior programmer at a Fortune Global 500 company, dedicated to sharing Java expertise. Visit Feng's site: Java Knowledge Sharing, www.java1234.com

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.