Skip to content

PasinduOG/api-response

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

44 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

⚠️ DEPRECATED - Project Relocated

This project has been relocated to OG4Dev Spring Response

New Coordinates:

<dependency>
    <groupId>io.github.og4dev</groupId>
    <artifactId>og4dev-spring-response</artifactId>
    <version>1.0.0</version>
</dependency>

API Response Library (Deprecated)

Tech Stack

⚠️ This library is deprecated. Please use og4dev-spring-response instead.

Deprecated Relocated Maven Central License Java Spring Boot Version

⚠️ DEPRECATED: This project has been rebranded and relocated to OG4Dev Spring Response. Please update your dependencies to use io.github.og4dev:og4dev-spring-response:1.0.0 for future updates and support.

A lightweight, type-safe API Response wrapper for Spring Boot applications. Standardize your REST API responses with consistent structure, automatic timestamps, distributed tracing support, and clean factory methods. Features zero-configuration Spring Boot auto-configuration and production-ready exception handling with comprehensive RFC 9457 ProblemDetail support covering 10 common error scenarios. No external dependencies required - uses pure Java with a custom builder pattern.

πŸ”— Quick Links

πŸ“‘ Table of Contents


🚨 IMPORTANT: Migration to OG4Dev

This Project Has Been Relocated

io.github.pasinduog:api-response β†’ io.github.og4dev:og4dev-spring-response

We have rebranded this library and moved it to the OG4Dev organization for better community support and long-term maintenance.

Quick Migration Guide

1. Update Your Dependencies

Maven:

<!-- REMOVE THIS (Deprecated) -->
<dependency>
    <groupId>io.github.pasinduog</groupId>
    <artifactId>api-response</artifactId>
    <version>3.0.1</version>
</dependency>

<!-- ADD THIS (New) -->
<dependency>
    <groupId>io.github.og4dev</groupId>
    <artifactId>og4dev-spring-response</artifactId>
    <version>1.0.0</version>
</dependency>

Gradle:

// REMOVE THIS (Deprecated)
implementation 'io.github.pasinduog:api-response:3.0.1'

// ADD THIS (New)
implementation 'io.github.og4dev:og4dev-spring-response:1.0.0'

2. No Code Changes Required! βœ…

The API is 100% compatible. You don't need to change any code - just update the dependency coordinates.

// This works exactly the same in both versions
return ApiResponse.success("User found", user);
return ApiResponse.created("User created", newUser);

3. Benefits of Migration

  • βœ… Continued Support - Active development and bug fixes
  • βœ… New Features - Latest enhancements and improvements
  • βœ… Better Organization - Professional organization structure
  • βœ… Community Support - Enhanced collaboration and contribution
  • βœ… Long-term Maintenance - Guaranteed ongoing support

4. What Happens if I Don't Migrate?

  • ⚠️ No New Features - Version 3.0.1 is the final release
  • ⚠️ No Bug Fixes - Critical issues won't be addressed
  • ⚠️ No Support - Questions and issues should go to new project
  • βœ… Still Works - Existing functionality continues to work

Need Help?

Visit the new project for:

  • πŸ“š Updated documentation
  • πŸ› Issue reporting
  • πŸ’¬ Community discussions
  • πŸš€ Latest features

New Repository: https://github.com/og4dev/og4dev-spring-response


🎯 Key Highlights

  • πŸš€ Truly Zero Configuration - Spring Boot 3.x/4.x auto-configuration with META-INF imports
  • 🎯 Production-Ready - Built-in RFC 9457 ProblemDetail with 10 comprehensive exception handlers
  • πŸ›‘οΈ Complete Error Coverage - Handles validation, JSON parsing, 404s, method mismatches, media types, and more (Enhanced in v3.0.0)
  • πŸ” Trace IDs in Errors Only - Error responses include traceId for debugging (Changed in v3.0.0)
  • πŸ”’ Type-Safe & Immutable - Thread-safe design with generic type support
  • πŸ“¦ Ultra-Lightweight - Only ~10KB JAR size with provided dependencies
  • πŸ” Microservices-Ready - Built-in trace IDs for distributed tracing
  • βœ… Battle-Tested - Used in production Spring Boot applications
  • πŸ“‹ Clean Javadoc - Zero warnings with explicit constructor documentation (New in v3.0.0)
  • 🚫 Zero External Dependencies - Pure Java, no Lombok required (Changed in v3.0.0)

✨ Features

  • 🎯 Consistent Structure - All responses follow the same format: status, message, content, timestamp
  • πŸ”’ Type-Safe - Full generic type support with compile-time type checking
  • πŸ” Distributed Tracing - Trace IDs in error responses with MDC integration for request tracking (Enhanced in v3.0.0)
  • ⏰ Auto Timestamps - Automatic RFC 3339 UTC formatted timestamps on every response
  • 🏭 Factory Methods - Clean static methods: success(), created(), status()
  • πŸš€ Zero Config - Spring Boot Auto-Configuration for instant setup (Enhanced in v1.3.0)
  • πŸͺΆ Lightweight - Only ~10KB JAR with single provided dependency (Spring Web)
  • πŸ“¦ Immutable - Thread-safe with final fields
  • πŸ”Œ Spring Native - Built on ResponseEntity and HttpStatus
  • πŸ“‹ RFC 9457 Compliance - Standard ProblemDetail format (supersedes RFC 7807) (Updated in v3.0.0)
  • πŸ“š Complete JavaDoc - Every class fully documented with explicit constructor documentation (New in v3.0.0)
  • πŸ›‘οΈ Comprehensive Exception Handling - 10 built-in handlers covering all common scenarios (Enhanced in v3.0.0)
    • βœ… Validation errors (@Valid annotations)
    • βœ… Type mismatches (wrong parameter types)
    • βœ… Malformed JSON (invalid request bodies)
    • βœ… Missing parameters (required @RequestParam)
    • βœ… 404 Not Found (missing endpoints/resources)
    • βœ… 405 Method Not Allowed (wrong HTTP method)
    • βœ… 415 Unsupported Media Type (invalid Content-Type)
    • βœ… Null pointer exceptions
    • βœ… Custom business exceptions (ApiException)
    • βœ… General unexpected errors
  • 🎭 Custom Business Exceptions - Abstract ApiException class for domain-specific errors (New in v1.2.0)
  • βœ… Validation Support - Automatic @Valid annotation error handling

πŸ“¦ Requirements

  • Java 17 or higher
  • Spring Boot 3.2.0 or higher (tested up to 4.0.2)
  • No additional dependencies required (pure Java implementation)

🌟 What Makes This Different?

Unlike other response wrapper libraries, this one offers:

  • βœ… Native Spring Boot 3.x/4.x Auto-Configuration - No manual setup required
  • βœ… RFC 9457 ProblemDetail Support - Industry-standard error responses (latest RFC)
  • βœ… Zero External Dependencies - Pure Java implementation, won't conflict with your application
  • βœ… Extensible Exception Handling - Create custom business exceptions easily
  • βœ… Trace ID Support - Built-in distributed tracing capabilities
  • βœ… Comprehensive JavaDoc - Every class fully documented with explicit constructor documentation and zero warnings
  • βœ… Production-Grade Quality - Clean builds, proper documentation, and battle-tested code

πŸš€ Installation

Maven (Latest - v3.0.0)

<dependency>
    <groupId>io.github.pasinduog</groupId>
    <artifactId>api-response</artifactId>
    <version>3.0.0</version>
</dependency>

Gradle (Latest - v3.0.0)

implementation 'io.github.pasinduog:api-response:3.0.0'

Gradle Kotlin DSL (Latest - v3.0.0)

implementation("io.github.pasinduog:api-response:3.0.0")

Previous Stable Version (v2.0.0)

If you need the previous stable version with Lombok:

<dependency>
    <groupId>io.github.pasinduog</groupId>
    <artifactId>api-response</artifactId>
    <version>2.0.0</version>
</dependency>

Note: Version 2.0.0 includes 6 additional exception handlers and enhanced features compared to v1.3.0. See Version History for details.

πŸ“ Project Structure

The library is organized into four main packages:

io.github.pasinduog
β”œβ”€β”€ config/
β”‚   └── ApiResponseAutoConfiguration.java    # Spring Boot auto-configuration
β”œβ”€β”€ dto/
β”‚   └── ApiResponse.java                     # Generic response wrapper
β”œβ”€β”€ exception/
β”‚   β”œβ”€β”€ ApiException.java                    # Abstract base for custom exceptions
β”‚   └── GlobalExceptionHandler.java          # RFC 9457 exception handler
└── filter/
    └── TraceIdFilter.java                   # Request trace ID generation

Package Overview

Package Description
config Spring Boot auto-configuration classes for zero-config setup
dto Data Transfer Objects - main ApiResponse<T> wrapper class
exception Exception handling infrastructure with ProblemDetail support
filter Servlet filters for trace ID generation and MDC management

Key Components

  • ApiResponse - Type-safe response wrapper with factory methods
  • ApiResponseAutoConfiguration - Automatic Spring Boot integration
  • GlobalExceptionHandler - Centralized exception handling with RFC 9457
  • ApiException - Base class for domain-specific exceptions
  • TraceIdFilter - Automatic trace ID generation for distributed tracing

🎯 Quick Start

import io.github.pasinduog.dto.ApiResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

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

    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        return ApiResponse.success("User retrieved successfully", user);
    }
}

Response:

{
  "status": 200,
  "message": "User retrieved successfully",
  "content": {
    "id": 1,
    "name": "John Doe",
    "email": "john@example.com"
  },
  "timestamp": "2026-02-06T10:30:45.123Z"
}

βš™οΈ Auto-Configuration (New in v1.3.0)

The library now features Spring Boot Auto-Configuration for truly zero-config setup! Simply add the dependency and everything works automatically.

What Gets Auto-Configured

When you include this library in your Spring Boot application, the following components are automatically registered:

βœ… GlobalExceptionHandler - Automatic exception handling with RFC 7807 ProblemDetail format
βœ… Component Scanning - All library components are automatically discovered
βœ… Bean Registration - No manual @ComponentScan or @Import required

How It Works

The library includes META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports which Spring Boot 3.x automatically detects and loads the ApiResponseAutoConfiguration class.

No configuration needed! Just add the dependency:

<dependency>
    <groupId>io.github.pasinduog</groupId>
    <artifactId>api-response</artifactId>
    <version>2.0.0</version>
</dependency>

Disabling Auto-Configuration (Optional)

If you need to customize or disable the auto-configuration:

@SpringBootApplication(exclude = ApiResponseAutoConfiguration.class)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Or in application.properties:

spring.autoconfigure.exclude=io.github.pasinduog.config.ApiResponseAutoConfiguration

πŸ›‘οΈ Built-in Exception Handling (Enhanced in v1.2.0)

The library includes a production-ready GlobalExceptionHandler that automatically handles common exceptions using Spring Boot's ProblemDetail (RFC 9457) standard.

What's Included

βœ… General Exception Handler - Catches all unhandled exceptions (HTTP 500)
βœ… Validation Error Handler - Automatically processes @Valid annotation failures (HTTP 400)
βœ… Type Mismatch Handler - Handles method argument type conversion errors (HTTP 400) (New in v1.3.0)
βœ… Malformed JSON Handler - Handles invalid JSON request bodies (HTTP 400) (New in v2.0.0)
βœ… Missing Parameter Handler - Detects missing required request parameters (HTTP 400) (New in v2.0.0)
βœ… 404 Not Found Handler - Handles missing endpoints and resources (HTTP 404) (New in v2.0.0)
βœ… Method Not Allowed Handler - Handles unsupported HTTP methods (HTTP 405) (New in v2.0.0)
βœ… Unsupported Media Type Handler - Handles invalid Content-Type headers (HTTP 415) (New in v2.0.0)
βœ… Null Pointer Handler - Specific handling for NullPointerException (HTTP 500)
βœ… Custom ApiException Handler - Handles custom business exceptions extending ApiException (New in v1.2.0)
βœ… Automatic Logging - SLF4J integration for all errors with consistent trace IDs (Enhanced in v2.0.0)
βœ… Trace ID Consistency - Logs and responses always have matching trace IDs (New in v2.0.0)
βœ… Timestamp Support - All error responses include RFC 3339 timestamps
βœ… RFC 9457 Compliance - Standard ProblemDetail format (supersedes RFC 7807) (New in v2.0.0)

