Best Practices for API Request and Response Handlings in Spring Boot Development
All APIs of every Orginization. maintaining a standardized request and response format across various endpoints is not just good practice; it’s a crucial element in crafting robust and scalable APIs. One effective way to achieve this consistency is by implementing a API Request
and API Response
for every endpoint.
In this article, we’ll bring up Spring Boot applications to explore the significance of this approach and how it can streamline your development process.
Assume we have an application that does the CRUD operations which is written in spring boot.
Default API Response class
In this I’ll create three classes that can build response with success and fail.
public class ResponseErrorVo {
private String code;
private String message;
private String description;
public ResponseErrorVo(String code, String message) {
this.code = code;
this.message = message;
this.description = message;
}
public ResponseErrorVo(String code, String message, String description) {
this.code = code;
this.message = message;
this.description = description;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getDescription() {
return ObjectUtils.isEmpty(description) ? message : description;
}
public void setDescription(String description) {
this.description = description;
}
}
public class ResponseVO<T> {
private String status;
private String result;
private ResponseErrorVo error;
private T data;
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
public T getData() {
return data;
}
public ResponseErrorVo getError() {
return error;
}
public void setError(ResponseErrorVo error) {
this.error = error;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public void setData(T data) {
this.data = data;
}
}
public class ResponseVOBuilder<T> {
private final ResponseVO<T> responseVO = new ResponseVO<>();
private ResponseVOBuilder<T> result(String result) {
responseVO.setResult(result);
return this;
}
private ResponseVOBuilder<T> status(String status) {
responseVO.setStatus(status);
return this;
}
public ResponseVOBuilder<T> success() {
return new ResponseVOBuilder<T>().result("Succeed").status("200");
}
public ResponseVOBuilder<T> fail() {
return new ResponseVOBuilder<T>().result("Failed").status("500");
}
public ResponseVOBuilder<T> error(ResponseErrorVo error) {
responseVO.setError(error);
responseVO.setResult("Failed");
responseVO.setStatus("500");
return this;
}
public ResponseVOBuilder<T> addData(final T body) {
responseVO.setData(body);
responseVO.setResult("Succeeded");
responseVO.setStatus("200");
return this;
}
public ResponseVO<T> build() {
return responseVO;
}
}
Sample success response
curl --location 'localhost:8080/api/users/5'
{
"status": "200",
"result": "Succeeded",
"data": {
"id": "1",
"username": "admin",
"status": "ACTIVE"
}
}
Sample error response: no record exist with id 50, no URL found and error on String convert to number.
curl --location 'localhost:8080/api/users/50'
{
"status": "500",
"result": "Failed",
"error": {
"code": "E0002",
"message": "Record not found.",
"description": "Record not found."
}
}
curl --location 'localhost:8080/api/test'
{
"status": "404",
"result": "Failed",
"error": {
"code": "E0404",
"description": "Not Found",
"message": "Not Found"
}
}
curl --location 'localhost:8080/api/users/5s'
{
"status": "500",
"result": "Failed",
"error": {
"code": "E0001",
"message": "General exception error.",
"description": "For input string: \"5s\""
}
}
Default Class for Request and Response with pagination
In this I’ll create two classes that can build response with success and fail and contain page request.
public class RequestPageableVO {
public RequestPageableVO() {
}
public RequestPageableVO(Integer page, Integer rpp) {
this.page = page;
this.rpp = rpp;
}
public static final Integer DEFAULT_PAGE = NumberUtils.INTEGER_ONE;
public static final Integer DEFAULT_RPP = Integer.valueOf(10);
public Integer getPage() {
return page;
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getRpp() {
return rpp;
}
public void setRpp(Integer rpp) {
this.rpp = rpp;
}
private Integer page = DEFAULT_PAGE;
private Integer rpp = DEFAULT_RPP;
public int getOffset() {
return (getPage() - 1) * getRpp();
}
public final int getNumberOfRows() {
return getRpp();
}
}
public class ResponsePageableVO<T> {
protected static final long DEFAULT_RECORDS = NumberUtils.LONG_ZERO;
private Long records = DEFAULT_RECORDS;
private List<T> items;
private Integer pages;
private Integer page;
public List<T> getItems() {
return items;
}
public void setItems(List<T> items) {
this.items = items;
}
public Integer getPages() {
return pages;
}
public void setPages(Integer pages) {
this.pages = pages;
}
public Integer getPage() {
return page;
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getRecordFrom() {
return recordFrom;
}
public void setRecordFrom(Integer recordFrom) {
this.recordFrom = recordFrom;
}
public Integer getRecordTo() {
return recordTo;
}
public void setRecordTo(Integer recordTo) {
this.recordTo = recordTo;
}
@JsonProperty("record_from")
private Integer recordFrom;
@JsonProperty("record_to")
private Integer recordTo;
public Long getRecords() {
return records;
}
public void setRecords(Long records) {
this.records = records;
}
public ResponsePageableVO(int records, List<T> items, RequestPageableVO pageable) {
this((long) records, items, pageable);
}
public ResponsePageableVO(long records, List<T> items, RequestPageableVO pageable) {
this.records = records;
this.items = items;
this.pages = (int) Math.ceil((double) this.records / pageable.getRpp());
this.page = pageable.getPage();
if (ObjectUtils.isEmpty(this.items)) {
this.recordFrom = 1;
this.recordTo = Math.toIntExact(this.records);
} else {
this.recordFrom = (this.page * pageable.getRpp()) - pageable.getRpp() + 1;
this.recordTo = (int) (this.page.equals(this.pages) ? this.records : this.page * pageable.getRpp());
}
}
}
Sample API data response with page(default value page=1 and rpp=10)
*RPP = Row Per Page
curl --location 'localhost:8080/api/users
{
"status": "200",
"result": "Succeeded",
"data": {
"records": 9,
"items": [
{
"id": "3",
"username": "admin",
"status": "ACTIVE"
},
{
"id": "4",
"username": "peter",
"status": "ACTIVE"
},
{
"id": "5",
"username": "jonh",
"status": "ACTIVE"
},
{
"id": "6",
"username": "janu",
"status": "ACTIVE"
},
{
"id": "7",
"username": "bopha",
"status": "ACTIVE"
},
{
"id": "8",
"username": "kolap",
"status": "ACTIVE"
},
{
"id": "9",
"username": "jat",
"status": "ACTIVE"
},
{
"id": "10",
"username": "fank",
"status": "ACTIVE"
},
{
"id": "11",
"username": "yuseph",
"status": "ACTIVE"
}
],
"pages": 1,
"page": 1,
"record_from": 1,
"record_to": 9
}
}
curl --location 'localhost:8080/api/users?rpp=5&page=2'
{
"status": "200",
"result": "Succeeded",
"data": {
"records": 9,
"items": [
{
"id": "8",
"username": "kolap",
"status": "ACTIVE"
},
{
"id": "9",
"username": "jat",
"status": "ACTIVE"
},
{
"id": "10",
"username": "fank",
"status": "ACTIVE"
},
{
"id": "11",
"username": "yuseph",
"status": "ACTIVE"
}
],
"pages": 2,
"page": 2,
"record_from": 6,
"record_to": 9
}
}
From controller of these same APIs:
@RestController
@RequestMapping("api/users")
@AllArgsConstructor
public class UserController {
private final UserService service;
@GetMapping("{id}")
public ResponseVO<UserResponseVo> getUserById(@PathVariable String id) {
UserDto user = this.service.findById(id);
return new ResponseVOBuilder<UserResponseVo>().addData(UserMapper.INSTANCE.dtoToVo(user)).build();
}
@GetMapping
public ResponseVO<ResponsePageableVO<UserResponseVo>> getUsers(RequestPageableVO request) {
Page<UserDto> page = this.service.findByUsers(request);
List<UserResponseVo> users = UserMapper.INSTANCE.dtoToVoList(page.getContent());
ResponsePageableVO<UserResponseVo> responseVo = new ResponsePageableVO<>(page.getTotalElements(), users, request);
return new ResponseVOBuilder<ResponsePageableVO<UserResponseVo>>().addData(responseVo).build();
}
@PostMapping
public ResponseVO<UserResponseVo> save(@RequestBody UserRequestVo vo) {
UserDto dto = UserMapper.INSTANCE.voToDto(vo);
this.service.save(dto);
UserResponseVo responseVo = UserMapper.INSTANCE.dtoToVo(dto);
return new ResponseVOBuilder<UserResponseVo>().addData(responseVo).build();
}
}
Error handling:
To handle all error from API I created two classes below:
@RestControllerAdvice
@Slf4j
public class RestExceptionHandlerAdvice {
@SneakyThrows
@ExceptionHandler(Exception.class)
public ResponseEntity<ResponseVO> responseException(Exception ex) {
log.error("Exception {}", ex.getMessage(), ex);
return new ResponseEntity<>(new ResponseVOBuilder<>().fail().error(new ResponseErrorVo(BizErrorCode.E0001.getValue(), BizErrorCode.E0001.getDescription(), ex.getLocalizedMessage())).build(), HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(BizException.class)
public ResponseEntity<ResponseVO> responseBizException(BizException ex) {
log.error("BizException {}", ex.getMessage(), ex);
return new ResponseEntity<>(new ResponseVOBuilder<>().fail().error(new ResponseErrorVo(ex.getError().getValue(), ex.getError().getDescription())).build(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Configuration
@Slf4j
public class CustomErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> map = super.getErrorAttributes(webRequest, options);
int status = (int) map.get("status");
String message = (String) map.get("error");
Map<String, Object> response = new HashMap<>();
Map<String, Object> error = new HashMap<>();
error.putIfAbsent("code", String.format("E0%s", status));
error.putIfAbsent("message", message);
error.putIfAbsent("description", message);
response.putIfAbsent("status", String.valueOf(status));
response.putIfAbsent("result", "Failed");
response.putIfAbsent("error", error);
response.putIfAbsent("data", null);
return response;
}
}
Remove null value on ObjectMapper:
@Configuration
public class AppConfig {
@Bean
ObjectMapper mapper() {
return new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
}
To summarize; In this article, I tried to explain how to create default and standard request and response for API. You can access the source code via the github link above.
Thank you for reading until the end. Before you go:
- Please consider clapping and following the writer! 👏
- Visit phsophea101.github.io to find out more about how we are democratizing free programming education around the world.
- If you like my work and would like to buy me a coffee please go this buymeacoffee link. Thank you :)
Good luck!!