Consistent API Response Modeling: Why It Matters

There are a few traits that make a developer stand out: clean code, consistent conventions, meaningful error handling… you know the list. When I first started building APIs, I was happy just returning Ok()
or BadRequest()
with either some data or an error message. It did the job. But as I got into more complex APIs - especially those integrating with middleware or external systems - that simplistic approach started to fall apart. The issue? Every API returned something different. One gave you a data object, another a plain string, another wrapped everything in some mystery envelope. Consuming those APIs became a chore. So I decided to fix that by designing a consistent response structure - one wrapper type to rule them all.
The Structure
Here's the foundation:
public abstract class ResultBase<TData, TError>(
bool succeeded,
IEnumerable<TError> errors = null,
List<TData> items = null)
{
public bool Succeeded { get; protected init; } = succeeded;
public IEnumerable<TError> Errors { get; protected init; } = errors ?? Enumerable.Empty<TError>();
public List<TData>? Items { get; protected init; } = items;
public override string ToString() =>
JsonConvert.SerializeObject(this, Formatting.Indented);
}
This gives you a reusable base that standardizes how results are structured. Then you build specific implementations per domain or feature:
public class MemberResult(
bool succeeded,
IEnumerable<MemberError>? errors = null,
List<Member>? items = null)
: ResultBase<Member, MemberError>(succeeded, errors, items)
{
public static MemberResult Success(List<Member>? data = null) =>
new(true, items: data);
public static MemberResult Failed(IEnumerable<MemberError> errors) =>
new(false, errors: errors);
}
Why This Helps
This approach gives you consistency and clarity. You get: A Succeeded flag: Quick way to tell if the operation worked. Typed error information: I split errors into Validation and Processing errors. Validation failures return a BadRequest, while processing issues (e.g., exceptions) return a Problem.
A uniform data container: Whether you return one item or many, it's always a list - super handy for predictable frontend handling.
Bonus Tip: Use Describer Classes for Errors
Don't just throw strings around. Use describers to standardize and structure your errors:
public static class EmailTemplateErrorDescriber
{
public static EmailTemplateError DefaultError(Exception? ex)
{
var err = new EmailTemplateError
{
Code = nameof(DefaultError),
Description = "Something went wrong.",
};
if (ex != null)
{
err.Comment = "See exception details.";
err.Exception = ex;
}
return err;
}
}
You do need an error structure as well, though. I've gone with a simple structure. Implementations just derive from this base can be extended to include additional error data (something like InnerExceptions) if you need it.
public class ErrorBase
{
public string Code { get; set; }
public string Description { get; set; }
public string Comment { get; set; }
public Exception Exception { get; set; }
public ErrorTypes Type { get; set; }
public override string ToString()
{
var json = JsonConvert.SerializeObject(this, Formatting.Indented);
return json;
}
}
public enum ErrorTypes
{
Validation,
Processing
}
Usage
Since the structure is generic and consistent, usage is simple and clear.
Successful responses
return EmailTemplateResult<EmailTemplateViewModel>.Success();
// or with data:
return EmailTemplateResult<EmailTemplateViewModel>.Success([item]);
Failed responses
return EmailTemplateResult<EmailTemplateViewModel>.Failed([
EmailTemplateErrorDescriber.DefaultError(ex)
]);
Handling the Result in Your API
Your controller logic becomes clean and predictable:
var result = await emailTemplateDataService.GetAsync(pageNumber, pageSize, searchQuery, sort, sortDirection);if (!result.Succeeded)
{
if (result.Errors.Any(x => x.Type == ErrorTypes.Validation))
return BadRequest(result);
return Problem(result.ToString());
}
return Ok(result);
Final Thoughts
This pattern isn't groundbreaking, but it makes life easier - for you, your frontend devs, and anyone else touching your APIs. Consistency pays off. Especially when things go wrong.
Subscribe to my newsletter
Read articles from Logan Young directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