Trace ID Consistency (New in v2.0.0)

All exception handlers now ensure consistent trace IDs between logs and error responses:

  • With TraceIdFilter: Uses the trace ID from SLF4J MDC
  • Without Filter: Generates a UUID and stores it in MDC for consistent logging
  • Guaranteed: Logs and responses always have matching trace IDs

Example Log Output:

2026-02-07 10:30:45.123 [550e8400-e29b-41d4-a716-446655440000] ERROR GlobalExceptionHandler - Error in SQLExceptionTranslator:112

Matching Error Response:

{
  "type": "about:blank",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "Internal Server Error. Please contact technical support",
  "traceId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-02-07T10:30:45.123Z"
}

βœ… No more "N/A" trace IDs - Every error has a real, correlatable trace ID
βœ… Easy debugging - Copy trace ID from response and find all related logs
βœ… Thread-safe - Proper MDC management ensures no cross-thread contamination

Custom Business Exceptions (New in v1.2.0)

Instead of throwing generic exceptions, you can now extend the abstract ApiException class to create domain-specific exceptions with automatic exception handling:

public class ResourceNotFoundException extends ApiException {
    public ResourceNotFoundException(String resource, Long id) {
        super(String.format("%s not found with ID: %d", resource, id), HttpStatus.NOT_FOUND);
    }
}

public class UnauthorizedAccessException extends ApiException {
    public UnauthorizedAccessException(String message) {
        super(message, HttpStatus.UNAUTHORIZED);
    }
}

public class BusinessRuleViolationException extends ApiException {
    public BusinessRuleViolationException(String message) {
        super(message, HttpStatus.CONFLICT);
    }
}

Usage in Controllers:

@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
    User user = userService.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("User", id));
    return ApiResponse.success("User retrieved successfully", user);
}

Automatic Error Response (RFC 9457):

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "User not found with ID: 123",
  "traceId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-02-02T10:30:45.123Z"
}

The GlobalExceptionHandler automatically:

  • Extracts the HTTP status from your custom exception
  • Formats it as a ProblemDetail response
  • Logs the error with appropriate severity
  • Includes timestamps for traceability

Example: Validation Errors

Add validation to your DTOs:

public class UserDto {
    @NotBlank(message = "Name is required")
    private String name;
    
    @Email(message = "Email must be valid")
    @NotBlank(message = "Email is required")
    private String email;
    
    @Min(value = 18, message = "Age must be at least 18")
    private Integer age;
}

Use @Valid in your controller:

@PostMapping
public ResponseEntity<ApiResponse<User>> createUser(@Valid @RequestBody UserDto dto) {
    User newUser = userService.create(dto);
    return ApiResponse.created("User created successfully", newUser);
}

Automatic Error Response:

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "Validation Failed",
  "errors": {
    "email": "Email must be valid",
    "name": "Name is required",
    "age": "Age must be at least 18"
  },
  "traceId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-02-02T10:30:45.123Z"
}

Example: Malformed JSON Errors (New in v2.0.0)

When a client sends invalid JSON (missing quotes, commas, etc.):

# Request with malformed JSON
POST /api/users
Content-Type: application/json

{
  "name": "John Doe"
  "email": "invalid"  # Missing comma
}

Automatic Error Response:

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "Malformed JSON request. Please check your request body format.",
  "traceId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-02-02T10:30:45.123Z"
}

Example: Missing Required Parameters (New in v2.0.0)

When a required @RequestParam is missing:

@GetMapping("/search")
public ResponseEntity<ApiResponse<List<User>>> search(
    @RequestParam(required = true) String query) {
    // ...
}

Automatic Error Response:

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "Required request parameter 'query' (type: String) is missing.",
  "traceId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-02-02T10:30:45.123Z"
}

Example: 404 Not Found (New in v2.0.0)

When accessing a non-existent endpoint or resource:

GET /api/nonexistent

Automatic Error Response:

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "The requested resource '/api/nonexistent' was not found.",
  "traceId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-02-02T10:30:45.123Z"
}

Example: Method Not Allowed (New in v2.0.0)

When using an unsupported HTTP method:

# POST to an endpoint that only supports GET
POST /api/users/123

Automatic Error Response:

{
  "type": "about:blank",
  "title": "Method Not Allowed",
  "status": 405,
  "detail": "Method 'POST' is not supported for this endpoint. Supported methods are: [GET, PUT, DELETE]",
  "traceId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-02-02T10:30:45.123Z"
}

Example: Unsupported Media Type (New in v2.0.0)

When sending an unsupported Content-Type:

POST /api/users
Content-Type: application/xml

<user><name>John</name></user>

Automatic Error Response:

{
  "type": "about:blank",
  "title": "Unsupported Media Type",
  "status": 415,
  "detail": "Content type 'application/xml' is not supported. Supported content types: [application/json]",
  "traceId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-02-02T10:30:45.123Z"
}

Logging

All exceptions are automatically logged with appropriate severity levels:

  • ERROR level - General exceptions and null pointer exceptions
  • WARN level - Validation errors, type mismatches, malformed JSON, missing parameters, 404 errors, method not allowed, unsupported media types, and business logic exceptions
2026-02-02 10:30:45.123 WARN  i.g.p.e.GlobalExceptionHandler - Validation error: {email=Email must be valid, name=Name is required}
2026-02-02 10:30:45.456 ERROR i.g.p.e.GlobalExceptionHandler - An unexpected error occurred:
java.lang.RuntimeException: Database connection failed
    at com.example.service.UserService.findById(UserService.java:42)
2026-02-02 10:31:12.789 WARN  i.g.p.e.GlobalExceptionHandler - Malformed JSON request: JSON parse error: Unexpected character...
2026-02-02 10:31:45.234 WARN  i.g.p.e.GlobalExceptionHandler - Missing parameter: Required request parameter 'query' (type: String) is missing.
2026-02-02 10:32:15.567 WARN  i.g.p.e.GlobalExceptionHandler - 404 Not Found: The requested resource '/api/invalid' was not found.
2026-02-02 10:32:45.890 WARN  i.g.p.e.GlobalExceptionHandler - Method not allowed: Method 'POST' is not supported for this endpoint.
2026-02-02 10:33:15.123 WARN  i.g.p.e.GlobalExceptionHandler - Unsupported media type: Content type 'application/xml' is not supported.

## πŸ” Distributed Tracing (Enhanced in v2.0.0)

The library provides **production-ready distributed tracing** with automatic trace ID generation and MDC (Mapped Diagnostic Context) integration for seamless log correlation across your microservices architecture.

### Key Features

βœ… **Automatic Trace ID Generation** - Every response includes a unique UUID for request tracking  
βœ… **MDC Integration** - Trace IDs automatically available in all log statements via SLF4J MDC  
βœ… **TraceIdFilter** - Optional servlet filter for consistent trace ID management *(New in v2.0.0)*  
βœ… **Flexible Priority** - Supports explicit trace IDs, MDC context, or auto-generation  
βœ… **Thread-Safe** - Proper MDC cleanup prevents memory leaks  
βœ… **Zero Configuration** - Works out of the box with sensible defaults  
βœ… **UUID Format** - Standard 128-bit globally unique identifiers  

### Industry Standards Compatibility

The trace ID implementation uses **UUID format** (128-bit), which is:
- βœ… Widely supported across distributed systems
- βœ… Compatible with most logging and APM tools
- βœ… Globally unique without coordination

**For enhanced interoperability**, consider implementing header propagation to support:
- `X-Trace-Id` / `X-Request-ID` (Common custom headers)
- `X-B3-TraceId` (Zipkin/B3 format)
- `traceparent` (W3C Trace Context standard)

See the [Enhanced TraceIdFilter](#enhanced-traceidfilter-with-header-propagation) section below for implementation details.

### How It Works

#### Default Behavior (Without Filter)

By default, `ApiResponse` automatically generates a trace ID for each response:

```java
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
    User user = userService.findById(id);
    return ApiResponse.success("User found", user);
}

Response:

{
  "status": 200,
  "message": "User found",
  "content": { "id": 1, "name": "John" },
  "timestamp": "2026-02-06T10:30:45.123Z"
}

Note: Success responses (ApiResponse) do NOT include traceId. Trace IDs are only included in error responses (ProblemDetail format) for debugging purposes.


#### Enhanced Tracing with TraceIdFilter *(New in v2.0.0)*

For comprehensive distributed tracing, register the `TraceIdFilter` to automatically manage trace IDs across your entire request lifecycle:

**Step 1: Register the Filter**

```java
@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<TraceIdFilter> traceIdFilter() {
        FilterRegistrationBean<TraceIdFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new TraceIdFilter());
        registration.addUrlPatterns("/*");
        registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return registration;
    }
}

Step 2: Automatic Benefits

Once registered, the filter:

  1. Generates a unique UUID for each incoming request
  2. Stores it in SLF4J MDC (available for logging)
  3. Makes it available to ApiResponse automatically
  4. Cleans up MDC after request completion

Step 3: Trace ID in Logs

All your log statements automatically include the trace ID:

@Service
@Slf4j
public class UserService {
    
    public User findById(Long id) {
        log.info("Finding user by ID: {}", id);  // Trace ID automatically included
        // ... business logic
        log.debug("User retrieved from database");
        return user;
    }
}

Log Output (with Logback configuration):

2026-02-06 10:30:45.123 [a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d] INFO  c.e.s.UserService - Finding user by ID: 123
2026-02-06 10:30:45.234 [a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d] DEBUG c.e.s.UserService - User retrieved from database

Logback Configuration (logback-spring.xml):

<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-DD HH:mm:ss.SSS} [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

Trace ID Priority

The trace ID is resolved with the following priority:

  1. Explicit Parameter - If you explicitly set a trace ID via builder
  2. MDC Context - If TraceIdFilter has set a trace ID in MDC
  3. Auto-Generated - Falls back to a random UUID
// Priority 1: Explicit trace ID
UUID customId = UUID.randomUUID();
ApiResponse.<User>builder()
    .traceId(customId)  // This takes highest priority
    .status(200)
    .message("User found")
    .content(user)
    .build();

// Priority 2: From MDC (when TraceIdFilter is active)
// Automatically uses the filter-generated trace ID

// Priority 3: Auto-generated (when no filter and no explicit ID)
// Falls back to random UUID

End-to-End Tracing Example

Scenario: Client β†’ API Gateway β†’ User Service β†’ Database

// API Gateway receives request with trace ID
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
        // TraceIdFilter already set MDC with trace ID
        log.info("Received request for user: {}", id);
        
        User user = userService.findById(id);
        
        // Response automatically includes the same trace ID
        return ApiResponse.success("User found", user);
    }
}

@Service
@Slf4j
public class UserService {
    
    public User findById(Long id) {
        // All logs automatically include trace ID from MDC
        log.info("Finding user in database: {}", id);
        
        User user = userRepository.findById(id)
            .orElseThrow(() -> {
                log.warn("User not found: {}", id);
                return new ResourceNotFoundException("User", id);
            });
            
        log.info("User found: {}", user.getEmail());
        return user;
    }
}

Complete Request Flow:

1. Request arrives β†’ TraceIdFilter generates UUID: a1b2c3d4-...
2. MDC.put("traceId", "a1b2c3d4-...")
3. Controller logs: [a1b2c3d4-...] INFO - Received request for user: 123
4. Service logs:    [a1b2c3d4-...] INFO - Finding user in database: 123
5. Service logs:    [a1b2c3d4-...] INFO - User found: john@example.com
6. Response includes: "traceId": "a1b2c3d4-..."
7. MDC.clear() in finally block

Propagating Trace IDs to Downstream Services

For microservices architectures, propagate the trace ID to downstream services:

@Service
@RequiredArgsConstructor
public class OrderService {
    
    private final RestTemplate restTemplate;
    
