← Back to list

Mastering Custom API Response in Java Spring Boot

When you start building APIs in Spring Boot, things feel simple at first. You return objects, and Spring automatically converts them into…

Jane Frances in MasteringBackend · 2026-05-07 08:01 · 1 claps · 3.6 min read
#api #java #spring-boot #api-response #backend-development
Open on Medium ↗
Wiki topics: GEN · Genomics & Sequencing 🌐 · Web Development

Mastering Custom API Response in Java Spring Boot

When you start building APIs in Spring Boot, things feel simple at first. You return objects, and Spring automatically converts them into JSON. But as your application grows, problems begin to appear:

  • Different APIs return different response formats
  • Error handling becomes messy
  • Frontend developers struggle to handle multiple structures
  • Debugging becomes time-consuming

This is where a Custom API Response structure becomes extremely important.

In this blog, we will deeply understand not just how to implement it, but also why each part exists and how it helps in real-world projects.

Problem with Default API Design

Let’s take a real scenario.

Case 1: Success API

{
  "id": 1,
  "name": "Ayush"
}

Case 2: Error API

{
  "message": "User not found"
}

Case 3: Validation Error

{
  "errors": [
    "Name is required",
    "Email is invalid"
  ]
}

What Breaks Here

Now the frontend must write logic like:

  • Check if ***id***exists → success
  • Check if ***message***exists → error
  • Check if ***errors***exist → validation

This creates unnecessary conditional handling across web, mobile, and third-party clients.

As APIs scale, inconsistent contracts increase maintenance costs.

Solution: Standard API Response

We fix this by defining a fixed structure:

{
  "data": {},
  "success": true,
  "message": "Request successful",
  "errors": null,
  "status": "OK",
  "timestamp": "2026-04-28T10:00:00"
}

Why Each Field Exists

  • data → Holds actual response (can be object, list, or null)
  • success → Quick boolean check (frontend friendly)
  • message → Human-readable message
  • errors → Detailed error info (useful for debugging)
  • status → HTTP status for clarity
  • timestamp → Helps in tracking and debugging issues

This makes your API predictable and easy to consume.

Step 1: StandardResponse Class

public class StandardResponse<T> {

    private T data;
    private boolean success;
    private String message;
    private Object errors;
    private HttpStatus status;
    private LocalDateTime timestamp;

    public StandardResponse(T data, boolean success, String message,
                            Object errors, HttpStatus status) {
        this.data = data;
        this.success = success;
        this.message = message;
        this.errors = errors;
        this.status = status;
        this.timestamp = LocalDateTime.now();
    }
}

Deep Explanation

  • Generic *<T>*
  • Makes the class reusable for any data type
  • Example: ***String, User, List<User>***
  • Object errors
  • Flexible → can store string, list, or map
  • Useful for validation errors
  • timestamp
  • Helps track when API was called
  • Useful in logs and debugging production issues

This class becomes the backbone of your API.

Step 2: ResponseBuilder (Why It Matters)

Without a builder, you would write this everywhere:

new StandardResponse<>(data, true, "Success", null, HttpStatus.OK);

This is repetitive and error-prone.

Solution:

public class ResponseBuilder {

    public static <T> StandardResponse<T> success(T data, String message) {
        return new StandardResponse<>(data, true, message, null, HttpStatus.OK);
    }

    public static <T> StandardResponse<T> error(String message, Object errors, HttpStatus status) {
        return new StandardResponse<>(null, false, message, errors, status);
    }
}

Why This Matters

  • Reduces code duplication
  • Improves readability
  • Central place to modify response logic
  • Makes your code cleaner
  • Supports cleaner architecture

Step 3: Service Layer

public StandardResponse<String> getUser() {
    String user = "Ayush";
    return ResponseBuilder.success(user, "User fetched successfully");
}

Why This Is Good Design

  • Business logic stays clean
  • No HTTP logic here
  • Only returns a structured response

This follows the Separation of Concerns

Step 4: Controller Layer

@GetMapping("/user")
public ResponseEntity<StandardResponse<String>> getUser() {
    return ResponseEntity.ok(userService.getUser());
}

Why Use ResponseEntity?

  • Allows control over HTTP status
  • Can add headers if needed
  • Makes API more flexible

Even though the response has a status inside, the HTTP status is still important.

Global Exception Handling (Very Important)

Instead of:

try {
   // logic
} catch(Exception e) {
   // handle
}

Use centralized handling:

@RestControllerAdvice
public class GlobalExceptionHandler {

Why?

  • Centralized error handling
  • No repeated try-catch
  • Cleaner code
  • Consistent error response

Step 6: Validation Handling (Advanced Understanding)

When using @Valid, Spring throws:

MethodArgumentNotValidException

We handle it like this:

List<String> errors = ex.getBindingResult()
    .getFieldErrors()
    .stream()
    .map(err -> err.getField() + ": " + err.getDefaultMessage())
    .toList();

What This Does

  • Extracts all validation errors
  • Converts them into readable messages
  • Sends them in response

Example Output:

"errors": [
  "email: must be valid",
  "name: must not be blank"
]

Step 7: Real-World Enhancements

1. Error Codes

Instead of just a message:

"errorCode": "USER_NOT_FOUND"

Helps frontend and logging systems.

2. Request ID (Tracing)

In microservices:

"requestId": "abc-123-xyz"

Helps track requests across services.

3. Pagination Support

{
  "data": {
    "content": [],
    "page": 1,
    "size": 10,
    "totalElements": 100
  }
}

Important for large datasets.

4. Execution Time

Track performance:

long start = System.currentTimeMillis();

Helps optimize slow APIs.

Common Mistakes to Avoid

  • Returning raw entities directly
  • Mixing multiple response formats
  • Not handling exceptions globally
  • Ignoring validation errors
  • Hardcoding messages everywhere

Have a great one!!!

Author: Ayush Shrivastava

Thank you for being a part of the community

Before you go:

Whenever you’re ready

There are 4 ways we can help you become a great backend engineer:

  • **The MB Platform:** Join thousands of backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learning and set schedules, and solve backend engineering tasks, exercises, and challenges.
  • **The MB Academy:** The “MB Academy” is a 6-month intensive Advanced Backend Engineering Boot Camp to produce great backend engineers.
  • **Join Backend Weekly:** If you like posts like this, you will absolutely enjoy our exclusive weekly newsletter, sharing exclusive backend engineering resources to help you become a great Backend Engineer.
  • **Get Backend Jobs:** Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board.

Originally published at https://blog.masteringbackend.com.


메타데이터
post_id
7c3fbeea766f
slug
mastering-custom-api-response-in-java-spring-boot-7c3fbeea766f
url
https://publication.masteringbackend.com/mastering-custom-api-response-in-java-spring-boot-7c3fbeea766f
canonical_url
https://publication.masteringbackend.com/mastering-custom-api-response-in-java-spring-boot-7c3fbeea766f
author_url
https://medium.com/@ashleyjfrances
status
ok
fetched_at
2026-06-12 22:02:08