Introduction
Standardizing API responses is critical for building scalable and maintainable systems. This article explores the implementation of a generic ApiResponse<T>
class in C# for Azure Functions and its corresponding TypeScript handling. We’ll also discuss the challenges of not having a standardized response, the benefits of error collections, and best practices for implementation.
Implementing ApiResponse in C
Updated ApiResponse Class
The updated implementation supports robust error handling, tracking via CorrelationId
, and human-readable errors.
public class ApiResponse<T> : BaseApiResponse
{
public T Result { get; set; }
public ApiResponse() { }
public ApiResponse(T result)
{
Result = result;
}
}
public abstract class BaseApiResponse
{
public string CorrelationId { get; set; } = Guid.NewGuid().ToString();
public IReadOnlyList<ApiError> Errors { get; protected set; } = new List<ApiError>();
public void AddError(ApiError error)
{
((List<ApiError>)Errors).Add(error);
}
public int ErrorCount => Errors.Count;
public bool IsSuccessful => !Errors.Any();
}
public class ApiError
{
public string PropertyName { get; set; }
public string ErrorMessage { get; set; }
public string ErrorCode { get; set; }
public ApiError(string propertyName, string errorMessage, string errorCode = null)
{
PropertyName = propertyName;
ErrorMessage = errorMessage;
ErrorCode = errorCode;
}
}
Example Azure Function
[FunctionName("GetData")]
public static async Task<IActionResult> GetData(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "data")] HttpRequest req,
ILogger log)
{
log.LogInformation("Processing GetData request.");
var data = new { Id = 1, Name = "Sample Data" };
var response = new ApiResponse<object>(data);
return new OkObjectResult(response);
}
Handling ApiResponse in TypeScript
Updated TypeScript Interfaces
The TypeScript interfaces mirror the updated C# implementation, supporting CorrelationId
, error collections, and the IsSuccessful
flag.
interface ApiError {
propertyName: string;
errorMessage: string;
errorCode?: string;
}
interface BaseApiResponse {
correlationId: string;
errors: ApiError[];
errorCount: number;
isSuccessful: boolean;
}
interface ApiResponse<T> extends BaseApiResponse {
result: T;
}
Example Function to Consume the API
async function fetchApiResponse<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
const data: ApiResponse<T> = await response.json();
if (!data.isSuccessful) {
console.error("Errors:", data.errors);
}
return data;
}
// Example usage
interface Data {
id: number;
name: string;
}
fetchApiResponse<Data>("https://<your-function-app-name>.azurewebsites.net/api/data")
.then(apiResponse => {
if (apiResponse.isSuccessful) {
console.log("Data fetched successfully:", apiResponse.result);
}
})
.catch(error => console.error("Error fetching data:", error));
Challenges of Not Having a Standardized Response
- Inconsistent Client Handling: Without a standard response, clients must implement custom parsing logic for each endpoint, increasing complexity.
- Ambiguous Error Handling: Errors returned inconsistently can confuse clients, especially if only HTTP status codes are used without additional details.
-
Reduced Observability: Without fields like
CorrelationId
, debugging distributed systems becomes challenging. - Business Rule Violations: Lack of clear communication for business rule violations (e.g., "Insufficient balance") complicates client-side error handling.
Advantages of an Error Collection
- Human-Readable Errors: Errors can map directly to fields in the request body, improving user experience.
{
"errors": [
{ "propertyName": "email", "errorMessage": "Email is invalid." },
{ "propertyName": "password", "errorMessage": "Password must be at least 8 characters." }
]
}
- Business Rule Violations: Clearly communicate issues like insufficient permissions or invalid operations.
{
"errors": [
{ "propertyName": "businessRule", "errorMessage": "Insufficient balance to complete the transaction." }
]
}
- Improved Debugging with Correlation ID: Track requests end-to-end across systems.
- Scalability: Add new error types without breaking existing clients.
Best Practices for Implementation
- Include Correlation ID: Pass it as a header or query parameter for end-to-end tracking.
-
Separate Validation and Business Rule Errors: Use
PropertyName
for validation errors andbusinessRule
for logic violations. - Log Errors with Correlation ID: Facilitate debugging and analytics.
- Document the Error Structure: Ensure clients understand how to handle responses.
Conclusion
Using a standardized ApiResponse<T>
structure with error collections and correlation IDs improves consistency, maintainability, and debugging capabilities. While it introduces slight payload overhead, the benefits far outweigh the drawbacks, especially for complex or distributed systems. By following best practices, you can create APIs that are robust, scalable, and easy to integrate with.
FAQs
Why use
ApiResponse<T>
instead of raw responses?
ApiResponse<T>
provides a consistent structure, improving maintainability and error handling.How does
CorrelationId
help?
It enables tracking and debugging of requests across distributed systems.What is the purpose of the
IsSuccessful
flag?
It provides a quick way to check if the operation succeeded, independent of HTTP status codes.Can I add more fields to
ApiResponse<T>
?
Yes, you can extend it with fields likeErrorCode
orAdditionalData
as needed.How to handle breaking changes in the response structure?
Use versioning in your API to ensure backward compatibility for existing clients.
Top comments (0)