Yükleniyor...

ASP.NET Core'da Standart API Response Formatı (Result<T>)

ASP.NET Core projelerinde başarı ve hata yanıtlarını Result<T> wrapper'ı ile standartlaştırın: message, code, data ve errors alanlarıyla tutarlı bir response yapısı oluşturun.

Birçok API’de endpoint’e göre farklı response yapıları döner: bazen direkt veri, bazen { error: "..." }, bazen de plain text. Bu durum istemci (frontend/mobile) tarafında işleri zorlaştırır; çünkü her endpoint için ayrı parse/handle mantığı yazmak gerekir.

Basit bir çözüm, yanıtları Result<T> wrapper’ı ile standartlaştırmaktır. İstemci tarafı öngörülebilir bir yapı alır (message, code, data, errors), API tarafı ise HTTP status kodlarını anlamlı kullanmaya devam eder (200/201/400/404/409/500).

Bir detay daha: Başarılı yanıtta errors: [] dönmek veya hatalı yanıtta data: null dönmek payload’ı gereksiz şişirir. Bu örnekte JsonIgnore ile WhenWritingNull kullanıp null olan alanları JSON çıktısından çıkarıyoruz.


Amaç


Hedef Response Yapısı

Başarılı yanıtta gereksiz alanlar olmadan sadece data döner:


{
  "data": {
    "id": 42,
    "code": "RSV-2025-00042",
    "guestName": "Alex",
    "date": "2026-02-21"
  },
  "message": "Reservation loaded.",
  "code": "RESERVATION_LOADED"
}

Hatalı yanıtta data dönmeden sadece errors döner:


{
  "message": "Validation failed.",
  "code": "VALIDATION_ERROR",
  "errors": [
    { "field": "id", "message": "Id must be greater than zero." }
  ]
}

Result Modellerini Oluştur (JsonIgnore ile Kompakt JSON)

Generic Result<T> ve küçük bir hata DTO’su tanımlayın. null olan alanlar JSON çıktısına yazılmaz.


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
        };
}

Bu versiyonda bilinçli olarak success alanı yok. Başarı/başarısızlık bilgisi zaten HTTP status kodu ile net bir şekilde anlaşılır; payload daha küçük kalır.


Reservation Modeli + Service

Örneği gerçek hayata yaklaştırmak için küçük bir model ve servis kullanalım:


public sealed record ReservationDto(int Id, string Code, string GuestName, DateOnly Date);

public sealed class ReservationService
{
    // Gerçekte burada veritabanı çağrısı olur.
    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);
    }
}

Controller’da Kullanım (Service ile)

Controller, response body’yi Result<T> ile standartlaştırırken HTTP status kodlarını da anlamlı tutar.


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"
        ));
    }
}

Opsiyonel: Validation Hataları için Helper

Validation senaryolarında küçük helper’lar controller kodunu daha temiz tutar:


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();
}

Sık Yapılan İyileştirmeler


TL;DR

  • Result<T> ile message, code, data, errors alanlarını standartlaştırın.
  • [JsonIgnore(Condition = WhenWritingNull)] ile null alanları JSON’dan çıkarıp payload’ı küçültün.
  • HTTP status kodlarını anlamlı tutarken response body’yi tek bir yapıda toplayın.
  • İç detayları sunucu tarafında loglayın, istemciye güvenli mesajlar döndürün.