Skip to main content

Handling External API Responses in Spring Boot: A Practical Guide

· 3 min read

When building backend applications, you often need to consume external APIs. One common scenario is dealing with APIs that return either success data or errors, sometimes inconsistently. This guide will walk you step by step through handling such responses cleanly in a Spring Boot application.


1. Problem Statement

Imagine you are integrating with an external KYB (Know Your Business) API. The API can return two types of responses:

Success response:

{
"data": {
"businessId": "123",
"status": "verified"
}
}

Error response:

{
"errors": [
{ "type": "not_found", "message": "Business profile not found" }
]
}

Sometimes, the API returns a mix you don’t want:

{
"data": null,
"errors": [{ "type": "not_found", "message": "Business profile not found" }]
}

Returning both data and errors together is confusing and not ideal.


2. Why Mapping Directly to a Single DTO Fails

If you map the response directly to a single DTO like KybResponse, you may end up with:

public class KybResponse {
private Data data;
private List<ErrorResponse> errors;
}

If data is null but errors exist, your controller might accidentally return a response with both data and errors, which is misleading for clients.


3. Designing Clean Response Models

To solve this, create two clear models:

Error model:

public class ErrorResponse {
private String type;
private String message;

// getters & setters
}

API response model:

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class KybResponse {
private Data data;
private List<ErrorResponse> errors;

// getters & setters
}

@JsonInclude(JsonInclude.Include.NON_NULL) ensures null fields are not serialized, preventing responses from containing both data and errors.


4. Service Layer Responsibility

The service layer should:

  1. Call the external API.
  2. Inspect the response.
  3. Throw exceptions if errors exist.
  4. Return data only if successful.

Example:

@Service
public class KybService {

private final RestTemplate restTemplate;

public KybService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}

public Data getBusiness(String businessId) {
KybResponse response = restTemplate.getForObject(
"https://external-api.com/kyb/" + businessId,
KybResponse.class
);

if (response.getErrors() != null && !response.getErrors().isEmpty()) {
throw new KybException(response.getErrors());
}

return response.getData();
}
}

5. Custom Exception Implementation

Define a custom exception to encapsulate API errors:

public class KybException extends RuntimeException {
private final List<ErrorResponse> errors;

public KybException(List<ErrorResponse> errors) {
super("KYB API returned errors");
this.errors = errors;
}

public List<ErrorResponse> getErrors() {
return errors;
}
}

6. Exception Handling Approaches

  1. Try-catch in controller – works but clutters the controller, not recommended.
  2. @ExceptionHandler in controller – better, but still limited.
  3. @RestControllerAdvice – centralized global handling, recommended.

7. Global Exception Handler

Implement a global handler for KybException:

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(KybException.class)
public ResponseEntity<Map<String, List<ErrorResponse>>> handleKybException(KybException ex) {
Map<String, List<ErrorResponse>> errorBody = Map.of("errors", ex.getErrors());
return ResponseEntity.badRequest().body(errorBody);
}
}

This keeps controllers clean and ensures consistent error responses.


8. Final Controller

Now the controller only focuses on successful responses:

@RestController
@RequestMapping("/kyb")
public class KybController {

private final KybService kybService;

public KybController(KybService kybService) {
this.kybService = kybService;
}

@GetMapping("/{businessId}")
public Data getKyb(@PathVariable String businessId) {
return kybService.getBusiness(businessId);
}
}

No error handling logic is inside the controller anymore.


9. Final API Outputs

Success response:

{
"businessId": "123",
"status": "verified"
}

Error response:

{
"errors": [
{ "type": "not_found", "message": "Business profile not found" }
]
}

Notice there’s no overlap of data and errors.


10. Best Practices

  • Never return data and errors together.
  • Keep controllers thin, services smart.
  • Centralize exception handling.
  • Use a consistent API response format.

11. Optional Improvements

  • Add timestamp, status, and path in error responses.
  • Map different error types to proper HTTP status codes.
  • Introduce a generic ApiResponse<T> for more consistency.

Example enhanced error response:

{
"timestamp": "2026-03-17T12:30:00Z",
"status": 400,
"errors": [
{ "type": "not_found", "message": "Business profile not found" }
],
"path": "/kyb/123"
}