Skip to main content

Spring REST Response Handling

When building RESTful APIs with Spring, properly handling responses is crucial for creating robust and user-friendly applications. This guide will walk you through the essentials of Spring REST response handling, from basic concepts to advanced techniques.

Introduction to Spring REST Response Handling

Response handling refers to how your Spring application formats and returns data to clients that consume your API. A well-designed response should:

  • Return appropriate HTTP status codes
  • Provide consistent response structures
  • Handle errors gracefully
  • Support different content types
  • Include relevant metadata when needed

Spring provides several mechanisms to help you control how responses are generated and returned to clients.

HTTP Status Codes

HTTP status codes are an essential part of RESTful communication. Spring makes it easy to set appropriate status codes in your responses.

Common HTTP Status Codes

Status CodeMeaningCommon Use Case
200OKSuccessful request
201CreatedResource successfully created
204No ContentSuccessful request with no response body
400Bad RequestInvalid input
401UnauthorizedAuthentication required
403ForbiddenAuthenticated but not authorized
404Not FoundResource not found
500Internal Server ErrorServer-side error

Basic Response Handling

Let's start with the simplest way to return responses from a Spring REST controller.

Returning Objects Directly

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

@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
// Fetch product from service
Product product = productService.findById(id);
return product; // Spring automatically converts this to JSON
}
}

In this example, Spring automatically:

  1. Converts the Product object to JSON (using Jackson by default)
  2. Sets content type to application/json
  3. Returns HTTP status 200 (OK)

Specifying Return Status

To specify a different status code, you can use the @ResponseStatus annotation:

java
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Product createProduct(@RequestBody Product product) {
return productService.save(product);
}

Using ResponseEntity

For more control over the response, use Spring's ResponseEntity class. It allows you to customize:

  • HTTP status code
  • Response headers
  • Response body

Basic ResponseEntity Example

java
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
Product product = productService.findById(id);

if (product != null) {
return ResponseEntity.ok(product);
} else {
return ResponseEntity.notFound().build();
}
}

ResponseEntity with Custom Headers

java
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
Product product = productService.findById(id);

return ResponseEntity.ok()
.header("Custom-Header", "value")
.body(product);
}

Building Complex Responses

The ResponseEntity builder pattern is flexible and readable:

java
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
Product savedProduct = productService.save(product);

URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(savedProduct.getId())
.toUri();

return ResponseEntity.created(location)
.body(savedProduct);
}

This example:

  1. Creates a new resource
  2. Builds a URI pointing to the new resource
  3. Returns status 201 (Created)
  4. Includes a Location header with the URI
  5. Returns the created resource in the body

Custom Response Wrappers

For consistent API responses, you might want to create a standard response structure. Here's an example of a custom response wrapper:

java
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private List<String> errors;

// Constructors, getters, and setters

public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setSuccess(true);
response.setData(data);
return response;
}

public static <T> ApiResponse<T> error(String message, List<String> errors) {
ApiResponse<T> response = new ApiResponse<>();
response.setSuccess(false);
response.setMessage(message);
response.setErrors(errors);
return response;
}
}

You can then use this wrapper in your controllers:

java
@GetMapping("/products")
public ResponseEntity<ApiResponse<List<Product>>> getAllProducts() {
List<Product> products = productService.findAll();
return ResponseEntity.ok(ApiResponse.success(products));
}

The resulting JSON would look like:

json
{
"success": true,
"message": null,
"data": [
{
"id": 1,
"name": "Laptop",
"price": 999.99
},
{
"id": 2,
"name": "Phone",
"price": 699.99
}
],
"errors": null
}

Content Negotiation

Spring supports content negotiation, allowing clients to request data in different formats.

Configuration

Add this to your application properties:

properties
spring.mvc.contentnegotiation.favor-parameter=true
spring.mvc.contentnegotiation.parameter-name=format

Controller Implementation

java
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(
@PathVariable Long id,
@RequestParam(required = false) String format
) {
Product product = productService.findById(id);

// Content negotiation happens automatically
return ResponseEntity.ok(product);
}

With this setup, clients can request different formats:

  • /api/products/1?format=json for JSON
  • /api/products/1?format=xml for XML (requires additional configuration)

Error Handling

Proper error handling is crucial for robust APIs. Spring provides several approaches:

1. Using @ExceptionHandler in Controllers

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

@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
Product product = productService.findById(id);
if (product == null) {
throw new ProductNotFoundException("Product not found with id: " + id);
}
return product;
}

