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ç
- Başarılı ve hatalı durumlar için tutarlı bir response yapısı döndürmek.
nullalanları JSON’dan çıkararak payload’ı küçük tutmak.- İstemci tarafında (özellikle UI/validation) öngörülebilir bir handle akışı sağlamak.
- Response body standardize olurken HTTP status kodlarını anlamlı kullanmaya devam etmek.
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
-
RESERVATION_NOT_FOUNDgibi sabit ve dokümante edilmiş kodlar kullanın; servisler arasında tutarlı kalsın. -
Messagealanında exception detaylarını sızdırmayın. Detayları sunucu tarafında loglayın. -
RFC 7807 standardını daha sıkı takip etmek isterseniz, hata durumlarında
ProblemDetails, başarı durumlarındaResult<T>kullanmayı düşünebilirsiniz. -
Log korelasyonu için response’a bir
traceIdekleyin (veya header olarak dönün).
TL;DR
Result<T>ilemessage,code,data,errorsalanlarını standartlaştırın.[JsonIgnore(Condition = WhenWritingNull)]ilenullalanları 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.