Standardise the response of REST API in spring boot (Spring boot's ResponseBodyAdvice)

Standardise the response of REST API in spring boot (Spring boot's ResponseBodyAdvice)

As we know know Just like Captain america's Civil War fight, so in development  war between front end developers vs back end is hard to explain, usual complaint for any screw ups is API response is not consistent, this will be both burden to FE devs and BE developers, Which eats up lot of development time. during peer development and strict timelines as a backed developer  i always used to cut corners to finish the task, This is a traditional mind set of any developer and i am no exception in this. So i wanted simple solution and to stop wasting my time in enforcing and micro managing my peers for small mistake like this. so i made good use of spring boot's ResponseBodyAdvice. So now we shall dive into implementation rather than ranting about it :-P, BTW if your dealing with multiple micro services the fight could happen between service owners not just backed and front end developer.

Step 1 : Creating Standard POJO for Error Response  and Success Response.

public class SuccessResponse<T> {
    private SuccessDTO<T> response;

    public SuccessDTO<T> getResponse() {
        return response;
    }

    public void setResponse(SuccessDTO<T> response) {
        this.response = response;
    }

    public SuccessResponse(T object) {
        this.response = new SuccessDTO<>(object);
    }

    public SuccessResponse(T object, String message) {
        this.response = new SuccessDTO<>(object, message);
    }

    public SuccessResponse(T object, Integer length, String message) {
        this.response = new SuccessDTO<>(object, length, message);
    }

    public SuccessResponse(T object, Integer length) {
        this.response = new SuccessDTO<>(object, length);
    }


}
The Above Code Block to wrap success DTO
@JsonInclude(JsonInclude.Include.NON_NULL)
public class SuccessDTO<T> implements java.io.Serializable {
    private T body;
    private int length = 1;
    private String message = null;


    public SuccessDTO(T body, int length, String message) {
        this.body = body;
        this.length = length;
        this.message = message;
        if (length == 0) {
            if (this.body instanceof List) {
                this.length = ((List) this.body).size();
            }

            if (this.body instanceof Map) {
                this.length = ((Map) this.body).size();
            }
        }

    }

    public SuccessDTO(T body, String message) {
        this.body = body;
        this.message = message;

        if (this.body instanceof List) {
            this.length = ((List) this.body).size();
        }

        if (this.body instanceof Map) {
            this.length = ((Map) this.body).size();
        }
    }

    public SuccessDTO(T body, Integer length) {
        this.body = body;
        this.length = length;
    }

    public void setLength(Integer length) {
        this.length = length;
    }

    public SuccessDTO(T body) {
        this.body = body;
        if (this.body instanceof List) {
            this.length = ((List) this.body).size();
        }

        if (this.body instanceof Map) {
            this.length = ((Map) this.body).size();
        }
    }

    public T getBody() {

        return body;
    }

    public void setBody(T body) {
        this.body = body;
    }

    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}
Actual Response body
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorResponse<T> {
    private ErrorDTO<T> error;

    public ErrorResponse(T object, String message) {
        error = new ErrorDTO<T>(object, message);
    }


    public ErrorDTO<T> getError() {
        return error;
    }

    public void setError(ErrorDTO<T> error) {
        this.error = error;
    }
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorDTO<T> implements java.io.Serializable {
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T body;
    private String message;

    public ErrorDTO(T body, String message) {
        this.body = body;
        this.message = message;
    }

    public T getBody() {
        return body;
    }

    public void setBody(T body) {
        this.body = body;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

Above POJOs can be written how ever you need, these are my way of representing.BTW you can construct according to your taste or project requirement.

Step 2 : Writing a Controller Advice.

@ControllerAdvice
public class CustomResponseAdvise implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (methodParameter.getContainingClass().isAnnotationPresent(RestController.class)) {

            if (methodParameter.getMethod().isAnnotationPresent(IgnoreResponseBinding.class) == false) {
                if ((!(o instanceof ErrorResponse)) && (!(o instanceof SuccessResponse))) {
                    SuccessResponse<Object> responseBody = new SuccessResponse<>(o);
                    return responseBody;
                }
            }
        }
        return o;
    }


}

This is an important part where the response is converted to desired standard when developer forgets to send the response in required format, gist of code is very simple if the method is part of REST Controller, is not the instance of SuccessResponse or ErrorResponse and also not annotated with IgnoreResponseBinding the magic happens.

Step 3 : Writing IgnoreResponseBinding annotation .

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

This is to ensure controller response is an exception, you do not want to standardising the response in situation like sending csv or downloading an file by sending blob data,or manually chunking large amount of data.

Step 4: Writing Exception Advice.

@ControllerAdvice
public class ExceptionAdvice {
    @Value("${drees.stacktrace}")
    boolean stackTrace;

   @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public ErrorResponse<List<StackTraceElement>> processAllError(Exception ex) {
        List<StackTraceElement> ele = null;
        if (stackTrace == true) {
            ele = Arrays.asList(ex.getStackTrace());
        }
        ErrorResponse<List<StackTraceElement>> response = new ErrorResponse<>(ele, ex.getMessage());
        return response;
    }
}    
    

So any exceptions or errors apart from success are standardised here.

Step 4: Writing Controller

I will not explain writing controller here, but make sure you don't send any primitive type data type, so assume your developer writes the controller function with this method signature `public ResponsePOJO functionName()`   controller advice will automatically converts to `SuccessDTO<ResponsePOJO>`.