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.