How to Structure API Response in Spring Boot

Why Care About API Response Structure?

Before diving into the nitty-gritty, let’s address why having a well-structured API response is crucial. A consistent response structure:

  • Improves client-side error handling: Your frontend team will thank you.
  • Enhances readability and maintainability: Future you (or your team) will appreciate the clarity.
  • Simplifies debugging and logging: Spot issues quickly and efficiently.

What Makes a Good API Response?

A well-structured API response should be:

  • Consistent: Uniform format across different endpoints.
  • Informative: Includes relevant data, messages, status codes, and error codes.
  • Simple: Easy to parse and understand.

Crafting the Ideal Response Structure

1. Define a Standard Response Format

Start by creating a standard response format that all your APIs will follow. Here’s a simple and effective format:

public class ApiResponse<T> {
    private boolean success;
    private String message;
    private T data;
    private List<String> errors;
    private int errorCode;
    private long timestamp;  // Adding timestamp for tracking
    private String path;     // Adding path to identify the API endpoint

    // Constructors, getters, and setters
}

Understanding Each Field:

1. success:

  • Typeboolean
  • Description: Indicates whether the API call was successful or not.
  • Why Use It: Quickly determines the outcome of the request, simplifying client-side logic.

2. message:

  • TypeString
  • Description: Provides a human-readable message about the result of the API call.
  • Why Use It: Helps in giving contextual feedback to the client, useful for both success and error scenarios.

3. data:

  • TypeT
  • Description: Contains the payload of the response, which can be any data type.
  • Why Use It: Delivers the actual data requested by the client.

4. errors:

  • TypeList<String>
  • Description: A list of error messages if the API call was unsuccessful.
  • Why Use It: Provides detailed information about what went wrong, useful for debugging and user feedback.

5. errorCode:

  • Typeint
  • Description: A specific code representing the error type.
  • Why Use It: Helps in categorizing errors programmatically and responding appropriately.

6. timestamp:

  • Typelong
  • Description: The timestamp when the response was generated.
  • Why Use It: Useful for logging and tracking the timing of responses, which can aid in debugging and monitoring.

7. path:

  • TypeString
  • Description: The API endpoint that was called.
  • Why Use It: Helps identify which API endpoint generated the response, useful for debugging and logging.

2. Create Utility Methods for Responses

To keep things DRY (Don’t Repeat Yourself), let’s create utility methods to generate responses. This ensures consistency and reduces boilerplate code.

public class ResponseUtil {

    public static <T> ApiResponse<T> success(T data, String message, String path) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setSuccess(true);
        response.setMessage(message);
        response.setData(data);
        response.setErrors(null);
        response.setErrorCode(0); // No error
        response.setTimestamp(System.currentTimeMillis());
        response.setPath(path);
        return response;
    }

    public static <T> ApiResponse<T> error(List<String> errors, String message, int errorCode, String path) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setSuccess(false);
        response.setMessage(message);
        response.setData(null);
        response.setErrors(errors);
        response.setErrorCode(errorCode);
        response.setTimestamp(System.currentTimeMillis());
        response.setPath(path);
        return response;
    }

    public static <T> ApiResponse<T> error(String error, String message, int errorCode, String path) {
        return error(Arrays.asList(error), message, errorCode, path);
    }
}

3. Implement Global Exception Handling

Handling exceptions globally ensures that any unhandled errors are caught and returned in your standard response format. Use @ControllerAdvice and @ExceptionHandler annotations for this.


@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleException(HttpServletRequest request, Exception ex) {
        List<String> errors = Arrays.asList(ex.getMessage());
        ApiResponse<Void> response = ResponseUtil.error(errors, "An error occurred", 1000, request.getRequestURI()); // General error
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleResourceNotFoundException(HttpServletRequest request, ResourceNotFoundException ex) {
        ApiResponse<Void> response = ResponseUtil.error(ex.getMessage(), "Resource not found", 1001, request.getRequestURI()); // Resource not found error
        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ApiResponse<Void>> handleValidationException(HttpServletRequest request, ValidationException ex) {
        ApiResponse<Void> response = ResponseUtil.error(ex.getErrors(), "Validation failed", 1002, request.getRequestURI()); // Validation error
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    // Handle other specific exceptions similarly
}

4. Use the Response Format in Your Controllers

Now, let’s use our standardized response structure in a sample controller.

@RestController
@RequestMapping("/api/products")
public class ProductController {

    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<Product>> getProductById(@PathVariable Long id, HttpServletRequest request) {
        // Fetch product by id (dummy code)
        Product product = productService.findById(id);
        if (product == null) {
            throw new ResourceNotFoundException("Product not found with id " + id);
        }
        ApiResponse<Product> response = ResponseUtil.success(product, "Product fetched successfully", request.getRequestURI());
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @PostMapping
    public ResponseEntity<ApiResponse<Product>> createProduct(@RequestBody Product product, HttpServletRequest request) {
        // Create new product (dummy code)
        Product createdProduct = productService.save(product);
        ApiResponse<Product> response = ResponseUtil.success(createdProduct, "Product created successfully", request.getRequestURI());
        return new ResponseEntity<>(response, HttpStatus.CREATED);
    }

    // More endpoints...
}

Common Error Codes

Here’s a quick reference for common error codes you might use ( These are just examples, you can customise according to your project ):

  • 1000: General error
  • 1001: Resource not found
  • 1002: Validation failed
  • 1003: Unauthorized access
  • 1004: Forbidden access
  • 1005: Conflict (e.g., duplicate resource)

These error codes can be maintained in both the frontend and backend to ensure consistent error handling and to provide meaningful feedback to users. By standardizing error codes, you simplify the process of handling errors across different layers of your application, making it easier to manage and debug issues.

Post a Comment

Previous Post Next Post