    public Order createOrder(OrderDto dto) {
        // Get trace ID from MDC
        String traceId = MDC.get("traceId");
        
        // Create headers with trace ID
        HttpHeaders headers = new HttpHeaders();
        headers.set("X-Trace-Id", traceId);
        headers.setContentType(MediaType.APPLICATION_JSON);
        
        // Call downstream service
        HttpEntity<OrderDto> request = new HttpEntity<>(dto, headers);
        ResponseEntity<ApiResponse<Order>> response = restTemplate.exchange(
            "http://payment-service/api/payments",
            HttpMethod.POST,
            request,
            new ParameterizedTypeReference<ApiResponse<Order>>() {}
        );
        
        return response.getBody().getContent();
    }
}

Downstream Service (Payment Service):

@RestController
@RequestMapping("/api/payments")
public class PaymentController {
    
    @PostMapping
    public ResponseEntity<ApiResponse<Payment>> processPayment(
            @RequestHeader(value = "X-Trace-Id", required = false) String incomingTraceId,
            @RequestBody PaymentDto dto) {
        
        // Use incoming trace ID if provided
        if (incomingTraceId != null) {
            MDC.put("traceId", incomingTraceId);
        }
        
        Payment payment = paymentService.process(dto);
        return ApiResponse.created("Payment processed", payment);
    }
}

Testing with Trace IDs

Unit Tests:

@Test
void testTraceIdInResponse() {
    // Set trace ID in MDC (simulating TraceIdFilter)
    String expectedTraceId = UUID.randomUUID().toString();
    MDC.put("traceId", expectedTraceId);
    
    try {
        ApiResponse<User> response = ApiResponse.<User>builder()
            .status(200)
            .message("Success")
            .content(user)
            .build();
        
        assertEquals(UUID.fromString(expectedTraceId), response.getTraceId());
    } finally {
        MDC.clear();
    }
}

Integration Tests:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    void testTraceIdInApiResponse() {
        ResponseEntity<ApiResponse<User>> response = restTemplate.exchange(
            "/api/users/1",
            HttpMethod.GET,
            null,
            new ParameterizedTypeReference<ApiResponse<User>>() {}
        );
        
        assertNotNull(response.getBody().getTraceId());
        // Trace ID should be in response
    }
}

Benefits

  1. Simplified Debugging - Track requests across multiple services with a single ID
  2. Log Correlation - All logs for a request share the same trace ID
  3. Performance Monitoring - Identify slow requests by trace ID
  4. Error Investigation - Quickly find all logs related to a failed request
  5. Distributed Systems - Essential for microservices architecture
  6. Production Ready - No memory leaks with automatic MDC cleanup

Enhanced TraceIdFilter with Header Propagation

For better interoperability with industry-standard distributed tracing systems, you can extend TraceIdFilter to support incoming trace ID headers and add trace IDs to response headers:

package io.github.pasinduog.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.UUID;

/**
 * Enhanced TraceIdFilter with industry-standard header support.
 * Supports incoming trace IDs from common headers and propagates to response.
 */
public class EnhancedTraceIdFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        UUID traceId = extractOrGenerateTraceId(request);
        
        try {
            request.setAttribute("traceId", traceId);
            MDC.put("traceId", traceId.toString());
            
            // Add trace ID to response header for downstream services
            response.setHeader("X-Trace-Id", traceId.toString());
            
            filterChain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }
    
    /**
     * Extracts trace ID from standard headers or generates a new one.
     * Checks headers in order of priority:
     * 1. X-Trace-Id (custom)
     * 2. X-Request-ID (common)
     * 3. X-B3-TraceId (Zipkin)
     * 4. traceparent (W3C Trace Context)
     */
    private UUID extractOrGenerateTraceId(HttpServletRequest request) {
        // Check X-Trace-Id header
        String traceId = request.getHeader("X-Trace-Id");
        
        // Check X-Request-ID header
        if (traceId == null) {
            traceId = request.getHeader("X-Request-ID");
        }
        
        // Check X-B3-TraceId header (Zipkin format)
        if (traceId == null) {
            traceId = request.getHeader("X-B3-TraceId");
        }
        
        // Check W3C traceparent header
        if (traceId == null) {
            String traceparent = request.getHeader("traceparent");
            if (traceparent != null) {
                // Format: 00-{trace-id}-{span-id}-{flags}
                String[] parts = traceparent.split("-");
                if (parts.length >= 2) {
                    traceId = parts[1]; // Extract trace-id
                }
            }
        }
        
        // Try to parse as UUID, or generate new one
        if (traceId != null) {
            try {
                return UUID.fromString(traceId);
            } catch (IllegalArgumentException e) {
                // Invalid UUID format, generate new one
            }
        }
        
        return UUID.randomUUID();
    }
}

Register the Enhanced Filter:

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<EnhancedTraceIdFilter> traceIdFilter() {
        FilterRegistrationBean<EnhancedTraceIdFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new EnhancedTraceIdFilter());
        registration.addUrlPatterns("/*");
        registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return registration;
    }
}

Benefits of Enhanced Filter:

  • βœ… Accepts trace IDs from upstream services (API Gateway, Load Balancer)
  • βœ… Supports multiple industry-standard header formats
  • βœ… Adds trace ID to response headers for downstream services
  • βœ… Maintains compatibility with W3C, Zipkin, and custom formats
  • βœ… Graceful fallback to UUID generation

Testing with cURL:

# Send request with trace ID in header
curl -H "X-Trace-Id: 12345678-1234-1234-1234-123456789012" \
     http://localhost:8080/api/users/1

# Check response header - trace ID is propagated
HTTP/1.1 200 OK
X-Trace-Id: 12345678-1234-1234-1234-123456789012

# Success response body (no traceId field in ApiResponse)
{
  "status": 200,
  "message": "User found",
  "content": {...},
  "timestamp": "2026-02-06T10:30:45.123Z"
}

# Error responses include traceId in body (ProblemDetail format)
# For errors, the traceId appears in the response JSON:
{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "User not found",
  "traceId": "12345678-1234-1234-1234-123456789012",
  "timestamp": "2026-02-06T10:30:45.123Z"
}

Note:

  • Success responses (ApiResponse) do NOT have traceId in the response body
  • Error responses (ProblemDetail) DO include traceId for debugging
  • Both success and error responses can have X-Trace-Id header if TraceIdFilter is configured

πŸ“– Usage

Success Responses (HTTP 200)

With data:

@GetMapping
public ResponseEntity<ApiResponse<List<User>>> getAllUsers() {
    List<User> users = userService.findAll();
    return ApiResponse.success("Users retrieved successfully", users);
}

Without data:

@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> deleteUser(@PathVariable Long id) {
    userService.delete(id);
    return ApiResponse.success("User deleted successfully");
}

Created Responses (HTTP 201)

@PostMapping
public ResponseEntity<ApiResponse<User>> createUser(@RequestBody UserDto dto) {
    User newUser = userService.create(dto);
    return ApiResponse.created("User created successfully", newUser);
}

Custom Status Responses

Without data:

@GetMapping("/health")
public ResponseEntity<ApiResponse<Void>> healthCheck() {
    if (!service.isHealthy()) {
        return ApiResponse.status("Service unavailable", HttpStatus.SERVICE_UNAVAILABLE);
    }
    return ApiResponse.success("Service is healthy");
}

With data:

@PutMapping("/{id}")
public ResponseEntity<ApiResponse<User>> updateUser(
        @PathVariable Long id, 
        @RequestBody UserDto dto) {
    User updated = userService.update(id, dto);
    return ApiResponse.status("User updated", updated, HttpStatus.OK);
}

🌍 Real-World Examples

Example 1: Complete CRUD Controller

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

    private final ProductService productService;

    @GetMapping
    public ResponseEntity<ApiResponse<Page<Product>>> getAllProducts(
            @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
        Page<Product> products = productService.findAll(pageable);
        return ApiResponse.success("Products retrieved successfully", products);
    }

    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<Product>> getProduct(@PathVariable Long id) {
        Product product = productService.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Product", id));
        return ApiResponse.success("Product found", product);
    }

    @PostMapping
    public ResponseEntity<ApiResponse<Product>> createProduct(
            @Valid @RequestBody ProductDto dto) {
        Product product = productService.create(dto);
        log.info("Product created with ID: {}", product.getId());
        return ApiResponse.created("Product created successfully", product);
    }

    @PutMapping("/{id}")
    public ResponseEntity<ApiResponse<Product>> updateProduct(
            @PathVariable Long id,
            @Valid @RequestBody ProductDto dto) {
        Product product = productService.update(id, dto);
        return ApiResponse.success("Product updated successfully", product);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<ApiResponse<Void>> deleteProduct(@PathVariable Long id) {
        productService.delete(id);
        return ApiResponse.success("Product deleted successfully");
    }

    @PatchMapping("/{id}/status")
    public ResponseEntity<ApiResponse<Product>> updateStatus(
            @PathVariable Long id,
            @RequestParam ProductStatus status) {
        Product product = productService.updateStatus(id, status);
        return ApiResponse.success("Product status updated", product);
    }
}

Example 2: File Upload with Progress

@RestController
@RequestMapping("/api/files")
@RequiredArgsConstructor
public class FileUploadController {

    private final FileStorageService fileService;

    @PostMapping("/upload")
    public ResponseEntity<ApiResponse<FileMetadata>> uploadFile(
            @RequestParam("file") MultipartFile file) {
        
        if (file.isEmpty()) {
            throw new InvalidFileException("File cannot be empty");
        }

        if (file.getSize() > 10 * 1024 * 1024) { // 10MB limit
            throw new FileTooLargeException("File size exceeds 10MB limit");
        }

        FileMetadata metadata = fileService.store(file);
        return ApiResponse.created("File uploaded successfully", metadata);
    }

    @GetMapping("/{id}/download")
    public ResponseEntity<Resource> downloadFile(@PathVariable String id) {
        FileData fileData = fileService.load(id);
        
        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(fileData.getContentType()))
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                        "attachment; filename=\"" + fileData.getFilename() + "\"")
                .body(fileData.getResource());
    }
}

// Custom exceptions
public class InvalidFileException extends ApiException {
    public InvalidFileException(String message) {
        super(message, HttpStatus.BAD_REQUEST);
    }
}

public class FileTooLargeException extends ApiException {
    public FileTooLargeException(String message) {
        super(message, HttpStatus.PAYLOAD_TOO_LARGE);
    }
}

Example 3: Async Processing with Callbacks

@RestController
@RequestMapping("/api/reports")
@RequiredArgsConstructor
public class ReportController {

    private final ReportService reportService;

    @PostMapping("/generate")
    public ResponseEntity<ApiResponse<ReportJob>> generateReport(
            @Valid @RequestBody ReportRequest request) {
        
        ReportJob job = reportService.submitJob(request);
        
        return ApiResponse.status(
                "Report generation started. Check status at /api/reports/" + job.getId(),
                job,
                HttpStatus.ACCEPTED
        );
    }

    @GetMapping("/{jobId}/status")
    public ResponseEntity<ApiResponse<ReportJobStatus>> getStatus(@PathVariable String jobId) {
        ReportJobStatus status = reportService.getJobStatus(jobId);
        
        return switch (status.getState()) {
            case COMPLETED -> ApiResponse.success("Report is ready", status);
            case FAILED -> ApiResponse.status("Report generation failed", status, HttpStatus.INTERNAL_SERVER_ERROR);
            case PROCESSING -> ApiResponse.status("Report is being generated", status, HttpStatus.ACCEPTED);
            default -> ApiResponse.status("Report is queued", status, HttpStatus.ACCEPTED);
        };
    }
}

Example 4: Search with Filters

@RestController
@RequestMapping("/api/search")
@RequiredArgsConstructor
public class SearchController {

    private final SearchService searchService;

    @GetMapping("/products")
    public ResponseEntity<ApiResponse<SearchResults<Product>>> searchProducts(
            @RequestParam(required = false) String query,
            @RequestParam(required = false) BigDecimal minPrice,
            @RequestParam(required = false) BigDecimal maxPrice,
            @RequestParam(required = false) List<String> categories,
            @RequestParam(required = false) Boolean inStock,
            Pageable pageable) {

        SearchCriteria criteria = SearchCriteria.builder()
                .query(query)
                .minPrice(minPrice)
                .maxPrice(maxPrice)
                .categories(categories)
                .inStock(inStock)
                .build();

        SearchResults<Product> results = searchService.search(criteria, pageable);
        
        String message = String.format("Found %d results", results.getTotalElements());
        return ApiResponse.success(message, results);
    }
}

