Skip to main content
Advertisement

3.5 Designing a Global Common Response Structure

1. Why Do We Need a Common Response Structure?

As you build more APIs, each controller can end up returning data in wildly different shapes.

// Controller A response
{ "id": 1, "name": "John" }

// Controller B response (on error)
{ "message": "User not found." }

// Controller C response (list query)
[{ "id": 1 }, { "id": 2 }]

When response formats are inconsistent, frontend developers must interpret a different structure for every endpoint, and error-handling logic must be written differently for each case — making collaboration unnecessarily difficult.

By introducing a Common Response Wrapper, all API responses are delivered inside a ** consistent envelope**, regardless of the endpoint.


2. Designing the Common Response Class

public class ApiResponse<T> {

private boolean success; // Whether the request succeeded
private String message; // Human-readable result message
private T data; // Actual response data (generic)

// Private constructor — only accessible via static factory methods
private ApiResponse(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
}

// ✅ Factory method for successful responses
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, "Request processed successfully.", data);
}

public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(true, message, data);
}

// ✅ Factory method for failed responses
public static <T> ApiResponse<T> fail(String message) {
return new ApiResponse<>(false, message, null);
}

// Getters omitted (use Lombok @Getter or write manually)
}

3. Applying to Controllers

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

@GetMapping("/{id}")
public ResponseEntity<ApiResponse<UserResponseDto>> getUser(@PathVariable Long id) {
UserResponseDto user = userService.findById(id);

if (user == null) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.fail("No user found with the given ID."));
}

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

@PostMapping
public ResponseEntity<ApiResponse<UserResponseDto>> createUser(
@RequestBody UserCreateRequestDto request) {
UserResponseDto created = userService.create(request);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(ApiResponse.success("User created successfully.", created));
}
}

4. Actual JSON Response Examples

On success:

{
"success": true,
"message": "Request processed successfully.",
"data": {
"id": 1,
"name": "John",
"totalPoints": 1000
}
}

On failure:

{
"success": false,
"message": "No user found with the given ID.",
"data": null
}

5. Production-Ready Version with Lombok

In real projects, Lombok annotations like @Getter, @Builder, and @AllArgsConstructor keep the code much more concise.

import lombok.Getter;

@Getter
public class ApiResponse<T> {

private final boolean success;
private final String message;
private final T data;

private ApiResponse(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
}

public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, "OK", data);
}

public static <T> ApiResponse<T> fail(String message) {
return new ApiResponse<>(false, message, null);
}
}

Key Takeaway: A common response class ensures your entire team builds to a consistent API contract. The frontend always knows to look for success, message, and data — three fields, no surprises. This pattern is a foundational building block in every production-grade Spring Boot project.

Advertisement