Request Payload Structure
Use a structured format like JSON or XML for request payloads.
Use camelCase for field names to align with JavaScript conventions.
Define required fields and optional fields explicitly in API documentation.
Use PATCH
for partial updates with only the fields that need to be changed.
Validate the input request properly
Use proper validation messages and handle exceptions .
Response Design
Flat Response:
Use for simple resources with minimal metadata.
Copy {
"id" : 1 ,
"title" : "Product A" ,
"price" : 100.0
}
Envelope Response:
Use for more complex responses with metadata, pagination, or additional information.
Copy @ Getter
@ Builder
public class ApiResponse < T > {
private String status; // SUCCESS, ERROR, PARTIAL_SUCCESS
private String message; // Human readable message
private T data; // Actual response data
private List < String > errors; // List of error messages
private LocalDateTime timestamp; // Response timestamp
private String traceId; // For request tracing
public static < T > ApiResponse < T > success ( T data) {
return ApiResponse . < T > builder()
. status ( "SUCCESS" )
. data (data)
. timestamp ( LocalDateTime . now ())
. build ();
}
public static < T > ApiResponse < T > error ( String message , List < String > errors) {
return ApiResponse . < T > builder()
. status ( "ERROR" )
. message (message)
. errors (errors)
. timestamp ( LocalDateTime . now ())
. build ();
}
}
Specific Response Objects
Copy @ Getter
@ Builder
public class OrderResponse {
private String orderId;
private String status;
private BigDecimal totalAmount;
private List < OrderItemResponse > items;
private ShippingDetails shipping;
private PaymentDetails payment;
@ JsonInclude ( Include . NON_NULL )
private List < String > warnings; // Optional warnings
}
Paginated Response
Use offset
(starting point) and limit
(number of items to return).
Example:
/products?offset=10&limit=20
Copy {
"status" : "success" ,
"data" : [
{ "id" : 11 , "name" : "Product 11" } ,
{ "id" : 12 , "name" : "Product 12" }
] ,
"meta" : {
"total" : 100 ,
"offset" : 10 ,
"limit" : 20
}
}
Use page
and size
.
Example Request:
/products?page=2&size=20
Copy {
"status" : "success" ,
"data" : [
{ "id" : 21 , "name" : "Product 21" } ,
{ "id" : 22 , "name" : "Product 22" }
] ,
"meta" : {
"totalPages" : 5 ,
"currentPage" : 2 ,
"pageSize" : 20
}
}
Use a cursor (e.g., unique ID or timestamp) for fetching the next set of records.
Example Request:
/products?cursor=abc123&limit=20
Copy {
"status" : "success" ,
"data" : [
{ "id" : 31 , "name" : "Product 31" } ,
{ "id" : 32 , "name" : "Product 32" }
] ,
"meta" : {
"nextCursor" : "xyz456" ,
"limit" : 20
}
}
Metadata
Include metadata to provide additional context:
total: Total number of items in the collection.
currentPage: Current page number.
totalPages: Total number of pages.
nextCursor: For cursor-based pagination, the token for the next set of records.
Copy // Sample paginated response
@ Getter
@ Builder
public class PagedResponse < T > {
private List < T > data;
private int pageNumber;
private int pageSize;
private long totalElements;
private int totalPages;
private boolean hasNext;
private Map < String , Object > metadata; // For additional info like facets
}
Error Response Standardization
Standardizing error responses makes troubleshooting easier for API clients.
Code: Unique error identifier (e.g., HTTP status code or custom code).
Message: Human-readable description of the error.
Details: Additional information, such as invalid fields or suggested fixes (optional).
Timestamp: The time the error occurred (optional).
We can use Problem Detail Standardization introduced in spring 6
Copy @ Getter
@ Builder
public class ErrorResponse {
private String code; // Error code
private String message; // User-friendly message
private String details; // Technical details (optional)
private List < FieldError > fieldErrors; // Validation errors
private LocalDateTime timestamp;
@ Getter
@ Builder
public static class FieldError {
private String field;
private String message;
private String code;
}
}
@ RestControllerAdvice
public class GlobalExceptionHandler {
@ ExceptionHandler ( BusinessValidationException . class )
public ResponseEntity < ErrorResponse > handleValidationException (
BusinessValidationException ex) {
ErrorResponse error = ErrorResponse . builder ()
. code ( "VALIDATION_ERROR" )
. message ( "Validation failed" )
. fieldErrors ( ex . getErrors ())
. timestamp ( LocalDateTime . now ())
. build ();
return ResponseEntity
. badRequest ()
. body (error);
}
}
Using Problem Detail
Copy @ RestControllerAdvice
public class GlobalExceptionHandler {
@ ExceptionHandler ( ResourceNotFoundException . class )
public ProblemDetail handleResourceNotFoundException ( ResourceNotFoundException ex) {
ProblemDetail problemDetail = ProblemDetail . forStatus ( HttpStatus . NOT_FOUND );
problemDetail . setTitle ( "Resource Not Found" );
problemDetail . setDetail ( ex . getMessage ());
problemDetail . setProperty ( "errorCode" , "RESOURCE_NOT_FOUND" );
return problemDetail;
}
}
// response
{
"type" : "about:blank" ,
"title" : "Resource Not Found" ,
"status" : 404 ,
"detail" : "The requested resource was not found." ,
"instance" : "/api/users/123" ,
"errorCode" : "RESOURCE_NOT_FOUND"
}
Async Operation Response
Copy @ Getter
@ Builder
public class AsyncOperationResponse {
private String operationId; // ID to track the operation
private String status; // ACCEPTED, IN_PROGRESS, COMPLETED, FAILED
private String resourceUrl; // URL to check the status
private LocalDateTime acceptedAt;
private LocalDateTime estimatedCompletionTime;
}
@ RestController
@ RequestMapping ( "/api/v1/batch-operations" )
public class BatchOperationController {
@ PostMapping ( "/orders/import" )
public ResponseEntity < AsyncOperationResponse > importOrders (
@ RequestBody MultipartFile file) {
String operationId = batchService . startImport (file);
AsyncOperationResponse response = AsyncOperationResponse . builder ()
. operationId (operationId)
. status ( "ACCEPTED" )
. resourceUrl ( "/api/v1/batch-operations/" + operationId)
. acceptedAt ( LocalDateTime . now ())
. estimatedCompletionTime ( LocalDateTime . now () . plusMinutes ( 5 ))
. build ();
return ResponseEntity
. accepted ()
. body (response);
}
}
Streaming Response
Copy @ GetMapping (value = "/orders/stream" , produces = MediaType . TEXT_EVENT_STREAM_VALUE )
public Flux< OrderResponse > streamOrders() {
return orderService . streamOrders ()
. map (order -> OrderResponse . builder ()
. orderId ( order . getId ())
. status ( order . getStatus ())
. build ());
}
Best Practices Summary:
Request Design :
Use DTOs for request/response objects
Implement validation at multiple levels
Include tracking information
Support idempotency for non-idempotent operations
Response Design :
Use consistent response envelope
Include metadata when necessary
Support pagination for collections
Include relevant links (HATEOAS)
Error Handling :
Meaningful error messages
Performance :
Consider streaming for large datasets
Implement caching headers