Loading...

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


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


TL;DR

  • Result<T> standardizes responses with message, 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.