The OrderFlow.Core API uses a standardized ApiResponse<T> wrapper pattern for all endpoints, providing consistent response structure, clear success/failure indication, and better error handling.
{
"success": true,
"message": "Operation completed successfully",
"data": { /* actual response data */ },
"errors": null,
"timestamp": "2024-01-20T10:30:00Z"
}| Property | Type | Description |
|---|---|---|
success |
boolean | Indicates whether the operation was successful |
message |
string | Human-readable message describing the result |
data |
T (generic) | The actual response payload (type varies by endpoint) |
errors |
string[] | List of error messages (only present on failure) |
timestamp |
DateTime | UTC timestamp when the response was generated |
Request: POST /api/orders
{
"customerName": "John Doe",
"productName": "Laptop",
"quantity": 1,
"totalAmount": 1299.99
}Response: 200 OK
{
"success": true,
"message": "Order created and published successfully",
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"customerName": "John Doe",
"productName": "Laptop",
"quantity": 1,
"totalAmount": 1299.99,
"status": "Created",
"createdAt": "2024-01-20T10:30:00Z"
},
"errors": null,
"timestamp": "2024-01-20T10:30:00Z"
}Request: POST /api/orders/{orderId}/payment
Response: 200 OK
{
"success": true,
"message": "Payment verification event published successfully",
"data": {
"orderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"eventType": "payment.verified"
},
"errors": null,
"timestamp": "2024-01-20T10:35:00Z"
}Response: 500 Internal Server Error
{
"success": false,
"message": "An error occurred while creating the order",
"data": null,
"errors": [
"Connection to RabbitMQ failed: Unable to connect to broker"
],
"timestamp": "2024-01-20T10:30:00Z"
}- Endpoint:
POST /api/orders - Success Response:
ApiResponse<OrderResponseDto> - Data Structure:
{ id: string (GUID), customerName: string, productName: string, quantity: number, totalAmount: number, status: string, createdAt: DateTime }
- Endpoint:
POST /api/orders/{orderId}/payment - Success Response:
ApiResponse<OrderOperationResponseDto> - Data Structure:
{ orderId: string (GUID), eventType: string }
- Endpoint:
POST /api/orders/{orderId}/ship - Success Response:
ApiResponse<OrderOperationResponseDto> - Data Structure: Same as Verify Payment
- Endpoint:
POST /api/orders/{orderId}/deliver - Success Response:
ApiResponse<OrderOperationResponseDto> - Data Structure: Same as Verify Payment
- Endpoint:
DELETE /api/orders/{orderId} - Success Response:
ApiResponse<OrderOperationResponseDto> - Data Structure: Same as Verify Payment
interface ApiResponse<T> {
success: boolean;
message: string;
data?: T;
errors?: string[];
timestamp: string;
}
interface OrderResponseDto {
id: string;
customerName: string;
productName: string;
quantity: number;
totalAmount: number;
status: string;
createdAt: string;
}
// Example: Creating an order
async function createOrder(orderData: any): Promise<OrderResponseDto | null> {
try {
const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(orderData)
});
const apiResponse: ApiResponse<OrderResponseDto> = await response.json();
if (apiResponse.success) {
console.log('Success:', apiResponse.message);
return apiResponse.data!;
} else {
console.error('Failed:', apiResponse.message);
console.error('Errors:', apiResponse.errors);
return null;
}
} catch (error) {
console.error('Request failed:', error);
return null;
}
}
// Accessing order properties
const order = await createOrder({ /* ... */ });
if (order) {
console.log(`Order ID: ${order.id}`);
console.log(`Customer: ${order.customerName}`);
console.log(`Status: ${order.status}`);
}public class ApiResponse<T>
{
public bool Success { get; set; }
public string Message { get; set; }
public T? Data { get; set; }
public List<string>? Errors { get; set; }
public DateTime Timestamp { get; set; }
}
// Example: Consuming the API
public async Task<OrderResponseDto?> CreateOrderAsync(CreateOrderRequestDto request)
{
var response = await _httpClient.PostAsJsonAsync("/api/orders", request);
var apiResponse = await response.Content.ReadFromJsonAsync<ApiResponse<OrderResponseDto>>();
if (apiResponse?.Success == true)
{
Console.WriteLine($"Success: {apiResponse.Message}");
return apiResponse.Data; // Direct access to order data
}
else
{
Console.WriteLine($"Failed: {apiResponse?.Message}");
if (apiResponse?.Errors != null)
{
foreach (var error in apiResponse.Errors)
{
Console.WriteLine($"Error: {error}");
}
}
return null;
}
}import requests
from typing import Optional, Dict, Any
def create_order(order_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
Creates an order and returns the order data if successful.
"""
response = requests.post(
'http://localhost:8080/api/orders',
json=order_data
)
api_response = response.json()
if api_response['success']:
print(f"Success: {api_response['message']}")
return api_response['data'] # Direct access to order
else:
print(f"Failed: {api_response['message']}")
if api_response.get('errors'):
for error in api_response['errors']:
print(f"Error: {error}")
return None
# Usage
order = create_order({
'customerName': 'John Doe',
'productName': 'Laptop',
'quantity': 1,
'totalAmount': 1299.99
})
if order:
print(f"Order ID: {order['id']}")
print(f"Customer: {order['customerName']}")
print(f"Status: {order['status']}")✅ Consistency: All endpoints return the same structure
✅ Predictability: Clients know exactly what to expect
✅ Error Handling: Standardized error reporting with detailed messages
✅ Type Safety: Generic type parameter ensures compile-time checking
✅ Debugging: Timestamp helps with troubleshooting
✅ Extensibility: Easy to add metadata (correlation IDs, trace IDs, etc.)
✅ API Documentation: Better Swagger/OpenAPI documentation generation
✅ Simplicity: Flat data structure for easy access
We use a flat response structure for the CreateOrder endpoint:
{
"success": true,
"message": "Order created and published successfully",
"data": {
"id": "...",
"customerName": "...",
"productName": "...",
// ... direct order properties
}
}Benefits:
- Easier client-side access (
response.data.idvsresponse.data.model.id) - Consistent with other endpoints
- Reduced nesting complexity
- Better developer experience
See Also: API Response Simplification Guide for details on this design decision.
When adding new endpoints, follow this pattern:
[HttpPost("example")]
[ProducesResponseType(typeof(ApiResponse<YourDataDto>), 200)]
[ProducesResponseType(typeof(ApiResponse<YourDataDto>), 500)]
public async Task<ActionResult<ApiResponse<YourDataDto>>> ExampleEndpoint([FromBody] YourRequestDto request)
{
try
{
// Your business logic here
var data = new YourDataDto { /* ... */ };
var response = ApiResponse<YourDataDto>.SuccessResponse(
data,
"Operation completed successfully");
return Ok(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in example endpoint");
var errorResponse = ApiResponse<YourDataDto>.FailureResponse(
"An error occurred",
ex.Message);
return StatusCode(500, errorResponse);
}
}| Status Code | When Used | Response Structure |
|---|---|---|
| 200 OK | Successful operation | ApiResponse<T> with success: true |
| 400 Bad Request | Validation errors | ApiResponse<T> with success: false |
| 404 Not Found | Resource not found | ApiResponse<T> with success: false |
| 500 Internal Server Error | Server errors | ApiResponse<T> with success: false |
- v1.1 - Simplified Create Order response structure (removed
CreateResponseDtowrapper) - v1.0 - Initial implementation with generic ApiResponse pattern
- All endpoints migrated to use consistent response structure
- Added ProducesResponseType attributes for better Swagger documentation
- API Response Simplification Guide - Details on response structure simplification
- Testing Guide - API testing examples
- Main README - Project overview and quick start