Example 5: Batch Operations

@RestController
@RequestMapping("/api/batch")
@RequiredArgsConstructor
public class BatchOperationController {

    private final BatchService batchService;

    @PostMapping("/users/import")
    public ResponseEntity<ApiResponse<BatchResult>> importUsers(
            @RequestBody List<@Valid UserImportDto> users) {
        
        if (users.isEmpty()) {
            throw new InvalidRequestException("User list cannot be empty");
        }

        if (users.size() > 1000) {
            throw new BatchTooLargeException("Maximum 1000 users per batch");
        }

        BatchResult result = batchService.importUsers(users);
        
        String message = String.format(
                "Batch completed: %d successful, %d failed",
                result.getSuccessCount(),
                result.getFailureCount()
        );
        
        return ApiResponse.success(message, result);
    }

    @DeleteMapping("/users")
    public ResponseEntity<ApiResponse<BatchDeleteResult>> deleteUsers(
            @RequestBody List<Long> userIds) {
        
        BatchDeleteResult result = batchService.deleteUsers(userIds);
        
        return ApiResponse.success(
                String.format("Deleted %d users", result.getDeletedCount()),
                result
        );
    }
}

Example 6: Health Check & Monitoring

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class HealthController {

    private final DatabaseHealthChecker dbHealthChecker;
    private final CacheHealthChecker cacheHealthChecker;
    private final ExternalApiHealthChecker apiHealthChecker;

    @GetMapping("/health")
    public ResponseEntity<ApiResponse<HealthStatus>> health() {
        HealthStatus status = HealthStatus.builder()
                .database(dbHealthChecker.check())
                .cache(cacheHealthChecker.check())
                .externalApi(apiHealthChecker.check())
                .timestamp(Instant.now())
                .build();

        HttpStatus httpStatus = status.isHealthy() ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE;
        String message = status.isHealthy() ? "All systems operational" : "Some systems are down";
        
        return ApiResponse.status(message, status, httpStatus);
    }

    @GetMapping("/metrics")
    public ResponseEntity<ApiResponse<SystemMetrics>> metrics() {
        SystemMetrics metrics = SystemMetrics.builder()
                .uptime(ManagementFactory.getRuntimeMXBean().getUptime())
                .memoryUsage(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())
                .activeThreads(Thread.activeCount())
                .timestamp(Instant.now())
                .build();
        
        return ApiResponse.success("System metrics retrieved", metrics);
    }
}

Error Handling with GlobalExceptionHandler

Version 2.0.0 includes a comprehensive GlobalExceptionHandler with 10 built-in exception handlers using Spring Boot's ProblemDetail (RFC 9457) for standardized error responses:

Handled Exception Types:

  1. βœ… General Exceptions (HTTP 500)
  2. βœ… Validation Errors (HTTP 400) - @Valid annotation failures
  3. βœ… Type Mismatches (HTTP 400) - Wrong parameter types
  4. βœ… Malformed JSON (HTTP 400) - Invalid request body (New in v2.0.0)
  5. βœ… Missing Parameters (HTTP 400) - Required @RequestParam missing (New in v2.0.0)
  6. βœ… 404 Not Found (HTTP 404) - Missing endpoints/resources (New in v2.0.0)
  7. βœ… 405 Method Not Allowed (HTTP 405) - Wrong HTTP method (New in v2.0.0)
  8. βœ… 415 Unsupported Media Type (HTTP 415) - Invalid Content-Type (New in v2.0.0)
  9. βœ… Null Pointer Exceptions (HTTP 500)
  10. βœ… Custom ApiExceptions - Domain-specific business logic errors
package io.github.pasinduog.exception;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    // 10 comprehensive exception handlers included
    // See full implementation in the source code
    
    private String getOrGenerateTraceId() {
        String traceId = MDC.get("traceId");
        if (traceId == null) {
            traceId = UUID.randomUUID().toString();
            MDC.put("traceId", traceId);
        }
        return traceId;
    }
    
    @ExceptionHandler(Exception.class)
    public ProblemDetail handleAllExceptions(Exception ex) {
        String traceId = getOrGenerateTraceId();
        log.error("[TraceID: {}] An unexpected error occurred: ", traceId, ex);
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
            HttpStatus.INTERNAL_SERVER_ERROR, 
            "Internal Server Error. Please contact technical support"
        );
        problemDetail.setProperty("traceId", traceId);
        problemDetail.setProperty("timestamp", Instant.now());
        return problemDetail;
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ProblemDetail handleValidationExceptions(MethodArgumentNotValidException ex) {
        String traceId = getOrGenerateTraceId();
        Map<String, String> errorMessage = new HashMap<>();
        for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
            errorMessage.merge(fieldError.getField(), fieldError.getDefaultMessage(),
                    (msg1, msg2) -> msg1 + "; " + msg2);
        }
        log.warn("[TraceID: {}] Validation error: {}", traceId, errorMessage);
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
            HttpStatus.BAD_REQUEST, 
            "Validation Failed"
        );
        problemDetail.setProperty("errors", errorMessage);
        problemDetail.setProperty("traceId", traceId);
        problemDetail.setProperty("timestamp", Instant.now());
        return problemDetail;
    }

    @ExceptionHandler(NullPointerException.class)
    public ProblemDetail handleNullPointerExceptions(NullPointerException ex) {
        String traceId = getOrGenerateTraceId();
        log.error("[TraceID: {}] Null pointer exception occurred: ", traceId, ex);
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
            HttpStatus.INTERNAL_SERVER_ERROR, 
            "A null pointer exception occurred."
        );
        problemDetail.setProperty("traceId", traceId);
        problemDetail.setProperty("timestamp", Instant.now());
        return problemDetail;
    }
}

The GlobalExceptionHandler provides:

  • πŸ›‘οΈ ProblemDetail RFC 9457 - Latest standard error format (supersedes RFC 7807)
  • βœ… Validation Error Handling - Automatic @Valid annotation support
  • πŸ“ Comprehensive Logging - SLF4J integration with trace IDs
  • ⏰ Automatic Timestamps - On all error responses
  • πŸ” Trace ID Generation - Automatic UUID generation for all errors
  • πŸ” Null Pointer Protection - Dedicated NullPointerException handling
  • πŸ“Š 10 Exception Handlers - Covers all common error scenarios

Example Validation Error Response:

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "Validation Failed",
  "errors": {
    "email": "must be a well-formed email address",
    "name": "must not be blank"
  },
  "timestamp": "2026-02-02T10:30:45.123Z"
}

You can also create custom exception handlers using ApiResponse:

@ControllerAdvice
public class CustomExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleNotFound(ResourceNotFoundException ex) {
        return ApiResponse.status(ex.getMessage(), HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ApiResponse<Void>> handleBadRequest(IllegalArgumentException ex) {
        return ApiResponse.status("Invalid request: " + ex.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

πŸ“š API Reference

Static Factory Methods

Method Parameters Return Type HTTP Status Description
success(String message) message ResponseEntity<ApiResponse<Void>> 200 OK Success without data
success(String message, T data) message, data ResponseEntity<ApiResponse<T>> 200 OK Success with data
created(String message, T data) message, data ResponseEntity<ApiResponse<T>> 201 CREATED Resource creation
status(String message, HttpStatus status) message, status ResponseEntity<ApiResponse<Void>> Custom Custom status without data
status(String message, T data, HttpStatus status) message, data, status ResponseEntity<ApiResponse<T>> Custom Custom status with data

Response Fields

Field Type Description
status Integer HTTP status code (e.g., 200, 201, 404)
traceId UUID Unique identifier for request tracing and log correlation - Enhanced in v2.0.0 with MDC integration
message String Human-readable message describing the response
content T (Generic) Response payload (can be any type or null)
timestamp Instant ISO-8601 formatted UTC timestamp (auto-generated)

πŸ” Response Structure

All success responses follow this consistent structure:

{
  "status": 200,
  "message": "string",
  "content": {},
  "timestamp": "2026-02-06T10:30:45.123456Z"
}

Note: traceId is NOT included in success responses. Trace IDs are only added to error responses (ProblemDetail format) by the GlobalExceptionHandler for debugging purposes.

Examples

Single Object:

{
  "status": 200,
  "message": "Product found",
  "content": {
    "id": 1,
    "name": "Laptop",
    "price": 999.99
  },
  "timestamp": "2026-02-01T10:30:45.123Z"
}

Array/List:

{
  "status": 200,
  "message": "Products retrieved",
  "content": [
    {"id": 1, "name": "Laptop"},
    {"id": 2, "name": "Mouse"}
  ],
  "timestamp": "2026-02-01T10:30:45.123Z"
}

No Content (Void):

{
  "status": 200,
  "message": "Product deleted successfully",
  "timestamp": "2026-02-01T10:30:45.123Z"
}

Error Response (ProblemDetail format with traceId):

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "Product not found with ID: 123",
  "traceId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-02-01T10:30:45.123Z"
}

πŸ’‘ Best Practices

1. Use Appropriate Methods

// βœ… Use success() for standard operations
return ApiResponse.success("Retrieved", data);

// βœ… Use created() for resource creation
return ApiResponse.created("Created", newResource);

// βœ… Use status() for custom status codes
return ApiResponse.status("Accepted", HttpStatus.ACCEPTED);

2. Write Clear Messages

// βœ… Good - Descriptive and specific
return ApiResponse.success("User profile updated successfully", user);

// ❌ Avoid - Too generic
return ApiResponse.success("Success", user);

3. Leverage Generics

// Specific types
ResponseEntity<ApiResponse<User>> getUser();
ResponseEntity<ApiResponse<List<Product>>> getProducts();
ResponseEntity<ApiResponse<Map<String, Object>>> getMetadata();
ResponseEntity<ApiResponse<Void>> deleteResource();

4. Custom Business Exceptions (New in v1.2.0)

Version 1.2.0+ includes an abstract ApiException class for creating domain-specific exceptions. The built-in GlobalExceptionHandler automatically handles them with the correct HTTP status:

// Define custom exceptions
public class ResourceNotFoundException extends ApiException {
    public ResourceNotFoundException(String resource, Long id) {
        super(String.format("%s not found with ID: %d", resource, id), HttpStatus.NOT_FOUND);
    }
}

public class InsufficientBalanceException extends ApiException {
    public InsufficientBalanceException(String accountId) {
        super("Insufficient balance in account: " + accountId, HttpStatus.PAYMENT_REQUIRED);
    }
}

// Use them in your service/controller
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
    User user = userRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("User", id));
    return ApiResponse.success("User found", user);
}

Benefits:

  • βœ… No need to manually create @ExceptionHandler methods
  • βœ… Automatic RFC 9457 ProblemDetail formatting
  • βœ… Type-safe with compile-time checking
  • βœ… Clean, readable code

You can still create additional custom exception handlers if needed:

@ControllerAdvice
public class CustomExceptionHandler {
    
    @ExceptionHandler(ThirdPartyApiException.class)
    public ProblemDetail handleThirdPartyError(ThirdPartyApiException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.BAD_GATEWAY, ex.getMessage()
        );
        problem.setProperty("timestamp", Instant.now());
        return problem;
    }
}

5. RESTful Conventions

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

    @GetMapping("/{id}")  // 200 OK
    public ResponseEntity<ApiResponse<Product>> get(@PathVariable Long id) {
        return ApiResponse.success("Product found", productService.findById(id));
    }

    @PostMapping  // 201 CREATED
    public ResponseEntity<ApiResponse<Product>> create(@RequestBody ProductDto dto) {
        return ApiResponse.created("Product created", productService.create(dto));
    }

    @PutMapping("/{id}")  // 200 OK
    public ResponseEntity<ApiResponse<Product>> update(
            @PathVariable Long id, @RequestBody ProductDto dto) {
        return ApiResponse.success("Product updated", productService.update(id, dto));
    }

    @DeleteMapping("/{id}")  // 200 OK
    public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long id) {
        productService.delete(id);
        return ApiResponse.success("Product deleted");
    }
}

πŸ§ͺ Testing

