api-response-patterns
π―Skillfrom thapaliyabikendra/ai-artifacts
api-response-patterns skill from thapaliyabikendra/ai-artifacts
Installation
npx skills add https://github.com/thapaliyabikendra/ai-artifacts --skill api-response-patternsSkill Details
"API response wrapper patterns for consistent, predictable REST APIs in ABP Framework. Use when: (1) designing uniform API response contracts, (2) implementing success/error response wrappers, (3) handling pagination and metadata, (4) standardizing error responses."
Overview
# API Response Patterns
Master API response wrapper patterns for building consistent, predictable REST APIs that clients love.
When to Use This Skill
- Designing uniform API response contracts
- Implementing success/error response wrappers
- Standardizing error response formats
- Adding metadata to API responses (pagination, timing, version)
- Building APIs consumed by multiple client types (web, mobile, third-party)
Why Response Wrappers?
| Benefit | Description |
|---------|-------------|
| Consistency | All endpoints return same structure |
| Predictability | Clients know what to expect |
| Metadata | Include timing, pagination, version info |
| Error Handling | Uniform error format across all endpoints |
| Debugging | Include correlation IDs for tracing |
Response Wrapper Patterns
1. Basic ResponseModel
```csharp
// Application.Contracts/Models/ResponseModel.cs
public class ResponseModel
{
public bool IsSuccess { get; set; }
public T Data { get; set; }
public string Message { get; set; }
public List
protected ResponseModel() { }
protected ResponseModel(bool isSuccess, T data, string message, List
{
IsSuccess = isSuccess;
Data = data;
Message = message;
Errors = errors ?? new List
}
public static ResponseModel
=> new(true, data, message, null);
public static ResponseModel
=> new(false, default, message, new List
public static ResponseModel
=> new(false, default, errors.FirstOrDefault(), errors);
}
// Non-generic version for operations without return data
public class ResponseModel : ResponseModel
{
public static ResponseModel Success(string message = null)
=> new() { IsSuccess = true, Message = message };
public new static ResponseModel Failure(string message)
=> new() { IsSuccess = false, Message = message, Errors = new List
}
```
2. Extended ApiResponse with Metadata
```csharp
// Application.Contracts/Models/ApiResponse.cs
public class ApiResponse
{
public bool Success { get; set; }
public T Data { get; set; }
public string Message { get; set; }
public ApiResponseMeta Meta { get; set; }
public List
public static ApiResponse
{
Success = true,
Data = data,
Message = message,
Meta = new ApiResponseMeta()
};
public static ApiResponse
{
Success = false,
Message = message,
Errors = new List
{
new ApiError { Code = code ?? "ERROR", Message = message }
},
Meta = new ApiResponseMeta()
};
public static ApiResponse
{
Success = false,
Message = errors.FirstOrDefault()?.Message,
Errors = errors,
Meta = new ApiResponseMeta()
};
}
public class ApiResponseMeta
{
public string Version { get; set; } = "1.0";
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public string RequestId { get; set; } = Guid.NewGuid().ToString("N")[..8];
public long? ProcessingTimeMs { get; set; }
}
public class ApiError
{
public string Code { get; set; }
public string Message { get; set; }
public string Field { get; set; }
public object Details { get; set; }
}
```
3. Paginated Response
```csharp
// Application.Contracts/Models/PagedApiResponse.cs
public class PagedApiResponse>
{
public PaginationInfo Pagination { get; set; }
public static PagedApiResponse
List
long totalCount,
int pageNumber,
int pageSize,
string message = null) => new()
{
Success = true,
Data = items,
Message = message,
Meta = new ApiResponseMeta(),
Pagination = new PaginationInfo
{
TotalCount = totalCount,
PageNumber = pageNumber,
PageSize = pageSize,
TotalPages = (int)Math.Ceiling((double)totalCount / pageSize),
HasPreviousPage = pageNumber > 1,
HasNextPage = pageNumber * pageSize < totalCount
}
};
}
public class PaginationInfo
{
public long TotalCount { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
public int TotalPages { get; set; }
public bool HasPreviousPage { get; set; }
public bool HasNextPage { get; set; }
}
```
Usage in AppServices
1. Basic CRUD Operations
```csharp
public class ProductAppService : ApplicationService, IProductAppService
{
private readonly IRepository
private readonly ILogger
public async Task
{
var product = await _productRepository.FirstOrDefaultAsync(x => x.Id == id);
if (product == null)
{
return ApiResponse
"Product not found",
"PRODUCT_NOT_FOUND");
}
var dto = ObjectMapper.Map
return ApiResponse
}
public async Task
PagedAndSortedResultRequestDto input,
ProductFilter filter)
{
var queryable = await _productRepository.GetQueryableAsync();
var query = queryable
.WhereIf(!filter.Name.IsNullOrWhiteSpace(),
x => x.Name.Contains(filter.Name))
.WhereIf(filter.CategoryId.HasValue,
x => x.CategoryId == filter.CategoryId);
var totalCount = await AsyncExecuter.CountAsync(query);
var products = await AsyncExecuter.ToListAsync(
query
.OrderBy(input.Sorting ?? "Name")
.PageBy(input.SkipCount, input.MaxResultCount));
var dtos = ObjectMapper.Map, List
return PagedApiResponse
dtos,
totalCount,
pageNumber: (input.SkipCount / input.MaxResultCount) + 1,
pageSize: input.MaxResultCount);
}
public async Task
{
try
{
// Validate uniqueness
var exists = await _productRepository.AnyAsync(
x => x.ProductCode == input.ProductCode);
if (exists)
{
return ApiResponse
$"Product code '{input.ProductCode}' already exists",
"DUPLICATE_PRODUCT_CODE");
}
var product = new Product(
GuidGenerator.Create(),
input.ProductCode,
input.Name,
input.Price);
await _productRepository.InsertAsync(product);
var dto = ObjectMapper.Map
return ApiResponse
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create product");
return ApiResponse
}
}
public async Task
{
var product = await _productRepository.FirstOrDefaultAsync(x => x.Id == id);
if (product == null)
{
return ApiResponse
}
await _productRepository.DeleteAsync(product);
return ApiResponse
}
}
```
2. Bulk Operations
```csharp
public async Task
{
var products = await _productRepository.GetListAsync(x => ids.Contains(x.Id));
if (!products.Any())
{
return ApiResponse
"No products found",
"NOT_FOUND");
}
await _productRepository.DeleteManyAsync(products);
var result = new BulkOperationResult
{
RequestedCount = ids.Count,
ProcessedCount = products.Count,
SkippedCount = ids.Count - products.Count
};
return ApiResponse
result,
$"Deleted {products.Count} of {ids.Count} products");
}
public class BulkOperationResult
{
public int RequestedCount { get; set; }
public int ProcessedCount { get; set; }
public int SkippedCount { get; set; }
public List
}
```
Error Response Patterns
1. Validation Error Response
```csharp
public class ValidationErrorResponse
{
public bool Success => false;
public string Message => "Validation failed";
public string Code => "VALIDATION_ERROR";
public List
public static ValidationErrorResponse Create(List
{
Errors = errors
};
}
public class FieldError
{
public string Field { get; set; }
public string Message { get; set; }
public object AttemptedValue { get; set; }
public FieldError() { }
public FieldError(string field, string message, object attemptedValue = null)
{
Field = field;
Message = message;
AttemptedValue = attemptedValue;
}
}
```
2. Exception Filter for Consistent Errors
```csharp
public class ApiExceptionFilter : IExceptionFilter
{
private readonly ILogger
public ApiExceptionFilter(ILogger
{
_logger = logger;
}
public void OnException(ExceptionContext context)
{
var response = context.Exception switch
{
EntityNotFoundException ex => CreateNotFoundResponse(ex),
AbpValidationException ex => CreateValidationResponse(ex),
BusinessException ex => CreateBusinessErrorResponse(ex),
UnauthorizedAccessException => CreateUnauthorizedResponse(),
_ => CreateInternalErrorResponse(context.Exception)
};
context.Result = new ObjectResult(response)
{
StatusCode = GetStatusCode(context.Exception)
};
context.ExceptionHandled = true;
}
private int GetStatusCode(Exception ex) => ex switch
{
EntityNotFoundException => 404,
AbpValidationException => 400,
BusinessException => 422,
UnauthorizedAccessException => 401,
_ => 500
};
private ApiResponse
ApiResponse
private ApiResponse
ApiResponse
ex.ValidationErrors.Select(e => new ApiError
{
Code = "VALIDATION_ERROR",
Field = e.MemberNames.FirstOrDefault(),
Message = e.ErrorMessage
}).ToList());
private ApiResponse
ApiResponse
private ApiResponse
ApiResponse
private ApiResponse
{
_logger.LogError(ex, "Unhandled exception");
return ApiResponse
}
}
```
Controller Patterns
1. Standard Controller
```csharp
[ApiController]
[Route("api/products")]
public class ProductController : AbpController
{
private readonly IProductAppService _productAppService;
public ProductController(IProductAppService productAppService)
{
_productAppService = productAppService;
}
[HttpGet("{id}")]
[ProducesResponseType(typeof(ApiResponse
[ProducesResponseType(typeof(ApiResponse
public async Task
{
var response = await _productAppService.GetAsync(id);
if (!response.Success)
{
return NotFound(response);
}
return Ok(response);
}
[HttpGet]
[ProducesResponseType(typeof(PagedApiResponse
public async Task
[FromQuery] PagedAndSortedResultRequestDto input,
[FromQuery] ProductFilter filter)
{
var response = await _productAppService.GetListAsync(input, filter);
return Ok(response);
}
[HttpPost]
[ProducesResponseType(typeof(ApiResponse
[ProducesResponseType(typeof(ApiResponse
public async Task
{
var response = await _productAppService.CreateAsync(input);
if (!response.Success)
{
return BadRequest(response);
}
return CreatedAtAction(
nameof(GetAsync),
new { id = response.Data.Id },
response);
}
[HttpDelete("{id}")]
[ProducesResponseType(typeof(ApiResponse
[ProducesResponseType(typeof(ApiResponse
public async Task
{
var response = await _productAppService.DeleteAsync(id);
if (!response.Success)
{
return NotFound(response);
}
return Ok(response);
}
}
```
2. Response Helper Extension
```csharp
public static class ControllerExtensions
{
public static IActionResult ToActionResult
this ControllerBase controller,
ApiResponse
{
if (response.Success)
{
return controller.Ok(response);
}
var errorCode = response.Errors.FirstOrDefault()?.Code ?? "ERROR";
return errorCode switch
{
"NOT_FOUND" => controller.NotFound(response),
"VALIDATION_ERROR" => controller.BadRequest(response),
"UNAUTHORIZED" => controller.Unauthorized(response),
"FORBIDDEN" => controller.StatusCode(403, response),
_ => controller.BadRequest(response)
};
}
}
// Usage
[HttpGet("{id}")]
public async Task
{
var response = await _productAppService.GetAsync(id);
return this.ToActionResult(response);
}
```
OpenAPI Documentation
```csharp
// Swagger configuration
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
// Add response examples
c.OperationFilter
});
public class ApiResponseOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Add common error responses
operation.Responses.TryAdd("400", new OpenApiResponse
{
Description = "Bad Request - Validation Error"
});
operation.Responses.TryAdd("401", new OpenApiResponse
{
Description = "Unauthorized"
});
operation.Responses.TryAdd("404", new OpenApiResponse
{
Description = "Not Found"
});
operation.Responses.TryAdd("500", new OpenApiResponse
{
Description = "Internal Server Error"
});
}
}
```
Best Practices
- Use consistent response structure - All endpoints should return the same wrapper
- Include error codes - Machine-readable codes alongside human messages
- Add metadata - Timestamps, request IDs for debugging
- Paginate lists - Always include pagination info for collections
- Document with OpenAPI - Generate accurate API documentation
- Handle all exceptions - Use exception filters for consistency
- Use appropriate HTTP status codes - Map response status to HTTP codes
- Include timing information - Help clients understand performance
- Version your API - Include version in responses and routes
- Log correlation IDs - Make debugging distributed systems easier
References
- [Error Code Standards](references/error-codes.md)
- [Pagination Patterns](references/pagination.md)
More from this repository10
Provides clean code guidelines and refactoring techniques for C#/.NET, focusing on improving code readability, maintainability, and adherence to SOLID principles.
Implements comprehensive REST APIs in ABP Framework with robust AppServices, DTOs, pagination, filtering, and authorization for .NET applications.
Implements domain layer patterns for ABP Framework, providing robust entity, aggregate, repository, and domain service implementations following DDD principles.
Validates input DTOs in ABP Framework using FluentValidation with async checks, conditional rules, custom validators, and localized error messages.
abp-infrastructure-patterns skill from thapaliyabikendra/ai-artifacts
Configures and optimizes Entity Framework Core patterns for ABP Framework, focusing on entity configuration, migrations, and relationship design with PostgreSQL.
content-retrieval skill from thapaliyabikendra/ai-artifacts
Designs scalable, reliable distributed systems by applying proven architectural patterns and evaluating trade-offs across performance, consistency, and availability.
Generates ABP Application.Contracts layer scaffolding, enabling parallel development by creating standardized interfaces, DTOs, and permissions for .NET microservices.
Implements permission-based OAuth 2.0 authorization for ABP Framework using OpenIddict, enabling fine-grained access control and multi-tenant security.