@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleProductNotFound(ProductNotFoundException ex) {
ApiResponse<Void> response = ApiResponse.error(ex.getMessage(), Collections.emptyList());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}
}

2. Global Exception Handling with @ControllerAdvice

For application-wide exception handling:

java
@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleProductNotFound(ProductNotFoundException ex) {
ApiResponse<Void> response = ApiResponse.error(ex.getMessage(), Collections.emptyList());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception ex) {
ApiResponse<Void> response = ApiResponse.error("An unexpected error occurred",
Collections.singletonList(ex.getMessage()));
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Void>> handleValidationExceptions(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getAllErrors()
.stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.toList());

ApiResponse<Void> response = ApiResponse.error("Validation failed", errors);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
}

Custom Exception Classes

java
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(String message) {
super(message);
}
}

Pagination and Sorting

For APIs that return large collections, pagination is essential. Spring Data provides built-in support:

java
@GetMapping
public ResponseEntity<Page<Product>> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy
) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
Page<Product> productPage = productService.findAll(pageable);

return ResponseEntity.ok(productPage);
}

The response includes pagination metadata:

json
{
"content": [
{ "id": 1, "name": "Laptop", "price": 999.99 },
{ "id": 2, "name": "Phone", "price": 699.99 }
],
"pageable": {
"pageNumber": 0,
"pageSize": 10,
"sort": { "sorted": true, "unsorted": false, "empty": false }
},
"totalElements": 42,
"totalPages": 5,
"last": false,
"size": 10,
"number": 0,
"sort": { "sorted": true, "unsorted": false, "empty": false },
"numberOfElements": 10,
"first": true,
"empty": false
}

Real-world Example: E-commerce API

Let's put everything together in a more comprehensive example of a product API:

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

private final ProductService productService;

public ProductController(ProductService productService) {
this.productService = productService;
}

@GetMapping
public ResponseEntity<ApiResponse<Page<ProductDTO>>> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "name") String sortBy) {

Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
Page<ProductDTO> products = productService.findAll(pageable)
.map(this::convertToDTO);

return ResponseEntity.ok(ApiResponse.success(products));
}

@GetMapping("/{id}")
public ResponseEntity<ApiResponse<ProductDTO>> getProduct(@PathVariable Long id) {
return productService.findById(id)
.map(this::convertToDTO)
.map(ApiResponse::success)
.map(ResponseEntity::ok)
.orElseThrow(() -> new ProductNotFoundException("Product not found with id: " + id));
}

@PostMapping
public ResponseEntity<ApiResponse<ProductDTO>> createProduct(
@Valid @RequestBody ProductCreateRequest request) {

Product newProduct = productService.create(request);
ProductDTO dto = convertToDTO(newProduct);

URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(newProduct.getId())
.toUri();

return ResponseEntity
.created(location)
.body(ApiResponse.success(dto));
}

@PutMapping("/{id}")
public ResponseEntity<ApiResponse<ProductDTO>> updateProduct(
@PathVariable Long id,
@Valid @RequestBody ProductUpdateRequest request) {

Product updated = productService.update(id, request);
return ResponseEntity.ok(ApiResponse.success(convertToDTO(updated)));
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.delete(id);
return ResponseEntity.noContent().build();
}

private ProductDTO convertToDTO(Product product) {
// Convert entity to DTO
return new ProductDTO(
product.getId(),
product.getName(),
product.getDescription(),
product.getPrice(),
product.getCategory().getName(),
product.getImageUrl()
);
}
}

Summary

Spring REST response handling provides many tools to create well-designed APIs:

  1. Basic Responses: Return objects directly for simple cases
  2. @ResponseStatus: Annotation for specifying status codes
  3. ResponseEntity: For complete control over responses including status, headers, and body
  4. Custom Response Wrappers: Create consistent response formats
  5. Content Negotiation: Support multiple formats like JSON or XML
  6. Error Handling: Use @ExceptionHandler and @ControllerAdvice for graceful error responses
  7. Pagination: Handle large collections efficiently

Effective response handling makes your API more usable, predictable, and professional. By following these practices, you'll create APIs that clients can easily integrate with and that communicate clearly, especially when things go wrong.

Additional Resources

Exercises

  1. Create a REST controller with endpoints that demonstrate all major HTTP status codes (200, 201, 204, 400, 404, 500).
  2. Implement a custom response wrapper that includes fields for success status, data, error message, and timestamp.
  3. Create a global exception handler that handles at least three different types of exceptions.
  4. Extend the product API example to include filtering by price range and category.
  5. Implement content negotiation to support both JSON and XML formats for the product API.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)