Unit Testing with MockMvc

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    void shouldReturnUserSuccessfully() throws Exception {
        User user = new User(1L, "John Doe", "john@example.com");
        when(userService.findById(1L)).thenReturn(user);

        mockMvc.perform(get("/api/users/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.status").value(200))
                .andExpect(jsonPath("$.message").value("User retrieved successfully"))
                .andExpect(jsonPath("$.data.id").value(1))
                .andExpect(jsonPath("$.data.name").value("John Doe"))
                .andExpect(jsonPath("$.traceId").exists())
                .andExpect(jsonPath("$.timestamp").exists());
    }

    @Test
    void shouldReturnCreatedStatus() throws Exception {
        User newUser = new User(1L, "Jane Doe", "jane@example.com");
        when(userService.create(any())).thenReturn(newUser);

        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"name\":\"Jane Doe\",\"email\":\"jane@example.com\"}"))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.status").value(201))
                .andExpect(jsonPath("$.message").value("User created successfully"))
                .andExpect(jsonPath("$.data.id").value(1))
                .andExpect(jsonPath("$.traceId").exists());
    }
    
    @Test
    void shouldReturnValidationErrors() throws Exception {
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"name\":\"\",\"email\":\"invalid\"}"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.status").value(400))
                .andExpect(jsonPath("$.detail").value("Validation Failed"))
                .andExpect(jsonPath("$.errors.name").exists())
                .andExpect(jsonPath("$.errors.email").exists())
                .andExpect(jsonPath("$.timestamp").exists());
    }
    
    @Test
    void shouldHandleCustomException() throws Exception {
        when(userService.findById(999L))
                .thenThrow(new ResourceNotFoundException("User", 999L));

        mockMvc.perform(get("/api/users/999"))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.status").value(404))
                .andExpect(jsonPath("$.detail").value("User not found with ID: 999"))
                .andExpect(jsonPath("$.timestamp").exists());
    }
}

Integration Testing with TestRestTemplate

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldGetUser() {
        ResponseEntity<ApiResponse> response = restTemplate.getForEntity(
                "/api/users/1", ApiResponse.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody().getStatus()).isEqualTo(200);
        assertThat(response.getBody().getMessage()).contains("User");
        assertThat(response.getBody().getTraceId()).isNotNull();
        assertThat(response.getBody().getTimestamp()).isNotNull();
    }

    @Test
    void shouldCreateUser() {
        UserDto newUser = new UserDto("Jane Doe", "jane@example.com");
        
        ResponseEntity<ApiResponse> response = restTemplate.postForEntity(
                "/api/users", newUser, ApiResponse.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().getStatus()).isEqualTo(201);
    }
}

Testing Custom Exceptions

@ExtendWith(MockitoExtension.class)
class CustomExceptionTest {

    @Test
    void shouldThrowResourceNotFoundException() {
        ResourceNotFoundException exception = 
            new ResourceNotFoundException("User", 123L);

        assertThat(exception.getMessage()).isEqualTo("User not found with ID: 123");
        assertThat(exception.getStatus()).isEqualTo(HttpStatus.NOT_FOUND);
    }
}

Testing with WebTestClient (WebFlux)

If you adapt the library for reactive applications:

@WebFluxTest(UserController.class)
class UserControllerWebFluxTest {

    @Autowired
    private WebTestClient webTestClient;

    @MockBean
    private UserService userService;

    @Test
    void shouldReturnUser() {
        User user = new User(1L, "John Doe", "john@example.com");
        when(userService.findById(1L)).thenReturn(Mono.just(user));

        webTestClient.get()
                .uri("/api/users/1")
                .exchange()
                .expectStatus().isOk()
                .expectBody()
                .jsonPath("$.status").isEqualTo(200)
                .jsonPath("$.data.name").isEqualTo("John Doe")
                .jsonPath("$.traceId").exists();
    }
}
    User newUser = new User(1L, "Jane Doe", "jane@example.com");
    when(userService.create(any())).thenReturn(newUser);

    mockMvc.perform(post("/api/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"name\":\"Jane Doe\",\"email\":\"jane@example.com\"}"))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.status").value(201))
            .andExpect(jsonPath("$.message").value("User created successfully"))
            .andExpect(jsonPath("$.data.id").value(1))
            .andExpect(jsonPath("$.traceId").exists());
}

}


## πŸ—οΈ Architecture & Design Principles

### Thread Safety & Immutability

The `ApiResponse<T>` class is designed with **immutability** at its core:

- All fields are declared as `final`
- No setter methods exist (only getters)
- Uses a custom inner Builder class for object construction
- Thread-safe by design - can be safely shared across threads

```java
// Once created, the response cannot be modified
ApiResponse<User> response = new ApiResponse.ApiResponseBuilder<User>()
    .message("Success")
    .content(user)
    .build();

// This is thread-safe and can be safely cached or shared

Dependency Management

The library uses provided scope for Spring Boot:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <scope>provided</scope>  <!-- Will not bloat your application -->
</dependency>

Benefits:

  • βœ… No Dependency Conflicts - Uses your application's existing Spring Boot version
  • βœ… Zero External Dependencies - Pure Java implementation, no Lombok or other libraries required
  • βœ… Zero Bloat - Adds only ~10KB to your application
  • βœ… Version Flexibility - Compatible with Spring Boot 3.2.0 - 4.0.2 and Java 17+

Auto-Configuration Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Spring Boot Application Starts                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Reads META-INF/spring/                             β”‚
β”‚  org.springframework.boot.autoconfigure             β”‚
β”‚  .AutoConfiguration.imports                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Loads ApiResponseAutoConfiguration                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Registers GlobalExceptionHandler Bean              β”‚
β”‚  (as @RestControllerAdvice)                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Ready to Handle Exceptions Automatically           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Design Patterns Used

  • Factory Pattern - Static factory methods (success(), created(), status())
  • Builder Pattern - Custom inner Builder class for flexible object construction
  • Template Method Pattern - ApiException abstract class for custom exceptions
  • Advisor Pattern - GlobalExceptionHandler with @RestControllerAdvice

πŸ”Œ OpenAPI/Swagger Integration

Works seamlessly with SpringDoc OpenAPI:

@Operation(summary = "Get user by ID", description = "Returns a single user")
@ApiResponses(value = {
    @ApiResponse(responseCode = "200", description = "User found"),
    @ApiResponse(responseCode = "404", description = "User not found")
})
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
    User user = userService.findById(id);
    return ApiResponse.success("User retrieved successfully", user);
}

Add SpringDoc dependency:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.3.0</version>
</dependency>

πŸ”„ Compatibility Matrix

Tested Compatibility

Library Version Java Version Spring Boot Version Status
2.0.0 17, 21+ 3.2.0 - 4.0.2 βœ… Tested
1.3.0 17, 21+ 3.2.0 - 4.0.2 βœ… Tested
1.2.0 17, 21+ 3.2.0+ βœ… Tested
1.1.0 17, 21+ 3.2.0+ βœ… Tested
1.0.0 17, 21+ 3.2.0+ βœ… Tested

Version Requirements

Minimum Requirements:

  • Java: 17 or higher
  • Spring Boot: 3.2.0 or higher (tested up to 4.0.2)
  • Dependencies: None (pure Java implementation)

Recommended:

  • Java: 21 (LTS)
  • Spring Boot: 3.4.x or 4.0.x (fully compatible with Spring Boot 4)

Spring Boot 4.x Support

βœ… Full Spring Boot 4.0.2 Compatibility

  • The library has been tested and verified to work with Spring Boot 4.0.2
  • All features including auto-configuration work seamlessly
  • No breaking changes when upgrading from Spring Boot 3.x to 4.x
  • Uses provided scope dependencies to avoid version conflicts

Framework Compatibility

Framework Supported Notes
Spring Boot 4.x βœ… Yes Full support with version 4.0.2
Spring Boot 3.x βœ… Yes Full support with auto-configuration
Spring Boot 2.x ❌ No Use Spring Boot 3.x+
Spring WebFlux ⚠️ Partial Manual adaptation required
Micronaut ❌ No Spring-specific features used
Quarkus ❌ No Spring-specific features used

Build Tools

Build Tool Supported Configuration
Maven βœ… Yes Native support
Gradle βœ… Yes Groovy & Kotlin DSL
Gradle (Groovy) βœ… Yes implementation 'io.github.pasinduog:api-response:2.0.0'
Gradle (Kotlin) βœ… Yes implementation("io.github.pasinduog:api-response:2.0.0")

πŸ”§ Troubleshooting

Common Issues & Solutions

1. GlobalExceptionHandler Not Working

Problem: Exceptions are not being caught by the GlobalExceptionHandler.

Solution:

  • Ensure you're using version 1.3.0+ with auto-configuration
  • Check that auto-configuration is not excluded
  • Verify Spring Boot version is 3.2.0+
// Verify auto-configuration is active
@SpringBootApplication
// Do NOT exclude ApiResponseAutoConfiguration
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

2. Response Fields Not Serializing

Problem: Some fields like status, message, or content are missing in JSON responses.

Solution:

  • The library uses @JsonInclude(JsonInclude.Include.NON_NULL) to exclude null fields
  • This is intentional behavior - only non-null fields are included in the response
  • To include all fields, you would need to customize Jackson's configuration in your application

3. Auto-Configuration Not Working

Problem: GlobalExceptionHandler is not being picked up automatically.

Solution:

  • Ensure you're using Spring Boot 3.2.0 or higher
  • Check that the library JAR is on the classpath
  • Verify META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports exists in the JAR
  • Check application logs for auto-configuration reports

4. Version Conflicts with Spring Boot

.status(200)
.traceId(UUID.randomUUID())  // Must set manually
.message("Success")
.content(data)
.build());

- For **error responses**, trace IDs are **automatically generated** by GlobalExceptionHandler *(v2.0.0+)*
- All error logs and responses have matching trace IDs for easy correlation

#### 4. Dependency Conflicts

**Problem:** Version conflicts with Spring Boot.

**Solution:**
- The library uses `provided` scope for Spring Boot - it won't conflict
- Ensure your application has Spring Boot Web dependency:

```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

5. Custom Exception Not Being Caught

Problem: Custom ApiException subclass not returning ProblemDetail.

Solution:

  • Ensure your exception extends ApiException
  • Verify the exception is actually being thrown
  • Check GlobalExceptionHandler is registered
// βœ… Correct
public class MyException extends ApiException {
    public MyException(String message) {
        super(message, HttpStatus.BAD_REQUEST);
    }
}

// ❌ Wrong - must extend ApiException
public class MyException extends RuntimeException {
    // ...
}

6. Timestamp Format Issues

Problem: Timestamp format not as expected.

Solution:

  • The library uses Instant (UTC) by default
  • Configure Jackson if you need different format:
@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        return mapper;
    }
}

7. Auto-Configuration Not Loading

Problem: Auto-configuration doesn't work after upgrading to 1.3.0.

Solution:

  • Verify you're using Spring Boot 3.x (not 2.x)
  • Check the JAR includes META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  • Clear Maven/Gradle cache and rebuild:
# Maven
mvn clean install

# Gradle
./gradlew clean build --refresh-dependencies

8. Jackson Serialization Issues

Problem: Fields are not serializing as expected or timestamp format is wrong.

Solution:

  • The library uses Jackson's @JsonInclude(NON_NULL) by default
  • Ensure you have jackson-datatype-jsr310 for Java 8+ date/time support (included in Spring Boot)
  • Configure Jackson if needed:
@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        return mapper;
    }
}

9. UUID Not Serializing Properly

Problem: TraceId appears as an object instead of string.

Solution:

  • This shouldn't happen with standard Jackson configuration
  • Verify Jackson version is compatible with Spring Boot
  • UUID is serialized as string by default in Jackson
// βœ… Correct
"traceId": "550e8400-e29b-41d4-a716-446655440000"

// ❌ Wrong (shouldn't happen)
"traceId": {"mostSigBits": 123, "leastSigBits": 456}

Getting Help

If you encounter issues not covered here:

  1. Check the Issues: GitHub Issues
  2. Review JavaDocs: All classes are fully documented
  3. Enable Debug Logging:
    logging.level.io.github.pasinduog=DEBUG
  4. Open an Issue: Provide minimal reproducible example

❓ FAQ

How do I use trace IDs for debugging? (Enhanced in v2.0.0)

Error responses automatically include a traceId (UUID) in the ProblemDetail format for request tracking and debugging. Success responses (ApiResponse) do NOT include traceId in the response body, but you can add trace IDs via headers using the TraceIdFilter.

