Spring Boot API Response External Configuration File

Spring Boot API Response Design

Best Practice

Sample Response Structure

 
{
    "timestamp": "2024-06-23T18:25:43.511Z",
    "status": 200,
    "message": "Success",
    "data": {
        "id": 123,
        "name": "John Doe",
        "email": "john.doe@example.com"
    },
    "errors": []
}
   

Create a Response Wrapper Class


import java.time.LocalDateTime;
import java.util.List;

public class ApiResponse<T> {
    private LocalDateTime timestamp;
    private int status;
    private String message;
    private T data;
    private List<String> errors;

    public ApiResponse() {
        this.timestamp = LocalDateTime.now();
    }

    public ApiResponse(int status, String message, T data, List<String> errors) {
        this();
        this.status = status;
        this.message = message;
        this.data = data;
        this.errors = errors;
    }

    // Getters and Setters
}
    

Controller Example


import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;

@RestController
@RequestMapping("/api")
public class UserController {

    @GetMapping("/user")
    public ResponseEntity<ApiResponse<User>> getUser() {
        User user = new User(123, "John Doe", "john.doe@example.com");
        ApiResponse<User> response = new ApiResponse<>(
                HttpStatus.OK.value(),
                "Success",
                user,
                Collections.emptyList()
        );
        return new ResponseEntity<>(response, HttpStatus.OK);
    }
}
    

User Model Example


public class User {
    private int id;
    private String name;
    private String email;

    public User(int id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    // Getters and Setters
}
    

Examples Using ApiResponse

Creating a New Resource (POST)


@PostMapping("/user")
public ResponseEntity<ApiResponse<User>> createUser(@RequestBody User user) {
    // Assume userService.saveUser(user) saves the user and returns the saved user object
    User savedUser = userService.saveUser(user);
    ApiResponse<User> response = new ApiResponse<>(
            HttpStatus.CREATED.value(),
            "User created successfully",
            savedUser,
            Collections.emptyList()
    );
    return new ResponseEntity<>(response, HttpStatus.CREATED);
}
    

Handling Validation Errors


@PostMapping("/user")
public ResponseEntity<ApiResponse<User>> createUser(@Valid @RequestBody User user, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        List<String> errors = bindingResult.getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.toList());
        ApiResponse<User> response = new ApiResponse<>(
                HttpStatus.BAD_REQUEST.value(),
                "Validation failed",
                null,
                errors
        );
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }
    
    User savedUser = userService.saveUser(user);
    ApiResponse<User> response = new ApiResponse<>(
            HttpStatus.CREATED.value(),
            "User created successfully",
            savedUser,
            Collections.emptyList()
    );
    return new ResponseEntity<>(response, HttpStatus.CREATED);
}
    

Retrieving a Resource (GET)


@GetMapping("/user/{id}")
public ResponseEntity<ApiResponse<User>> getUserById(@PathVariable int id) {
    User user = userService.findUserById(id);
    if (user == null) {
        ApiResponse<User> response = new ApiResponse<>(
                HttpStatus.NOT_FOUND.value(),
                "User not found",
                null,
                Collections.singletonList("User with ID " + id + " not found")
        );
        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
    }
    
    ApiResponse<User> response = new ApiResponse<>(
            HttpStatus.OK.value(),
            "User retrieved successfully",
            user,
            Collections.emptyList()
    );
    return new ResponseEntity<>(response, HttpStatus.OK);
}
    

Updating a Resource (PUT)


@PutMapping("/user/{id}")
public ResponseEntity<ApiResponse<User>> updateUser(@PathVariable int id, @RequestBody User user) {
    User updatedUser = userService.updateUser(id, user);
    if (updatedUser == null) {
        ApiResponse<User> response = new ApiResponse<>(
                HttpStatus.NOT_FOUND.value(),
                "User not found",
                null,
                Collections.singletonList("User with ID " + id + " not found")
        );
        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
    }
    
    ApiResponse<User> response = new ApiResponse<>(
            HttpStatus.OK.value(),
            "User updated successfully",
            updatedUser,
            Collections.emptyList()
    );
    return new ResponseEntity<>(response, HttpStatus.OK);
}
    

Deleting a Resource (DELETE)


@DeleteMapping("/user/{id}")
public ResponseEntity<ApiResponse<Void>> deleteUser(@PathVariable int id) {
    boolean isDeleted = userService.deleteUser(id);
    if (!isDeleted) {
        ApiResponse<Void> response = new ApiResponse<>(
                HttpStatus.NOT_FOUND.value(),
                "User not found",
                null,
                Collections.singletonList("User with ID " + id + " not found")
        );
        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
    }
    
    ApiResponse<Void> response = new ApiResponse<>(
            HttpStatus.NO_CONTENT.value(),
            "User deleted successfully",
            null,
            Collections.emptyList()
    );
    return new ResponseEntity<>(response, HttpStatus.NO_CONTENT);
}
    

Returning a List of Users


@GetMapping("/users")
public ResponseEntity<ApiResponse<List<User>>> getAllUsers() {
    List<User> users = userService.findAllUsers();
    ApiResponse<List<User>> response = new ApiResponse<>(
            HttpStatus.OK.value(),
            "Users retrieved successfully",
            users,
            Collections.emptyList()
    );
    return new ResponseEntity<>(response, HttpStatus.OK);
}
    

Error Handling in ApiResponse

Global Exception Handler Class


import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

import java.util.Collections;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleAllExceptions(Exception ex, WebRequest request) {
        ApiResponse<Void> response = new ApiResponse<>(
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                "An internal server error occurred",
                null,
                Collections.singletonList(ex.getMessage())
        );
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    // You can add more methods to handle specific exceptions here
}
    

Modify Your Controllers to Use the Global Exception Handler


import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.List;

@RestController
@RequestMapping("/api")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/users")
    public ResponseEntity<ApiResponse<List<User>>> getAllUsers() {
        List<User> users = userService.findAllUsers();
        ApiResponse<List<User>> response = new ApiResponse<>(
                HttpStatus.OK.value(),
                "Users retrieved successfully",
                users,
                Collections.emptyList()
        );
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    // Other controller methods...
}
    

Testing the Global Exception Handler

To test this, you can introduce a bug or throw an exception in your service or controller method. For example, modify the findAllUsers method to throw an exception:


public List<User> findAllUsers() {
    throw new RuntimeException("Simulated server error");
}
    

When you make a request to /api/users, the global exception handler will catch the exception and return a standardized error response.

Extending the Global Exception Handler

You can add more methods to handle specific exceptions, such as ResourceNotFoundException, ValidationException, etc.


@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
    ApiResponse<Void> response = new ApiResponse<>(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            null,
            Collections.singletonList(ex.getMessage())
    );
    return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}