Standard API Response Wrapper in ASP.NET Core (Result<T>)
Standardize success and error responses in ASP.NET Core using a Result<T> wrapper with message, code, data, and errors for predictable client handling.
Many APIs end up returning different response shapes depending on the endpoint:
sometimes it’s raw data, sometimes it’s { error: "..." }, sometimes it’s plain text.
This makes client code harder to maintain because every endpoint needs custom parsing.
A simple solution is to standardize responses with a Result<T> wrapper.
The client receives a predictable structure (such as message, code, data, errors),
while the API keeps meaningful HTTP status codes (200/201/400/404/409/500).
One more detail: returning errors: [] on success or data: null on failure can bloat payloads.
In this example we use JsonIgnore with WhenWritingNull to omit null properties from JSON output.
Goal
- Return a consistent response shape for both success and failure.
- Keep payloads compact by omitting null properties.
- Make client-side handling predictable (especially for UI and validation).
- Keep HTTP status codes meaningful while standardizing response bodies.
Target Response Shape
On success, return data without unnecessary fields:
{
"data": {
"id": 42,
"code": "RSV-2025-00042",
"guestName": "Alex",
"date": "2026-02-21"
},
"message": "Reservation loaded.",
"code": "RESERVATION_LOADED"
}
On error, return errors without data:
{
"message": "Validation failed.",
"code": "VALIDATION_ERROR",
"errors": [
{ "field": "id", "message": "Id must be greater than zero." }
]
}
Create the Result Models (Compact JSON with JsonIgnore)
Use a generic Result<T> plus a small error DTO. Null properties are omitted from JSON output.
using System.Text.Json.Serialization;
public sealed record ErrorItem(string Field, string Message);
public sealed class Result<T>
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public T? Data { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<ErrorItem>? Errors { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Message { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Code { get; init; }
public static Result<T> Ok(T data, string? message = null, string? code = null)
=> new()
{
Data = data,
Message = message,
Code = code
};
public static Result<T> Fail(string message, string code, List<ErrorItem>? errors = null)
=> new()
{
Message = message,
Code = code,
Errors = errors
};
}
Notice that this version intentionally does not include a success field.
The HTTP status code already communicates success/failure, and the payload stays smaller.
Reservation Model + Service
A tiny model and service to simulate real application flow:
public sealed record ReservationDto(int Id, string Code, string GuestName, DateOnly Date);
public sealed class ReservationService
{
// In a real app, this would call a database.
public Task<ReservationDto?> GetByIdAsync(int id, CancellationToken ct)
{
if (id == 42)
{
return Task.FromResult<ReservationDto?>(
new ReservationDto(42, "RSV-2025-00042", "Alex", new DateOnly(2026, 2, 21))
);
}
return Task.FromResult<ReservationDto?>(null);
}
}
Use It in a Controller (Service-Based)
The controller returns standardized Result<T> bodies, while keeping status codes meaningful.
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/reservations")]
public sealed class ReservationsController : ControllerBase
{
private readonly ReservationService _service;
public ReservationsController(ReservationService service)
{
_service = service;
}
[HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id, CancellationToken ct)
{
if (id <= 0)
{
return BadRequest(Result<object>.Fail(
message: "Validation failed.",
code: "VALIDATION_ERROR",
errors: new List<ErrorItem>
{
new("id", "Id must be greater than zero.")
}
));
}
var reservation = await _service.GetByIdAsync(id, ct);
if (reservation is null)
{
return NotFound(Result<object>.Fail(
message: "Reservation not found.",
code: "RESERVATION_NOT_FOUND"
));
}
return Ok(Result<ReservationDto>.Ok(
data: reservation,
message: "Reservation loaded.",
code: "RESERVATION_LOADED"
));
}
}
Optional: Helpers for Validation Errors
For validation scenarios, small helpers keep controller code clean:
public static class ErrorList
{
public static List<ErrorItem> One(string field, string message)
=> new() { new ErrorItem(field, message) };
public static List<ErrorItem> Many(params (string field, string message)[] items)
=> items.Select(x => new ErrorItem(x.field, x.message)).ToList();
}
Common Improvements
-
Use stable, documented codes (e.g.,
RESERVATION_NOT_FOUND) and keep them consistent across services. -
Do not leak exception details in
Message. Log full exceptions server-side. -
Consider using
ProblemDetailsfor errors andResult<T>only for success, if you want to follow RFC 7807 strictly. -
Add a
traceIdextension (or a header) to correlate client errors with logs.
TL;DR
Result<T>standardizes responses withmessage,code,data,errors.- Null properties are omitted via
[JsonIgnore(Condition = WhenWritingNull)]to keep payloads small. - Keep HTTP status codes meaningful while returning predictable JSON bodies.
- Log internal details server-side and return safe messages to clients.