Building APIs in ASP.NET Core becomes much easier to maintain when all endpoints return a consistent response structure.
A common API response format helps:
- Frontend developers consume APIs more easily
- Standardize success and error responses
- Improve debugging and logging
- Simplify API documentation
- Create cleaner enterprise-grade applications
Why Use a Common API Response Structure?
Without a standard format, APIs often return inconsistent responses like:
{
"name": "John"
}
or
{
"message": "User created successfully"
}
or
{
"error": "Something went wrong"
}
This inconsistency creates problems for frontend applications and API consumers.
Instead, use a unified response structure like:
{
"success": true,
"message": "User fetched successfully",
"data": {
"id": 1,
"name": "John"
},
"errors": null
}
Benefits of Standardized API Responses
A common structure provides:
| Benefit | Description |
|---|---|
| Consistency | Every endpoint behaves similarly |
| Better Frontend Integration | Easier parsing on client side |
| Improved Debugging | Centralized error handling |
| Scalability | Easier to maintain large projects |
| Cleaner Documentation | Predictable API contracts |
Step 1: Create a Generic API Response Model
Create a folder named Models and add the following class.
ApiResponse.cs
namespace DemoAPI.Models
{
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 ApiResponse()
{
Errors = new List<string>();
}
}
}
Understanding the Properties
| Property | Purpose |
|---|---|
| Success | Indicates request success/failure |
| Message | Human-readable response message |
| Data | Actual response payload |
| Errors | Validation or exception details |
Using generics (T) makes this reusable for all APIs.
Step 2: Create a Sample DTO
UserDto.cs
namespace DemoAPI.Models
{
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
}
Step 3: Implement in Controller
UsersController.cs
using DemoAPI.Models;
using Microsoft.AspNetCore.Mvc;
namespace DemoAPI.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
var user = new UserDto
{
Id = id,
Name = "John Doe",
Email = "john@example.com"
};
var response = new ApiResponse<UserDto>
{
Success = true,
Message = "User fetched successfully",
Data = user
};
return Ok(response);
}
}
}
Sample Success Response
{
"success": true,
"message": "User fetched successfully",
"data": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"errors": []
}
Step 4: Handle Error Responses
Example Error Response
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
if (id <= 0)
{
var errorResponse = new ApiResponse<object>
{
Success = false,
Message = "Invalid user id",
Errors = new List<string>
{
"User id must be greater than zero"
}
};
return BadRequest(errorResponse);
}
return Ok();
}
Sample Error Output
{
"success": false,
"message": "Invalid user id",
"data": null,
"errors": [
"User id must be greater than zero"
]
}
Step 5: Create a Reusable Response Helper (Optional)
To reduce repetitive code, create a helper class.
ApiResponseHelper.cs
namespace DemoAPI.Helpers
{
public static class ApiResponseHelper
{
public static ApiResponse<T> Success<T>(
T data,
string message = "Request successful")
{
return new ApiResponse<T>
{
Success = true,
Message = message,
Data = data
};
}
public static ApiResponse<T> Fail<T>(
string message,
List<string> errors = null)
{
return new ApiResponse<T>
{
Success = false,
Message = message,
Errors = errors ?? new List<string>()
};
}
}
}
Cleaner Controller Code
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
if (id <= 0)
{
return BadRequest(
ApiResponseHelper.Fail<object>(
"Invalid user id",
new List<string>
{
"Id must be greater than zero"
}));
}
var user = new UserDto
{
Id = id,
Name = "John Doe",
Email = "john@example.com"
};
return Ok(
ApiResponseHelper.Success(
user,
"User fetched successfully"));
}
Step 6: Global Exception Handling
For production-grade applications, use middleware for centralized exception handling.
ExceptionMiddleware.cs
using System.Net;
using System.Text.Json;
using DemoAPI.Models;
namespace DemoAPI.Middleware
{
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionMiddleware> _logger;
public ExceptionMiddleware(
RequestDelegate next,
ILogger<ExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
context.Response.ContentType = "application/json";
context.Response.StatusCode =
(int)HttpStatusCode.InternalServerError;
var response = new ApiResponse<object>
{
Success = false,
Message = "Internal server error",
Errors = new List<string>
{
ex.Message
}
};
var json = JsonSerializer.Serialize(response);
await context.Response.WriteAsync(json);
}
}
}
}
Register Middleware
In Program.cs:
app.UseMiddleware<ExceptionMiddleware>();
Final API Response Standard
A robust enterprise response model typically includes:
{
"success": true,
"message": "Operation completed successfully",
"data": {},
"errors": [],
"statusCode": 200,
"timestamp": "2026-05-16T10:00:00Z"
}
You can extend your response model with:
- Status codes
- Pagination metadata
- Correlation IDs
- Request tracking IDs
- Timestamps
Best Practices
- Keep Responses Predictable
- Every endpoint should follow the same structure.
- Avoid Returning Raw Exceptions
- Never expose sensitive internal details in production.
- Use Proper HTTP Status Codes
- Examples:
200 OK201 Created400 Bad Request401 Unauthorized404 Not Found500 Internal Server Error
- Examples:
- Use DTOs
- Avoid exposing database entities directly.
- Implement Validation
- Use FluentValidation or DataAnnotations.
Common Mistakes to Avoid
| Mistake | Problem |
|---|---|
| Returning inconsistent JSON | Hard for frontend parsing |
| Exposing stack traces | Security risk |
| Using exceptions for validation | Poor performance |
| Returning HTTP 200 for errors | Misleading API behavior |