API Response Patterns
Comprehensive guide to implementing standard REST API response patterns using EasyMessages in ASP.NET Core.
Table of Contents
- Standard REST API Patterns
- CRUD Operations
- Validation Patterns
- Error Handling
- Authentication & Authorization
- Pagination & Filtering
- Best Practices
Standard REST API Patterns
ApiResponse Format
All EasyMessages API responses follow this standardized format via .ToJsonObject():
{
"success": true,
"code": "CRUD_001",
"type": "success",
"title": "User Created",
"description": "User has been created successfully.",
"data": { ... },
"timestamp": "2026-01-09T14:30:00.000Z",
"correlationId": "0HMVQK8F3J8QK:00000001",
"metadata": { ... }
}
Fields:
success- Boolean indicating success/failurecode- Unique message code (e.g., “CRUD_001”)type- Message type (“success”, “error”, “warning”, “info”)title- Short summarydescription- Detailed explanationdata- Response payload (entity, list, etc.)timestamp- When the message was created (optional)correlationId- Request tracking ID (optional)metadata- Additional context (optional)
Core Pattern
The fundamental pattern for all API responses:
var msg = Msg.Xxx.Method();
return StatusCode(msg.HttpStatusCode ?? 200, msg.ToJsonObject());
For Minimal APIs:
var msg = Msg.Xxx.Method();
return Results.Json(msg.ToJsonObject(), statusCode: msg.HttpStatusCode ?? 200);
CRUD Operations
Create (POST)
Pattern: Return created entity with appropriate status code
Controller:
using Microsoft.AspNetCore.Mvc;
using RecurPixel.EasyMessages;
[HttpPost]
public IActionResult Create([FromBody] CreateUserDto dto)
{
// Validate
if (!ModelState.IsValid)
{
var err = Msg.Validation.Failed()
.WithData(new { errors = ModelState });
return StatusCode(err.HttpStatusCode ?? 422, err.ToJsonObject()); // 422 Unprocessable Entity
}
// Create entity
var user = _userService.Create(dto);
// Return created response
var created = Msg.Crud.Created("User")
.WithData(user)
.Log(_logger);
return StatusCode(created.HttpStatusCode ?? 200, created.ToJsonObject()); // 200 OK
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"code": "CRUD_001",
"type": "success",
"title": "User Created",
"description": "User has been created successfully.",
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2026-01-09T14:30:00.000Z"
}
}
Read - Single (GET by ID)
Pattern: Return entity or 404 Not Found
Controller:
using Microsoft.AspNetCore.Mvc;
using RecurPixel.EasyMessages;
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
var user = _userService.FindById(id);
if (user == null)
{
var notFound = Msg.Crud.NotFound("User")
.WithMetadata("userId", id)
.Log(_logger);
return StatusCode(notFound.HttpStatusCode ?? 404, notFound.ToJsonObject()); // 404 Not Found
}
var ok = Msg.Crud.Retrieved("User").WithData(user);
return StatusCode(ok.HttpStatusCode ?? 200, ok.ToJsonObject()); // 200 OK
}
Success Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"code": "CRUD_003",
"type": "success",
"title": "User Retrieved",
"description": "User has been retrieved successfully.",
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com"
}
}
Not Found Response:
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"success": false,
"code": "CRUD_004",
"type": "error",
"title": "User Not Found",
"description": "The requested User could not be found.",
"metadata": {
"userId": 999
}
}
Read - Collection (GET all)
Pattern: Return list of entities
Controller:
using Microsoft.AspNetCore.Mvc;
using RecurPixel.EasyMessages;
[HttpGet]
public IActionResult GetAll()
{
var users = _userService.GetAll();
var ok = Msg.Crud.Retrieved("Users")
.WithData(users)
.WithMetadata("count", users.Count);
return StatusCode(ok.HttpStatusCode ?? 200, ok.ToJsonObject()); // 200 OK
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"code": "CRUD_003",
"type": "success",
"title": "Users Retrieved",
"description": "Users has been retrieved successfully.",
"data": [
{ "id": 1, "name": "John Doe" },
{ "id": 2, "name": "Jane Smith" }
],
"metadata": {
"count": 2
}
}
Update (PUT)
Pattern: Return updated entity or 404 if not found
Controller:
using Microsoft.AspNetCore.Mvc;
using RecurPixel.EasyMessages;
[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody] UpdateUserDto dto)
{
// Validate
if (!ModelState.IsValid)
{
var err = Msg.Validation.Failed()
.WithData(new { errors = ModelState });
return StatusCode(err.HttpStatusCode ?? 422, err.ToJsonObject()); // 422
}
// Check exists
if (!_userService.Exists(id))
{
var notFound = Msg.Crud.NotFound("User").WithMetadata("userId", id);
return StatusCode(notFound.HttpStatusCode ?? 404, notFound.ToJsonObject()); // 404
}
// Update
var user = _userService.Update(id, dto);
var updated = Msg.Crud.Updated("User")
.WithData(user)
.Log(_logger);
return StatusCode(updated.HttpStatusCode ?? 200, updated.ToJsonObject()); // 200 OK
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"code": "CRUD_002",
"type": "success",
"title": "User Updated",
"description": "User has been updated successfully.",
"data": {
"id": 123,
"name": "John Doe Updated",
"email": "john.updated@example.com",
"updatedAt": "2026-01-09T14:35:00.000Z"
}
}
Delete (DELETE)
Pattern: Return 200 OK with confirmation message
Controller:
using Microsoft.AspNetCore.Mvc;
using RecurPixel.EasyMessages;
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
if (!_userService.Exists(id))
{
var notFound = Msg.Crud.NotFound("User").WithMetadata("userId", id);
return StatusCode(notFound.HttpStatusCode ?? 404, notFound.ToJsonObject()); // 404
}
_userService.Delete(id);
var deleted = Msg.Crud.Deleted("User")
.WithData(new { id, deletedAt = DateTime.UtcNow })
.Log(_logger);
return StatusCode(deleted.HttpStatusCode ?? 200, deleted.ToJsonObject()); // 200 OK
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"code": "CRUD_005",
"type": "success",
"title": "User Deleted",
"description": "User has been deleted successfully.",
"data": {
"id": 123,
"deletedAt": "2026-01-09T14:40:00.000Z"
}
}
Validation Patterns
Model Validation
Pattern: Return 422 Unprocessable Entity with validation errors
Controller:
[HttpPost]
public IActionResult Create([FromBody] CreateUserDto dto)
{
if (!ModelState.IsValid)
{
var errors = ModelState
.Where(e => e.Value.Errors.Count > 0)
.ToDictionary(
e => e.Key,
e => e.Value.Errors.Select(x => x.ErrorMessage).ToArray()
);
var err = Msg.Validation.Failed()
.WithData(new { errors });
return StatusCode(err.HttpStatusCode ?? 422, err.ToJsonObject()); // 422 Unprocessable Entity
}
// ... create user
}
Response:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"success": false,
"code": "VAL_001",
"type": "error",
"title": "Validation Failed",
"description": "One or more validation errors occurred.",
"data": {
"errors": {
"Email": ["Invalid email format"],
"Password": ["Password must be at least 8 characters"]
}
}
}
FluentValidation Integration
Validator:
using FluentValidation;
public class CreateUserDtoValidator : AbstractValidator<CreateUserDto>
{
public CreateUserDtoValidator()
{
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress();
RuleFor(x => x.Password)
.NotEmpty()
.MinimumLength(8);
}
}
Controller:
using Microsoft.AspNetCore.Mvc;
using RecurPixel.EasyMessages;
[HttpPost]
public IActionResult Create([FromBody] CreateUserDto dto)
{
var validationResult = _validator.Validate(dto);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(
g => g.Key,
g => g.Select(e => e.ErrorMessage).ToArray()
);
var err = Msg.Validation.Failed().WithData(new { errors });
return StatusCode(err.HttpStatusCode ?? 422, err.ToJsonObject()); // 422
}
// ... create user
}
Business Rule Validation
Pattern: Validate business rules and return appropriate error
Controller:
[HttpPost]
public IActionResult Create([FromBody] CreateUserDto dto)
{
// Check if email already exists
if (_userService.EmailExists(dto.Email))
{
var err = Msg.Validation.Duplicate("Email")
.WithMetadata("email", dto.Email);
return StatusCode(err.HttpStatusCode ?? 422, err.ToJsonObject()); // 422
}
// Check if username is reserved
if (_userService.IsReservedUsername(dto.Username))
{
var err = Msg.Validation.InvalidFormat("Username")
.WithHint("This username is reserved. Please choose another.");
return StatusCode(err.HttpStatusCode ?? 422, err.ToJsonObject()); // 422
}
// ... create user
}
Response:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"success": false,
"code": "VAL_014",
"type": "error",
"title": "Duplicate Value",
"description": "The email 'john@example.com' already exists.",
"metadata": {
"email": "john@example.com"
}
}
Error Handling
Global Exception Handling
Middleware:
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using RecurPixel.EasyMessages;
public class GlobalExceptionHandler
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(RequestDelegate next, ILogger<GlobalExceptionHandler> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (NotFoundException ex)
{
await HandleExceptionAsync(context, Msg.Crud.NotFound(ex.ResourceName));
}
catch (ValidationException ex)
{
await HandleExceptionAsync(context, Msg.Validation.Failed()
.WithData(new { errors = ex.Errors }));
}
catch (UnauthorizedAccessException)
{
await HandleExceptionAsync(context, Msg.Auth.Unauthorized());
}
catch (DbUpdateException ex)
{
await HandleExceptionAsync(context, Msg.Database.TransactionFailed()
.WithMetadata("error", ex.Message));
}
catch (Exception ex)
{
await HandleExceptionAsync(context, Msg.System.Error()
.WithMetadata("error", ex.Message));
}
}
private async Task HandleExceptionAsync(HttpContext context, Message message)
{
_logger.LogError("Unhandled exception for request {Path}", context.Request.Path);
var enriched = message.WithCorrelationId(context.TraceIdentifier);
context.Response.ContentType = "application/json";
context.Response.StatusCode = enriched.HttpStatusCode ?? 500;
await context.Response.WriteAsJsonAsync(enriched.ToJsonObject());
}
}
// Register in Program.cs
app.UseMiddleware<GlobalExceptionHandler>();
Database Errors
Pattern: Handle database-specific errors
Controller:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RecurPixel.EasyMessages;
[HttpPost]
public IActionResult Create([FromBody] CreateUserDto dto)
{
try
{
var user = _userService.Create(dto);
var created = Msg.Crud.Created("User").WithData(user);
return StatusCode(created.HttpStatusCode ?? 200, created.ToJsonObject());
}
catch (DbUpdateException ex) when (ex.InnerException is SqlException sqlEx && sqlEx.Number == 2627)
{
// Unique constraint violation
var err = Msg.Validation.Duplicate("User")
.WithMetadata("error", "A user with this email already exists");
return StatusCode(err.HttpStatusCode ?? 422, err.ToJsonObject()); // 422
}
catch (DbUpdateException ex)
{
var err = Msg.Database.TransactionFailed()
.WithMetadata("error", ex.Message)
.Log(_logger);
return StatusCode(err.HttpStatusCode ?? 500, err.ToJsonObject()); // 500
}
}
Response:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"success": false,
"code": "DB_004",
"type": "error",
"title": "Transaction Failed",
"description": "The database transaction failed and has been rolled back.",
"metadata": {
"error": "Connection timeout occurred"
}
}
External Service Errors
Pattern: Handle third-party service failures
Controller:
using Microsoft.AspNetCore.Mvc;
using RecurPixel.EasyMessages;
[HttpPost("send-email")]
public async Task<IActionResult> SendEmail([FromBody] EmailDto dto)
{
try
{
await _emailService.SendAsync(dto);
var ok = Msg.System.OperationCompleted().WithData(new { sent = true });
return StatusCode(ok.HttpStatusCode ?? 200, ok.ToJsonObject()); // 200 OK
}
catch (HttpRequestException ex)
{
var err = Msg.System.Unavailable()
.WithMetadata("service", "EmailService")
.WithMetadata("error", ex.Message)
.Log(_logger);
return StatusCode(err.HttpStatusCode ?? 503, err.ToJsonObject()); // 503 Service Unavailable
}
catch (TimeoutException)
{
var err = Msg.Network.Timeout()
.WithMetadata("service", "EmailService")
.Log(_logger);
return StatusCode(err.HttpStatusCode ?? 408, err.ToJsonObject()); // 408 Timeout
}
}
Authentication & Authorization
Login Success
Pattern: Return user info with auth token
Controller:
using Microsoft.AspNetCore.Mvc;
using RecurPixel.EasyMessages;
[HttpPost("login")]
public IActionResult Login([FromBody] LoginDto dto)
{
var user = _authService.Authenticate(dto.Email, dto.Password);
if (user == null)
{
var failed = Msg.Auth.LoginFailed()
.WithMetadata("email", dto.Email)
.Log(_logger);
return StatusCode(failed.HttpStatusCode ?? 401, failed.ToJsonObject()); // 401 Unauthorized
}
var token = _tokenService.GenerateToken(user);
var ok = Msg.Auth.LoginSuccessful()
.WithData(new
{
user = new { user.Id, user.Name, user.Email },
token,
expiresAt = DateTime.UtcNow.AddHours(24)
})
.Log(_logger);
return StatusCode(ok.HttpStatusCode ?? 200, ok.ToJsonObject()); // 200 OK
}
Success Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"code": "AUTH_003",
"type": "success",
"title": "Login Successful",
"description": "Welcome back, john!",
"data": {
"user": {
"id": 123,
"name": "John Doe",
"email": "john@example.com"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresAt": "2026-01-10T14:30:00.000Z"
}
}
Failure Response:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"success": false,
"code": "AUTH_001",
"type": "error",
"title": "Authentication Failed",
"description": "Invalid username or password.",
"metadata": {
"email": "john@example.com"
}
}
Unauthorized Access
Pattern: Return 403 Forbidden for insufficient permissions
Controller:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using RecurPixel.EasyMessages;
[HttpDelete("{id}")]
[Authorize(Roles = "Admin")]
public IActionResult Delete(int id)
{
// Check if user has permission
if (!User.IsInRole("Admin"))
{
var err = Msg.Auth.Unauthorized()
.WithMetadata("requiredRole", "Admin")
.Log(_logger);
return StatusCode(err.HttpStatusCode ?? 403, err.ToJsonObject()); // 403 Forbidden
}
_userService.Delete(id);
var deleted = Msg.Crud.Deleted("User");
return StatusCode(deleted.HttpStatusCode ?? 200, deleted.ToJsonObject());
}
Response:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"success": false,
"code": "AUTH_002",
"type": "error",
"title": "Unauthorized Access",
"description": "You do not have permission to perform this action.",
"metadata": {
"requiredRole": "Admin"
}
}
Expired Token
Pattern: Return 401 with token expired message
In JWT middleware/filter:
if (tokenExpired)
{
var expired = Msg.Auth.SessionExpired();
context.Response.StatusCode = expired.HttpStatusCode ?? 401;
await context.Response.WriteAsJsonAsync(expired.ToJsonObject());
return;
}
Response:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"success": false,
"code": "AUTH_004",
"type": "error",
"title": "Session Expired",
"description": "Your session has expired."
}
Pagination & Filtering
Paginated Response
Pattern: Return page of results with pagination metadata
Controller:
using Microsoft.AspNetCore.Mvc;
using RecurPixel.EasyMessages;
[HttpGet]
public IActionResult GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
var result = _userService.GetPaged(page, pageSize);
var ok = Msg.Crud.Retrieved("Users")
.WithData(result.Items)
.WithMetadata("pagination", new
{
page,
pageSize,
totalCount = result.TotalCount,
totalPages = result.TotalPages,
hasNext = result.HasNext,
hasPrevious = result.HasPrevious
});
return StatusCode(ok.HttpStatusCode ?? 200, ok.ToJsonObject());
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"code": "CRUD_003",
"type": "success",
"title": "Users Retrieved",
"description": "Users has been retrieved successfully.",
"data": [
{ "id": 1, "name": "User 1" },
{ "id": 2, "name": "User 2" },
{ "id": 3, "name": "User 3" }
],
"metadata": {
"pagination": {
"page": 1,
"pageSize": 10,
"totalCount": 25,
"totalPages": 3,
"hasNext": true,
"hasPrevious": false
}
}
}
Filtered/Searched Response
Pattern: Return filtered results with search criteria
Controller:
using Microsoft.AspNetCore.Mvc;
using RecurPixel.EasyMessages;
[HttpGet("search")]
public IActionResult Search([FromQuery] string query, [FromQuery] string role = null)
{
var users = _userService.Search(query, role);
var ok = Msg.Search.Completed(users.Count, query)
.WithData(users)
.WithMetadata("role", role);
return StatusCode(ok.HttpStatusCode ?? 200, ok.ToJsonObject());
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"code": "SEARCH_002",
"type": "success",
"title": "Search Completed",
"description": "Found 2 result(s) for 'john'.",
"data": [
{ "id": 5, "name": "John Doe", "role": "Admin" },
{ "id": 12, "name": "John Smith", "role": "User" }
],
"metadata": {
"query": "john",
"role": "Admin",
"count": 2
}
}
No Results Found
Pattern: Return empty array with informative message
Controller:
using Microsoft.AspNetCore.Mvc;
using RecurPixel.EasyMessages;
[HttpGet("search")]
public IActionResult Search([FromQuery] string query)
{
var users = _userService.Search(query);
if (!users.Any())
{
var empty = Msg.Search.NoResults(query)
.WithData(new List<object>());
return StatusCode(empty.HttpStatusCode ?? 200, empty.ToJsonObject()); // 200 OK with empty array
}
var ok = Msg.Search.Completed(users.Count, query).WithData(users);
return StatusCode(ok.HttpStatusCode ?? 200, ok.ToJsonObject());
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"code": "SEARCH_001",
"type": "info",
"title": "No Results Found",
"description": "No results found for 'xyz123'.",
"data": [],
"metadata": {
"query": "xyz123"
}
}
Best Practices
[✓] DO:
- Use
StatusCode(msg.HttpStatusCode ?? N, msg.ToJsonObject())// Good - Appropriate status codes var created = Msg.Crud.Created("User").WithData(user); return StatusCode(created.HttpStatusCode ?? 200, created.ToJsonObject()); var notFound = Msg.Crud.NotFound("User"); return StatusCode(notFound.HttpStatusCode ?? 404, notFound.ToJsonObject()); var err = Msg.Validation.Failed().WithData(errors); return StatusCode(err.HttpStatusCode ?? 422, err.ToJsonObject()); - Include relevant data in responses
// Good - Return created entity var created = Msg.Crud.Created("User").WithData(user); return StatusCode(created.HttpStatusCode ?? 200, created.ToJsonObject()); - Add metadata for debugging
// Good - Helpful metadata var notFound = Msg.Crud.NotFound("User") .WithMetadata("userId", id) .WithMetadata("requestedBy", User.Identity.Name); return StatusCode(notFound.HttpStatusCode ?? 404, notFound.ToJsonObject()); - Log errors and warnings
// Good - Log before returning var failed = Msg.Auth.LoginFailed() .WithMetadata("email", dto.Email) .Log(_logger); return StatusCode(failed.HttpStatusCode ?? 401, failed.ToJsonObject()); - Assign the message to a variable before returning
// Good - Readable and safe var msg = Msg.Crud.Created("User").WithData(user).Log(_logger); return StatusCode(msg.HttpStatusCode ?? 200, msg.ToJsonObject());
[ ] DON’T:
- Don’t return inconsistent formats
// Bad - Custom format return Ok(new { message = "User created" }); // Good - Standard format var msg = Msg.Crud.Created("User").WithData(user); return StatusCode(msg.HttpStatusCode ?? 200, msg.ToJsonObject()); - Don’t expose sensitive data
// Bad - Includes passwordHash var msg = Msg.Crud.Retrieved("User").WithData(user); // Good - Safe DTO without sensitive fields var msg = Msg.Crud.Retrieved("User").WithData(new UserDto(user)); - Don’t ignore validation
// Bad - No validation var user = _userService.Create(dto); // Good - Validate first if (!ModelState.IsValid) { var err = Msg.Validation.Failed().WithData(new { errors = ModelState }); return StatusCode(err.HttpStatusCode ?? 422, err.ToJsonObject()); } - Don’t swallow exceptions silently
// Bad - Silent failure try { _userService.Delete(id); } catch { } // Error hidden! // Good - Log and return error try { _userService.Delete(id); } catch (Exception ex) { var err = Msg.System.Error() .WithMetadata("error", ex.Message) .Log(_logger); return StatusCode(err.HttpStatusCode ?? 500, err.ToJsonObject()); } - Don’t hardcode status codes as magic numbers
// Bad - Magic number return StatusCode(200, msg.ToJsonObject()); // Good - Use message's status code return StatusCode(msg.HttpStatusCode ?? 200, msg.ToJsonObject());
Next Steps
- Logging Integration - Advanced logging strategies
- How-To Guides - Practical recipes
- Examples - Complete working examples
Questions? Ask in Discussions