Key Points:

  • βœ… Error responses - traceId is automatically included in ProblemDetail JSON
  • ❌ Success responses - traceId is NOT in ApiResponse JSON (use headers instead)
  • πŸ” Logging - All exceptions are logged with [TraceID: xxx] for correlation

For comprehensive distributed tracing with MDC integration and header propagation, see the Distributed Tracing section above.

Quick Example with TraceIdFilter:

// 1. Register TraceIdFilter for header propagation
@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<TraceIdFilter> traceIdFilter() {
        FilterRegistrationBean<TraceIdFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new TraceIdFilter());
        registration.addUrlPatterns("/*");
        registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return registration;
    }
}

// 2. Use in your service (trace ID automatically in logs)
@Service
@Slf4j
public class UserService {
    public User createUser(UserDto dto) {
        log.info("Creating user with email: {}", dto.getEmail());  // [traceId] in logs
        // ... business logic
        return user;
    }
}

For industry-standard header propagation (accepting trace IDs from upstream services), use the Enhanced TraceIdFilter which supports:

  • X-Trace-Id and X-Request-ID headers
  • X-B3-TraceId (Zipkin format)
  • traceparent (W3C Trace Context)
  • Automatic response header injection }

**Response:**
```json
{
  "status": 200,
  "message": "User created",
  "content": {...},
  "timestamp": "2026-02-06T10:30:45.123Z"
}

Note: The traceId is in the MDC for logging and can be added to response headers, but is NOT in the ApiResponse JSON body.

Logs (with traceId from MDC):

2026-02-06 10:30:45.123 [550e8400-e29b-41d4-a716-446655440000] INFO c.e.s.UserService - Creating user with email: john@example.com

For more details on MDC integration, propagating trace IDs to downstream services, and end-to-end tracing examples, see the **[Distributed Tracing](#-distributed-tracing-enhanced-in-v200)** section.

How do I create custom business exceptions? (New in v1.2.0)

Extend the abstract ApiException class to create domain-specific exceptions:

public class ResourceNotFoundException extends ApiException {
    public ResourceNotFoundException(String resource, Long id) {
        super(String.format("%s not found with ID: %d", resource, id), HttpStatus.NOT_FOUND);
    }
}

public class DuplicateResourceException extends ApiException {
    public DuplicateResourceException(String message) {
        super(message, HttpStatus.CONFLICT);
    }
}

Then throw them in your code - the GlobalExceptionHandler will automatically convert them to RFC 9457 ProblemDetail responses:

@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
    User user = userRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("User", id));
    return ApiResponse.success("User found", user);
}

How do I use the built-in GlobalExceptionHandler?

As of v1.3.0, the GlobalExceptionHandler is automatically configured via Spring Boot Auto-Configuration. No manual setup is required!

Simply add the library dependency, and the exception handler will be active immediately. The auto-configuration mechanism automatically registers the handler when the library is detected on the classpath.

For versions prior to 1.3.0, you needed to ensure component scanning:

@SpringBootApplication
@ComponentScan(basePackages = {"com.yourapp", "io.github.pasinduog.exception"})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Can I customize or extend the GlobalExceptionHandler?

Yes! You can add your own @ControllerAdvice handlers alongside the built-in one:

@ControllerAdvice
public class CustomExceptionHandler {
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleNotFound(ResourceNotFoundException ex) {
        return ApiResponse.status(ex.getMessage(), HttpStatus.NOT_FOUND);
    }
    
    @ExceptionHandler(UnauthorizedException.class)
    public ProblemDetail handleUnauthorized(UnauthorizedException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.UNAUTHORIZED, ex.getMessage()
        );
        problem.setProperty("timestamp", Instant.now());
        return problem;
    }
}

How do I disable the GlobalExceptionHandler?

As of v1.3.0, you can disable it by excluding the auto-configuration:

@SpringBootApplication(exclude = ApiResponseAutoConfiguration.class)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Or in application.properties:

spring.autoconfigure.exclude=io.github.pasinduog.config.ApiResponseAutoConfiguration

For versions prior to 1.3.0, you needed to use component scanning filters:

@SpringBootApplication
@ComponentScan(basePackages = "com.yourapp",
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.ASSIGNABLE_TYPE,
        classes = GlobalExceptionHandler.class
    ))
public class Application {
    // ...
}

How do I customize the timestamp format?

Configure Jackson's ObjectMapper:

@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        return mapper;
    }
}

Can I add custom fields?

The library provides a standard structure. To add custom fields, wrap the response:

public class ExtendedResponse<T> {
    private ApiResponse<T> response;
    private String requestId;
    private Map<String, String> metadata;
    // getters and setters
}

Does this work with Spring WebFlux?

Currently designed for Spring MVC. For WebFlux, you'd need to adapt it to work with Mono<ResponseEntity<ApiResponse<T>>>.

How do I handle paginated responses?

Use Spring's Page as the data type:

@GetMapping
public ResponseEntity<ApiResponse<Page<User>>> getUsers(Pageable pageable) {
    Page<User> users = userService.findAll(pageable);
    return ApiResponse.success("Users retrieved", users);
}

Why use this over plain ResponseEntity?

Plain ResponseEntity ApiResponse
Inconsistent structure βœ… Standardized everywhere
Manual timestamps βœ… Automatic
No trace IDs βœ… Built-in UUID trace IDs (v1.2.0)
No status in body βœ… Status code included in response (v1.2.0)
Manual exception handling βœ… Custom ApiException support (v1.2.0)
More boilerplate βœ… Concise factory methods
No message field βœ… Always includes message

πŸš€ Performance & Best Practices

Performance Characteristics

  • Response Creation: < 1ms (simple object instantiation with builder pattern)
  • Memory Footprint: ~200 bytes per response object (excluding data payload)
  • Thread Safety: 100% thread-safe (immutable design with final fields)
  • GC Impact: Minimal (uses immutable objects, eligible for quick collection)
  • JSON Serialization: Optimized with @JsonInclude(NON_NULL) to reduce payload size
  • UUID Generation: Negligible overhead (~0.1ms per UUID using UUID.randomUUID())
  • Timestamp Generation: Negligible overhead (~0.01ms using Instant.now())

Benchmark Results (Approximate)

Operation Time Notes
ApiResponse.success() ~0.5ms Including UUID and timestamp generation
ApiResponse.created() ~0.5ms Same as success()
ApiResponse.builder().build() ~0.3ms Manual builder without factory methods
JSON Serialization (small DTO) ~1-2ms Standard Jackson performance
GlobalExceptionHandler catch ~0.1ms Minimal overhead for exception transformation

Best Practices

1. Prefer Factory Methods Over Builder

// βœ… RECOMMENDED - Auto-generates trace ID and timestamp
return ApiResponse.success("User found", user);

// ⚠️ AVOID - More verbose, manual field management
return ResponseEntity.ok(ApiResponse.<User>builder()
    .status(200)
    .traceId(UUID.randomUUID())
    .message("User found")
    .data(user)
    .timestamp(Instant.now())
    .build());

2. Use Appropriate HTTP Status Codes

// βœ… POST - Use created() for 201
@PostMapping
public ResponseEntity<ApiResponse<User>> create(@RequestBody UserDto dto) {
    return ApiResponse.created("User created", userService.create(dto));
}

// βœ… DELETE - Use success() with no data
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long id) {
    userService.delete(id);
    return ApiResponse.success("User deleted");
}

// βœ… Custom status - Use status() method
@GetMapping("/health")
public ResponseEntity<ApiResponse<Void>> health() {
    return ApiResponse.status("Service degraded", HttpStatus.SERVICE_UNAVAILABLE);
}

3. Write Descriptive Messages

// βœ… GOOD - Clear and actionable
return ApiResponse.success("User profile updated successfully", updatedUser);

// ❌ BAD - Too generic
return ApiResponse.success("Success", updatedUser);

// ❌ BAD - Technical jargon
return ApiResponse.success("User entity persisted to database", updatedUser);

4. Handle Exceptions Properly

// βœ… GOOD - Use custom ApiException
public class InsufficientFundsException extends ApiException {
    public InsufficientFundsException(String accountId) {
        super("Insufficient funds in account: " + accountId, HttpStatus.PAYMENT_REQUIRED);
    }
}

// In your service
if (account.getBalance() < amount) {
    throw new InsufficientFundsException(account.getId());
}

// ❌ AVOID - Generic exceptions
throw new RuntimeException("Not enough money");

5. Leverage Trace IDs for Debugging

// Log the trace ID from incoming requests
@PostMapping
public ResponseEntity<ApiResponse<User>> createUser(@RequestBody UserDto dto) {
    UUID traceId = UUID.randomUUID();
    log.info("Processing user creation request, traceId: {}", traceId);
    
    User user = userService.create(dto);
    
    // Create response with same trace ID
    ApiResponse<User> response = ApiResponse.<User>builder()
        .status(HttpStatus.CREATED.value())
        .traceId(traceId)
        .message("User created successfully")
        .data(user)
        .build();
    
    return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

6. Caching Considerations

// ⚠️ CAUTION - Response includes timestamp, making caching difficult
// Consider extracting just the data for caching:

@Cacheable("users")
public User getUserData(Long id) {
    return userRepository.findById(id).orElseThrow();
}

@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
    // Fresh response wrapper, cached data
    return ApiResponse.success("User found", getUserData(id));
}

πŸ“‹ Migration Guide

⚠️ Upgrading from 2.0.0 to 3.0.0 - BREAKING CHANGES

Version 3.0.0 is a MAJOR release with BREAKING CHANGES. Please read carefully.

🚨 Breaking Change #1: Lombok Removed

v2.0.0: Used Lombok with @Builder annotation v3.0.0: Pure Java with manual builder pattern

Migration Steps:

If you were using the builder directly (rare):

// BEFORE (v2.0.0 - won't work in v3.0.0)
ApiResponse<User> response = ApiResponse.<User>builder()
    .status(200)
    .message("Success")
    .data(user)  // Also note: "data" changed to "content"
    .build();

// AFTER (v3.0.0)
ApiResponse<User> response = new ApiResponse.ApiResponseBuilder<User>()
    .message("Success")
    .content(user)  // Changed from "data" to "content"
    .build();
// Note: status and timestamp are set automatically

Recommended: Use static factory methods (no changes needed):

// This works in BOTH v2.0.0 and v3.0.0
return ApiResponse.success("User found", user);
return ApiResponse.created("User created", newUser);
return ApiResponse.status("Accepted", HttpStatus.ACCEPTED);

🚨 Breaking Change #2: No traceId in Success Responses

v2.0.0: Success responses (ApiResponse) included traceId field v3.0.0: Success responses NO LONGER include traceId field

Impact:

  • Success response JSON no longer has traceId field
  • Error responses (ProblemDetail) still have traceId

Before (v2.0.0):

{
  "status": 200,
  "traceId": "550e8400-e29b-41d4-a716-446655440000",
  "message": "Success",
  "content": {...},
  "timestamp": "2026-02-13T10:30:45.123Z"
}

After (v3.0.0):

{
  "status": 200,
  "message": "Success",
  "content": {...},
  "timestamp": "2026-02-13T10:30:45.123Z"
}

Migration Options:

  1. Use MDC for trace IDs (Recommended):

    String traceId = MDC.get("traceId");
    log.info("Processing request with traceId: {}", traceId);
  2. Use Response Headers:

    return ResponseEntity.ok()
        .header("X-Trace-Id", traceId)
        .body(ApiResponse.success("Success", data));
  3. Implement TraceIdFilter: Add trace ID to MDC and response headers automatically

βœ… Non-Breaking Changes

  • RFC 9457 (updated from RFC 7807) - Same structure, updated standard
  • All static factory methods work the same
  • Exception handling unchanged
  • Auto-configuration unchanged

Migration Checklist

  • Update dependency to v3.0.0
  • If using builder directly: Update to new ApiResponse.ApiResponseBuilder<T>()
  • Update client code expecting traceId in success responses
  • Test your application thoroughly
  • Update any documentation referencing traceId in success responses

Upgrading from 1.3.0 to 2.0.0 (Deprecated - Skip to 3.0.0)

⚠️ Note: v2.0.0 is deprecated. Upgrade directly to v3.0.0 instead.

Version 2.0.0 was fully backward compatible with v1.3.0 but used Lombok.

Upgrading from 1.3.0 to 3.0.0 (Recommended)

Version 1.3.0 introduces auto-configuration. No breaking changes - fully backward compatible.

What's New

  • βœ… Spring Boot auto-configuration (zero config needed)
  • βœ… Enhanced JavaDoc documentation
  • βœ… Type mismatch error handler added

Migration Steps

  1. Update dependency version:
<dependency>
    <groupId>io.github.pasinduog</groupId>
    <artifactId>api-response</artifactId>
    <version>2.0.0</version>  <!-- Changed from 1.3.0 -->
</dependency>
  1. Remove manual component scanning (optional):
// BEFORE (1.2.0)
@SpringBootApplication
@ComponentScan(basePackages = {"com.yourapp", "io.github.pasinduog.exception"})
public class Application { }

// AFTER (1.3.0) - No need for manual scanning
@SpringBootApplication
public class Application { }
  1. Verify auto-configuration (optional):
# application.properties - Enable debug logging to verify
logging.level.io.github.pasinduog=DEBUG

Upgrading from 1.1.0 to 1.2.0

⚠️ Note: This version (1.2.0) included traceId in ApiResponse, which was later removed in v3.0.0.

Version 1.2.0 adds trace IDs and status fields. Backward compatible with response structure changes.

What's New

  • βœ… traceId field (UUID) for distributed tracing (Removed in v3.0.0)
  • βœ… status field (Integer) in response body
  • βœ… Custom ApiException support
  • βœ… Instant timestamp (was LocalDateTime)

Migration Steps

  1. Update dependency to 1.2.0+

  2. Update response assertions in tests:

// Add new field checks
.andExpect(jsonPath("$.status").exists())
.andExpect(jsonPath("$.traceId").exists())
  1. Optional: Create custom exceptions:
public class ResourceNotFoundException extends ApiException {
    public ResourceNotFoundException(String resource, Long id) {
        super(String.format("%s not found with ID: %d", resource, id), HttpStatus.NOT_FOUND);
    }
}

Upgrading from 1.0.0 to 1.1.0

Version 1.1.0 adds GlobalExceptionHandler. No breaking changes.

What's New

  • βœ… GlobalExceptionHandler with RFC 9457 ProblemDetail (latest standard, supersedes RFC 7807)
  • βœ… Automatic validation error handling
  • βœ… SLF4J logging integration with trace IDs

Migration Steps

  1. Update dependency to 1.1.0+

  2. Remove custom exception handlers (optional):

If you were manually handling validation errors, you can now remove that code as it's handled automatically.

πŸ”’ Security Considerations

1. Exception Message Sanitization

The library's GlobalExceptionHandler provides safe defaults, but be mindful:

// βœ… SAFE - No sensitive data
throw new ApiException("User not found", HttpStatus.NOT_FOUND);

// ⚠️ CAUTION - May leak sensitive information
throw new ApiException("Database connection failed: " + sqlException.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);

// βœ… BETTER - Generic message
throw new ApiException("An internal error occurred", HttpStatus.INTERNAL_SERVER_ERROR);

2. Trace ID Privacy

Trace IDs are UUIDs and don't contain sensitive information. However:

  • Don't log sensitive data alongside trace IDs
  • Consider rate limiting to prevent trace ID enumeration
  • Rotate logs regularly to limit exposure
// βœ… SAFE
log.info("User created successfully, traceId: {}", traceId);

// ❌ UNSAFE - Logs password
log.info("User created, traceId: {}, password: {}", traceId, password);

3. Stack Trace Exposure

The GlobalExceptionHandler never exposes stack traces to clients. Stack traces are:

  • βœ… Logged server-side for debugging
  • ❌ Never sent in API responses
  • βœ… Replaced with generic messages

4. Validation Error Details

Validation errors include field names and constraints:

{
  "status": 400,
  "detail": "Validation Failed",
  "errors": {
    "email": "must be a well-formed email address",
    "password": "must not be blank"
  }
}

Security Tips:

  • βœ… Don't include sensitive field values in error messages
  • βœ… Use generic constraint messages for sensitive fields
  • βœ… Consider custom validators for sensitive data
public class SensitiveDto {
    @NotBlank(message = "Required field is missing")  // Generic message
    private String creditCardNumber;
    
    @Pattern(regexp = "...", message = "Invalid format")  // No details
    private String ssn;
}

5. CORS & Security Headers

The library doesn't interfere with Spring Security or CORS configuration:

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(csrf -> csrf.disable())
            // ... other configurations
        return http.build();
    }
}

6. Dependency Security

The library uses provided scope dependencies:

  • βœ… No transitive dependency vulnerabilities
  • βœ… Uses your application's Spring Boot version
  • βœ… No additional security surface area

Verify with:

mvn dependency:tree -Dincludes=io.github.pasinduog:api-response

7. ProblemDetail Information Disclosure

RFC 9457 ProblemDetail responses (supersedes RFC 7807) include:

  • type - URI reference (defaults to "about:blank")
  • title - Short, human-readable summary
  • status - HTTP status code
  • detail - Human-readable explanation
  • instance - URI reference (not used by default)

Best Practice: Don't include internal system details in error messages.

🀝 Contributing

We welcome and encourage contributions from everyone! πŸŽ‰

This project is licensed under the Apache License 2.0, which means:

  • βœ… Anyone can contribute - The project is open to all developers
  • βœ… Your contributions are protected - No one can claim exclusive ownership of your work
  • βœ… You retain copyright - You keep rights to your work while granting usage rights to the project
  • βœ… Fair and equal terms - All contributions are made under the same Apache 2.0 terms

πŸ” Contributor License Agreement

By contributing to this project, you agree that:

  1. Your contributions will be licensed under the Apache License 2.0
  2. You grant the project maintainers and users a perpetual, worldwide license to use your contributions
  3. You retain copyright to your contributions
  4. You confirm that you have the right to submit the contribution and grant these rights
  5. Your work is protected - No one (including the project maintainer) can claim exclusive ownership of your contributions or publish them elsewhere as their own

πŸ“œ What Apache 2.0 Means for Contributors

βœ… Your Rights:

  • You keep copyright to your contributions
  • Your name is attributed in the project
  • You can use your contributions elsewhere
  • You're protected from patent claims

πŸ›‘οΈ Project Protection:

  • Prevents anyone from copyrighting the project - No one can claim exclusive ownership
  • Ensures the project remains open source
  • Protects all contributors equally
  • Maintains free usage for everyone
  • Cannot be published elsewhere as proprietary - Derivative works must maintain the Apache 2.0 License

How to Contribute

  1. Fork the repository on GitHub

  2. Clone your fork locally

    git clone https://github.com/YOUR_USERNAME/api-response.git
    cd api-response
  3. Create a feature branch

    git checkout -b feature/amazing-feature
  4. Make your changes

    • Follow the existing code style and conventions
    • Add JavaDoc comments for all public methods and classes
    • Ensure all existing tests pass (when tests are added)
    • Keep changes focused and atomic
  5. Commit your changes (follow conventional commit format)

    git commit -m 'feat: add amazing feature'
  6. Push to your fork

    git push origin feature/amazing-feature
  7. Open a Pull Request on the main repository

    • Go to the original repository on GitHub
    • Click "New Pull Request"
    • Select your branch and describe your changes in detail
    • Reference any related issues

Contribution Guidelines

Code Quality Standards

  • βœ… Follow existing code style and conventions
  • βœ… Add comprehensive JavaDoc comments for all public methods and classes
  • βœ… Include explicit constructor documentation for all constructors
  • βœ… Ensure all existing tests pass
  • βœ… Keep changes focused and atomic
  • βœ… Update README.md if adding new features or changing behavior
  • βœ… Verify zero Javadoc warnings (mvn javadoc:javadoc should run cleanly)

Commit Message Format

Follow Conventional Commits specification:

# Format: <type>(<scope>): <subject>

# Types:
feat: add new response wrapper method
fix: correct trace ID generation
docs: update installation instructions
refactor: improve exception handling
test: add unit tests for ApiResponse
style: format code and fix whitespace
chore: update dependencies
perf: optimize response creation

Examples:

feat(dto): add pagination support to ApiResponse
fix(exception): resolve NPE in GlobalExceptionHandler
docs(readme): add contributing guidelines
test(response): add unit tests for success factory methods

Pull Request Process

  1. Update documentation - Modify README.md if adding features or changing behavior
  2. Add JavaDoc - Document all new public methods and classes with zero warnings
  3. Write clear PR description:
    • What problem does this solve?
    • What changes were made?
    • How was it tested?
    • Any breaking changes?
  4. Reference issues - Link related issues using #issue-number or Fixes #issue-number
  5. Wait for review - Maintainer will review and provide feedback
  6. Address feedback - Make requested changes and push updates
  7. Squash commits - Clean up commit history if needed before merge

Development Setup

# Clone the repository
git clone https://github.com/pasinduog/api-response.git
cd api-response

# Build the project
mvn clean install

# Generate JavaDoc (builds with zero warnings)
mvn javadoc:javadoc

# Generate JavaDoc JAR (clean build with comprehensive documentation)
mvn javadoc:jar

# Package for Maven Central (requires GPG key)
mvn clean deploy -P release

Note: The project includes explicit constructor documentation for all classes, ensuring zero Javadoc warnings during the build process. All constructors (including Spring bean constructors) are properly documented.

Project Structure for Contributors

api-response/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ main/
β”‚   β”‚   β”œβ”€β”€ java/io/github/pasinduog/
β”‚   β”‚   β”‚   β”œβ”€β”€ config/           # Auto-configuration classes
β”‚   β”‚   β”‚   β”œβ”€β”€ dto/              # Response wrapper classes
β”‚   β”‚   β”‚   └── exception/        # Exception handling
β”‚   β”‚   └── resources/
β”‚   β”‚       └── META-INF/spring/  # Auto-configuration metadata
β”‚   └── test/java/                # Unit tests (to be added)
β”œβ”€β”€ pom.xml                       # Maven configuration
└── README.md                     # Documentation

What We're Looking For

  • πŸ› Bug fixes
  • πŸ“ Documentation improvements
  • ✨ New features (discuss in issue first)
  • πŸ§ͺ Test coverage improvements
  • 🎨 Code quality enhancements
  • 🌐 Internationalization support

Code of Conduct

  • Be respectful and inclusive
  • Provide constructive feedback

πŸ“„ License

This project is licensed under the Apache License 2.0 - see the LICENSE file for full details.

What This Means

The Apache 2.0 License is a permissive open-source license that:

βœ… Allows You To:

  • βœ… Use this library in personal, commercial, or proprietary projects
  • βœ… Modify and distribute the source code
  • βœ… Sublicense the code
  • βœ… Use the software for any purpose (private, commercial, etc.)
  • βœ… Patent protection - contributors grant you patent rights

πŸ›‘οΈ Protects The Project:

  • πŸ›‘οΈ No one can claim ownership - Others cannot copyright this work as their own
  • πŸ›‘οΈ Attribution required - Anyone using or modifying this code must give credit
  • πŸ›‘οΈ License propagation - Modified versions must include the Apache 2.0 License
  • πŸ›‘οΈ Trademark protection - Project name and trademarks remain protected
  • πŸ›‘οΈ No liability - Software is provided "as-is" without warranty

🀝 For Contributors:

  • 🀝 Open contribution - Anyone can contribute under the same terms
  • 🀝 Grant of rights - By contributing, you grant Apache 2.0 rights to your contributions
  • 🀝 Your work is protected - Contributions are attributed and cannot be claimed by others
  • 🀝 Patent peace - Contributors cannot sue users for patent infringement related to their contributions

Key License Terms

Copyright 2026 Pasindu OG

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Summary for Users

βœ… You CAN:

  • Use in commercial products
  • Modify the code
  • Distribute copies
  • Grant sublicenses
  • Use privately

❌ You CANNOT:

  • Hold the author liable
  • Use contributor names for endorsement
  • Remove copyright notices
  • Claim ownership of the original work

πŸ“‹ You MUST:

  • Include the Apache 2.0 License
  • Include copyright notice
  • State significant changes made
  • Include the NOTICE file (if applicable)

For the complete license text, see LICENSE or visit Apache.org.

πŸ“§ Contact

Pasindu OG

πŸ™ Acknowledgments

  • Spring Boot team for the excellent framework
  • The open-source community for inspiration and support

πŸ“ˆ Version History

3.0.0 (February 13, 2026) - ⚠️ MAJOR RELEASE WITH BREAKING CHANGES

βœ… Now Available on Maven Central!

⚠️ BREAKING CHANGES - Read Before Upgrading

This is a MAJOR version release with breaking changes. Please review carefully before upgrading from v2.0.0.

🚨 Breaking Changes:

  1. ❌ Removed Lombok Dependency - BREAKING CHANGE

    • v2.0.0 used Lombok with @Builder annotation
    • v3.0.0 uses pure Java with manual builder pattern
    • Impact: If your code depends on Lombok-generated methods, you must update
    • Migration: Use the new ApiResponse.ApiResponseBuilder<T>() constructor
  2. ❌ Removed traceId Field from ApiResponse - BREAKING CHANGE

    • v2.0.0 included traceId in success responses (ApiResponse)
    • v3.0.0 removes traceId from success responses entirely
    • Impact: Success response JSON no longer includes traceId field
    • Migration: Trace IDs are now only in error responses (ProblemDetail)
    • Use MDC or response headers for trace ID propagation instead
  3. βœ… Updated RFC Standard - Non-Breaking

    • Upgraded from RFC 7807 to RFC 9457 (latest ProblemDetail standard)
    • Same structure, just updated specification

βœ… New Features:

  • Enhanced Exception Handling - Added 6 new exception handlers for comprehensive error coverage (total: 10 handlers)
    • HttpMessageNotReadableException - Malformed JSON body handling (HTTP 400)
    • MissingServletRequestParameterException - Missing required parameters (HTTP 400)
    • NoResourceFoundException - 404 Not Found for endpoints/resources (HTTP 404)
    • HttpRequestMethodNotSupportedException - Invalid HTTP method (HTTP 405)
    • HttpMediaTypeNotSupportedException - Unsupported Content-Type (HTTP 415)
    • NullPointerException - Null pointer handling with detailed stack traces (HTTP 500)
  • TraceIdFilter - Automatic trace ID generation and MDC integration for distributed tracing
  • RFC 9457 Compliance - Updated to latest RFC 9457 (supersedes RFC 7807) for ProblemDetail format
  • Production-Ready Error Responses - Clear, actionable error messages for all common scenarios
  • Zero External Dependencies - Removed Lombok dependency, now pure Java implementation
    • Custom builder pattern implementation (no annotation processing required)
    • Simpler setup - no IDE plugins needed
    • Faster compilation - no annotation processing overhead
  • Complete Javadoc Coverage - Zero warnings with comprehensive documentation
    • Added package-info.java for all 5 packages
    • Full @param, @return, @throws documentation
    • Constructor documentation for all classes
    • Enhanced class and method descriptions
  • Apache 2.0 License - Comprehensive license documentation with contributor protections
  • Maven Javadoc Plugin Configuration - Optimized settings for zero-warning builds
  • Major version bump to 3.0.0 due to breaking changes
  • Exception handling and auto-configuration features from v2.0.0 and v1.3.0 maintained

πŸ”§ Improvements:

  • Stabilized API for long-term support (LTS) with semantic versioning
  • Enhanced Documentation Quality:
    • Zero Javadoc warnings during build process
    • Complete API documentation with all @param, @return, @throws tags
    • Explicit constructor documentation for all classes
    • Package-level documentation for better organization
  • Better Error Messages with specific, actionable details for all 10 exception types
  • Improved Logging with consistent trace IDs across all exception handlers
  • Performance Optimizations:
    • Immutable response objects for thread safety
    • Efficient builder pattern implementation
    • Minimal memory footprint (~200 bytes per response)
    • Fast response time (<1ms overhead)
  • Build Quality Improvements:
    • Clean Maven builds with zero warnings
    • Optimized javadoc plugin configuration
    • Production-ready artifact generation
    • Enhanced CI/CD compatibility

πŸ“ Documentation:

  • Complete Javadoc Coverage - 100% documentation across all classes with zero warnings
    • ApiResponse - Full documentation including builder pattern with @param and @return tags
    • ApiException - Abstract base class with comprehensive documentation
    • GlobalExceptionHandler - All 10 exception handlers fully documented
    • TraceIdFilter - Filter implementation with @throws documentation
    • ApiResponseAutoConfiguration - Auto-configuration with detailed setup guide
  • Package Documentation - Added package-info.java files for all 5 packages:
    • io.github.pasinduog - Root package overview
    • io.github.pasinduog.config - Configuration classes documentation
    • io.github.pasinduog.dto - Data transfer objects documentation
    • io.github.pasinduog.exception - Exception handling documentation
    • io.github.pasinduog.filter - Servlet filters documentation
  • Enhanced JavaDoc Quality:
    • Added explicit constructor documentation for all classes
    • Added comprehensive @param, @return, @throws tags
    • Added @author, @version, @since tags to all classes
    • Field-level documentation for all public/protected fields
    • Detailed class-level descriptions with usage examples
  • Updated all code examples and guides
  • Added documentation for all 6 new exception handlers

πŸ”§ Technical Updates:

  • Maven Javadoc Plugin - Optimized configuration for zero-warning builds
    • Configured doclint:none for flexible documentation standards
    • Added failOnError:false and failOnWarnings:false for robust builds
    • Set detectJavaApiLink:false to avoid external API link issues
    • Optimized for Java 17+ with source level configuration
  • Removed Lombok Dependency - Pure Java implementation for better compatibility
    • No IDE plugin requirements
    • No annotation processing overhead
    • Simpler build process
    • Better IDE support out of the box
  • Maintained full compatibility with Spring Boot 3.2.0 - 4.0.2
  • Continued support for Java 17+ (Java 21 LTS recommended)
  • All Maven plugins updated to latest stable versions
  • Updated GlobalExceptionHandler with 10 comprehensive exception handlers
  • Clean Build Process - Zero Javadoc warnings during mvn javadoc:jar
  • Production-ready artifacts with complete documentation

2.0.0 (February 2026) - Deprecated - Use v3.0.0 Instead

⚠️ This version has been superseded by v3.0.0

This version included Lombok dependency and traceId in success responses, which have been removed in v3.0.0 for better compatibility and simpler implementation.

βœ… Features (Now in v3.0.0 with improvements):

  • Exception handling with ProblemDetail (RFC 7807)
  • Auto-configuration support
  • Trace ID support (with traceId in ApiResponse - removed in v3.0.0)
  • Lombok-based builder pattern (changed to manual builder in v3.0.0)

Recommendation: Upgrade to v3.0.0 for pure Java implementation without Lombok dependency.

1.3.0 (February 4, 2026) - Auto-Configuration & Stability Release

βœ… New Features:

  • Spring Boot Auto-Configuration - Added ApiResponseAutoConfiguration with automatic component registration
  • META-INF Auto-Configuration File - Included org.springframework.boot.autoconfigure.AutoConfiguration.imports for Spring Boot 3.x
  • Zero Manual Configuration - No more need for @ComponentScan or @Import annotations
  • Type Mismatch Error Handler - Added MethodArgumentTypeMismatchException handling for better error messages
  • Spring Boot 4.0.2 Support - Verified compatibility with the latest Spring Boot 4.x release

πŸ”§ Improvements:

  • Updated Spring Boot version support to 4.0.2 for latest features and security
  • Enhanced project stability and dependency management
  • Improved JavaDoc documentation across all classes with comprehensive examples
  • Added @since tags to all classes for better version tracking
  • Refined build process and artifact generation
  • Enhanced exception handling with more descriptive type conversion error messages
  • Updated Maven plugins: maven-source-plugin (3.3.0), maven-javadoc-plugin (3.6.3), maven-gpg-plugin (3.1.0)

πŸ“ Documentation:

  • Added comprehensive auto-configuration documentation
  • Updated FAQ section with auto-configuration details
  • Enhanced all JavaDoc comments with detailed descriptions and examples
  • Added migration notes for users upgrading from previous versions
  • Added type mismatch error handling documentation
  • Added performance benchmarks and characteristics
  • Added JSON serialization behavior documentation
  • Added Quick Links section for easy navigation
  • Added Before/After comparison examples
  • Added IDE setup instructions for contributors

πŸ”§ Technical Updates:

  • Maintained compatibility with Java 17+ and Spring Boot 3.2.0 - 4.0.2
  • Tested and verified full compatibility with Spring Boot 4.0.2
  • Enhanced Maven Central publishing workflow with updated plugin versions
  • Improved package structure and organization
  • Updated build plugins: maven-source-plugin (3.3.0), maven-javadoc-plugin (3.6.3), maven-gpg-plugin (3.1.0)

1.2.0 (February 2026) - Enhanced Response & Custom Exceptions

βœ… New Features:

  • Custom ApiException Support - Abstract base class for creating domain-specific business exceptions
  • Automatic ApiException Handling - GlobalExceptionHandler now catches and formats custom ApiException instances
  • Response Status Field - Added status field to ApiResponse for explicit HTTP status code in response body
  • Trace ID Support - GlobalExceptionHandler adds traceId (UUID) to error responses for distributed tracing and log correlation
  • Improved Timestamp Format - Changed from LocalDateTime to Instant (UTC) for consistent timezone handling

πŸ”§ Improvements:

  • Better support for microservices architecture with trace IDs in error responses
  • Enhanced debugging capabilities with status codes in response body
  • Cleaner exception handling pattern for business logic errors
  • More consistent timestamp format across all responses

πŸ“ Documentation:

  • Added comprehensive examples for custom ApiException usage
  • Updated all response examples to include new fields
  • Enhanced best practices section

1.1.0 (February 2026) - Exception Handling Update

βœ… New Features:

  • Built-in GlobalExceptionHandler with ProblemDetail (RFC 7807) support
  • Automatic validation error handling for @Valid annotations
  • Comprehensive exception logging with SLF4J
  • Null pointer exception handling
  • Standardized error response format with timestamps

πŸ”§ Improvements:

  • Enhanced error responses with structured format
  • Better integration with Spring Boot validation
  • Automatic error field aggregation for validation failures

1.0.0 (February 2026) - Initial Release

βœ… Core Features:

  • Standard API Response wrapper with generic type support
  • Five static factory methods: success(), created(), status()
  • Automatic ISO-8601 timestamp generation
  • Full Spring Boot 3.2.0+ integration
  • Immutable, thread-safe design
  • Comprehensive JavaDoc documentation

🎯 Roadmap:

  • Spring WebFlux support (reactive)
  • Pagination metadata support
  • OpenAPI schema generation
  • Additional exception handlers
  • Internationalization (i18n) support
  • Response compression support
  • Custom serialization options
  • Metrics and monitoring integration

πŸ“‹ Summary

The API Response Library is a production-ready, zero-configuration solution for standardizing REST API responses in Spring Boot applications.

🎯 Why Choose This Library?

βœ… Instant Setup - Add dependency, start using. No configuration needed.
βœ… Battle-Tested - Used in production Spring Boot applications
βœ… Modern Standards - RFC 9457 ProblemDetail (v3.0.0+), Spring Boot 4.x support βœ… Developer Friendly - Comprehensive docs, clear examples, active maintenance
βœ… Lightweight - Only ~10KB, zero runtime dependencies
βœ… Type Safe - Full generic support with compile-time checking
βœ… Pure Java - No Lombok or external dependencies (v3.0.0+)

πŸ“Š Quick Stats

Metric Value
JAR Size ~10KB
Response Time < 1ms
Memory per Response ~200 bytes
Thread Safety 100%
Spring Boot Support 3.2.0 - 4.0.2
Java Version 17+

πŸ”— Quick Access

  • πŸ“¦ Maven Central - Download & integration
  • πŸ“š JavaDoc - Complete API documentation
  • πŸ› Issues - Report bugs or request features
  • πŸ’¬ Discussions - Ask questions & share ideas

⭐ If you find this library helpful, please give it a star on GitHub!

Made with ❀️ by Pasindu OG

About

A lightweight, production-ready Spring Boot library to standardize REST API responses and Global Exception Handling (RFC Standard). Includes automatic timestamps and validation support.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages