0
\$\begingroup\$

I am working on a quite a big Spring Boot web service and I wanted a standardized and simplified way to handle responses and exceptions by following fluent API design.

So this is what I did and it works as I expected.

For handling responses, first i created an interface like this,

public interface BaseResponse {
    public short code(); //holds API specific code
    public HttpStatus httpStatus(); // holds corresponding HTTP status code
}

And then i have created an enum which implements above BaseResponse interface, All the response codes are defined here.

public enum ResponseType implements BaseResponse {
    SUCCESS((short) 10000, HttpStatus.OK),
    INVALID_FILE_TYPE((short) 10001, HttpStatus.BAD_REQUEST),
    //rest...
}

Here's the response object (ApiResponse) definition,

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse {
    //holds http status code
    @JsonProperty("httpStatus")
    private short httpStatus;

    //holds API response code
    @JsonProperty("code")
    private short code;

    //holds data return from API
    @JsonProperty("data")
    private Map<String, Object> data;

    //holds errors return from API, if error response
    @JsonProperty("error")
    private Map<String, Object> error;

    public static ErrorResponseBuilder error(ApiError err) {
        return new ErrorResponseBuilder(err);
    }

    public static SuccessResponseBuilder success() {
        return new SuccessResponseBuilder();
    }

    /**
     *
     */
    public static class ErrorResponseBuilder {
        private ApiError err;
        public ErrorResponseBuilder(ApiError err) {
            this.err = err;
        }

        //returns error API response
        public ApiResponse build() {
            Map<String, Object> d = new HashMap<>();
            d.put("error", err.getMessage());
            short httpStatus = (short) err.getType().httpStatus().value();

            return new ApiResponse(
                    httpStatus,
                    err.getType().code(),
                    null,
                    d
            );
        }
    }

    /**
     *
     */
    public static class SuccessResponseBuilder {
        private Map<String, Object> pd;
        public SuccessResponseBuilder() {
            pd = new HashMap<>();
        }

        public SuccessResponseBuilder attr(String key, Object value) {
            pd.put(key, value);
            return this;
        }

        //returns success API response
        public ApiResponse build(BaseResponse type) {
            short httpStatus = (short) type.httpStatus().value();
            return new ApiResponse(
                    httpStatus,
                    type.code(),
                    pd,
                    null
            );
        }
    }

    //this is for logging
    @Override
    public String toString() {
        return new StringBuilder()
                .append("httpStatus:").append(getHttpStatus()).append(", ")
                .append("code:").append(getCode()).append(", ")
                .append("data:").append(getData()).append(", ")
                .append("error:").append(getError())
                .toString();
    }
}

Then i use this object to create responses like below,

Success response

//call `@Service` method, and get result of it
String fileId = this.fileHandlerService.storeFile(file);
//create success response
ApiResponse apiRes = ApiResponse.success()
    .attr("fileId", fileId)
    .build(ResponseType.SUCCESS);
//return HTTP response with 'apiRes' in body
return ResponseEntity.status(apiRes.getHttpStatus()).body(apiRes);

This results in below json response,

{
    "httpStatus": 200,
    "code": 10000,
    "data": {
        "fileId": "a251cb619dc145f684b9682bcb503a5c"
    }
}

Error response

To handle exception responses, i created a Runtime Exception called ApiException. This is being used to handle every exceptional cases. For example: user-not-found, inactive-user, invalid-file-type, etc...

ApiError class was created for hold API error details,

public class ApiError {
    private BaseResponse type;
    private String message;
    private String debugMessage;
    private Exception exception;
}

ApiException

public class ApiException extends RuntimeException {
    private ApiError apiError;
    //boilerplate code omitted
}

Use this exception like this,

throw new ApiException(ApiError.builder()
    .type(ResponseType.INVALID_FILE_TYPE)
    .message("Invalid file, please upload ms-excel file")
    .debugMessage("expected file extension:xlsx, but found:" + fileExt)
    .build()
);

This results in below json response,

{
    "httpStatus": 400,
    "code": 10001,
    "error": {
        "error": "Invalid file, please upload ms-excel file"
    }
}

This is the mechanism used every where in the web service.

  • Is this a good way to handle response ?
  • How can i improve it?
\$\endgroup\$
4
  • \$\begingroup\$ All the json responses are composed of two equal named fields and a third field (named error in the error response and data in the success response) ? \$\endgroup\$ Sep 29, 2021 at 20:46
  • \$\begingroup\$ @dariosicily yes, they do, in data field of success response, i can add objects, arrays, strings etc... \$\endgroup\$
    – benjamin c
    Sep 30, 2021 at 7:16
  • \$\begingroup\$ One way to avoid duplicity of maps would be build a custom serializer but jackson-databind package has to be added in the starting spring boot configuration, have you it in your configuration ? \$\endgroup\$ Sep 30, 2021 at 15:57
  • 1
    \$\begingroup\$ there is a whole article Error Handling for REST with Spring addressing your concern... \$\endgroup\$ Oct 1, 2021 at 6:00

1 Answer 1

2
\$\begingroup\$

As you are using Spring Boot, the best way to handle you response is to add a RestControllerAdvice.

A few rules to follow :

  1. Create custom POJO to represent your custom message (you did it already)

  2. Return directly the response message in your controller, by setting the HttpStatus, here 2XX. You don't have to define your own HttpStatus, Spring Boot provides it already

  3. Intercept your exceptions in your RestControllerAdvice. It allows you to return the custom message you want

  4. Avoid sending the full exception message to your user. Send a customized clear and simple message. Log your exception full message for internal investigations instead

  5. Choose the right HttpStatus code for differents situations in your RestControllerAdvice :

    Parameters null or value not set : 400 => Bad request

    Returned value not found (record or list empty) : 404 => Not found

    Exception from server (database error, network error etc.) : 500 => Internal server error

Following this, you should be able to change your code to follow those few rules.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.