ASP.NET Core Web API ile Profesyonel REST API Geliştirme Rehberi
ASP.NET Core Web API ile sıfırdan profesyonel bir REST API kurmayı; doğru endpoint tasarımı, DTO, validasyon, hata yönetimi, versiyonlama ve dokümantasyonla adım adım anlatan kapsamlı rehber.
Bir web uygulamasının, mobil uygulamanın ya da başka bir servisin konuştuğu ortak dil çoğu zaman bir REST API'dir. ASP.NET Core Web API, bu dili hızlı, güvenli ve ölçeklenebilir şekilde konuşmanın .NET dünyasındaki en güçlü yolu. Bu yazıda "çalışıyor" seviyesinden "profesyonel" seviyeye geçmek için gereken her şeyi sırayla ele alacağız: kaynak odaklı URL tasarımı, doğru HTTP durum kodları, DTO kullanımı, validasyon, merkezi hata yönetimi, sayfalama, versiyonlama ve dokümantasyon.
REST API nedir, neden önemlidir?
REST (Representational State Transfer), istemci ile sunucunun HTTP üzerinden kaynaklar (resource) aracılığıyla haberleştiği bir mimari yaklaşımdır. Her kaynağın bir adresi (URL) vardır ve bu kaynak üzerinde yapılacak işlemi HTTP metodu belirler. Örneğin GET /api/products ürünleri listeler, POST /api/products yeni ürün ekler.
İyi tasarlanmış bir API'nin üç temel faydası vardır:
- Öngörülebilirlik: Geliştirici, dokümana bakmadan bile bir endpoint'in ne yaptığını tahmin edebilir.
- Bağımsızlık: Web, mobil ve üçüncü parti entegrasyonlar aynı API'yi kullanır; arayüz değişse de iş mantığı tek yerde kalır.
- Ölçeklenebilirlik: Durumsuz (stateless) yapı sayesinde API'yi birden fazla sunucuya yatay olarak dağıtmak kolaylaşır.
İyi bir API, arkasındaki altyapı ne kadar karmaşık olursa olsun istemciye sade bir yüz gösterir.
Projeyi oluşturmak
.NET SDK kurulu bir makinede yeni bir Web API projesi oluşturmak tek komut:
dotnet new webapi -n Shop.Api --use-controllers
cd Shop.Api
dotnet run
--use-controllers parametresi, Minimal API yerine klasik controller yapısıyla başlamanızı sağlar. Büyüyen projelerde controller yapısı; filtreler, model binding ve test edilebilirlik açısından düzenli bir iskelet sunar. Küçük servisler için Minimal API de gayet iyi bir tercihtir; iki yaklaşım aynı altyapıyı kullanır.
Kaynak odaklı endpoint tasarımı
En sık yapılan hata, URL'lere fiil koymaktır: /api/getProducts, /api/createOrder gibi. REST'te fiili HTTP metodu taşır, URL ise sadece isim içerir.
| İşlem | Metot | URL | Başarılı yanıt |
|---|---|---|---|
| Ürünleri listele | GET | /api/products | 200 OK |
| Tek ürün getir | GET | /api/products/42 | 200 OK / 404 Not Found |
| Ürün ekle | POST | /api/products | 201 Created |
| Ürünü güncelle | PUT | /api/products/42 | 204 No Content |
| Ürünü sil | DELETE | /api/products/42 | 204 No Content |
| Ürünün yorumları | GET | /api/products/42/reviews | 200 OK |
Birkaç pratik kural:
- Koleksiyon isimleri çoğul olsun:
products,orders. - İlişkili kaynaklar iç içe gösterilebilir ama iki seviyeyi geçmemeye çalışın.
- Filtreleme, sıralama ve sayfalama için query string kullanın:
/api/products?category=ayakkabi&sort=price&page=2.
Controller ve doğru durum kodları
Aşağıdaki controller, temel CRUD işlemlerini doğru HTTP durum kodlarıyla yapar:
[ApiController]
[Route("api/[controller]")]
public class ProductsController(IProductService products) : ControllerBase
{
[HttpGet("{id:int}")]
[ProducesResponseType<ProductDto>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Get(int id, CancellationToken ct)
{
var product = await products.GetAsync(id, ct);
return product is null ? NotFound() : Ok(product);
}
[HttpPost]
public async Task<IActionResult> Create(CreateProductRequest request, CancellationToken ct)
{
var created = await products.CreateAsync(request, ct);
return CreatedAtAction(nameof(Get), new { id = created.Id }, created);
}
}
[ApiController] özniteliği burada çok iş yapar: model doğrulaması başarısız olursa otomatik olarak 400 Bad Request döner, parametrelerin nereden okunacağını (body, route, query) akıllıca çıkarır. CreatedAtAction ise yeni kaynağın adresini Location başlığına yazar; bu, REST'in istemciye "oluşturduğun şey burada" deme şeklidir.
Her action'a CancellationToken eklemek de küçük ama değerli bir alışkanlık: istemci isteği iptal ettiğinde veritabanı sorgusu da iptal edilir ve sunucu boşuna çalışmaz.
Entity değil, DTO döndürün
Veritabanı entity'sini doğrudan dışarı açmak cazip görünür ama ciddi sorunlar doğurur: gereksiz alanlar (ör. şifre hash'i) sızabilir, ilişkili nesneler sonsuz döngüye girebilir ve veritabanındaki her değişiklik API sözleşmesini bozar. Çözüm DTO (Data Transfer Object) kullanmaktır:
public record ProductDto(int Id, string Name, decimal Price, string Category);
public record CreateProductRequest(
[Required, StringLength(120)] string Name,
[Range(0.01, 1_000_000)] decimal Price,
[Required] string Category);
record tipleri DTO için idealdir: kısa, değişmez (immutable) ve değer karşılaştırması yapar. Entity ile DTO arasındaki dönüşümü elle yazmak küçük projelerde en okunur yoldur; büyüdükçe bir mapping kütüphanesi düşünebilirsiniz.
Validasyon: hatayı kapıda yakalayın
Basit kurallar için Data Annotations ([Required], [Range], [StringLength]) yeterlidir. Kurallar karmaşıklaştığında (ör. "indirimli fiyat normal fiyattan küçük olmalı") FluentValidation gibi bir kütüphane okunabilirliği artırır:
public class CreateProductValidator : AbstractValidator<CreateProductRequest>
{
public CreateProductValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(120);
RuleFor(x => x.Price).GreaterThan(0);
}
}
Önemli olan, geçersiz verinin iş katmanına hiç ulaşmamasıdır. Validasyon hataları 400 ile ve hangi alanın neden hatalı olduğunu söyleyen bir gövdeyle dönmelidir.
Merkezi hata yönetimi ve ProblemDetails
Her action'da try/catch yazmak hem tekrar hem de tutarsızlık demektir. ASP.NET Core, hataları standart RFC 7807 ProblemDetails formatında döndürmeyi destekler. .NET 8 ile gelen IExceptionHandler arayüzü bunu tek bir yerde toplamayı kolaylaştırır:
public class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext http, Exception ex, CancellationToken ct)
{
logger.LogError(ex, "İşlenmeyen hata");
var (status, title) = ex switch
{
NotFoundException => (404, "Kaynak bulunamadı"),
BusinessRuleException => (422, "İş kuralı ihlali"),
_ => (500, "Beklenmeyen bir hata oluştu")
};
http.Response.StatusCode = status;
await http.Response.WriteAsJsonAsync(new ProblemDetails { Status = status, Title = title }, ct);
return true;
}
}
// Program.cs
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler();
Böylece istemci her hatada aynı şekle sahip bir yanıt alır, siz de hata detaylarını (stack trace gibi) dışarı sızdırmadan loglarsınız.
Sayfalama, filtreleme ve sıralama
Binlerce kaydı tek seferde döndürmek hem sunucuyu hem istemciyi yorar. Basit bir sayfalama sözleşmesi:
public record PagedResult<T>(IReadOnlyList<T> Items, int Page, int PageSize, int TotalCount);
[HttpGet]
public async Task<PagedResult<ProductDto>> List([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
{
pageSize = Math.Clamp(pageSize, 1, 100);
return await products.ListAsync(page, pageSize);
}
pageSize için üst sınır koymayı unutmayın; aksi halde biri pageSize=1000000 göndererek sunucunuzu zorlayabilir. Çok büyük tablolarda OFFSET tabanlı sayfalama yavaşlar; bu durumda "son görülen id'den sonrakileri getir" mantığıyla çalışan keyset (cursor) sayfalama daha performanslıdır.
Sayfalama, versiyonlama ve hata yönetimi gibi detaylar API'nin uzun ömürlü olmasını sağlar.
Versiyonlama: sözleşmeyi bozmadan ilerlemek
API'niz yayına girdikten sonra bir alanın adını değiştirmek, onu kullanan tüm istemcileri bozar. Bu yüzden kırıcı (breaking) değişiklikleri yeni bir versiyonla yayınlamak gerekir. En yaygın ve anlaşılır yöntem URL'de versiyon taşımaktır: /api/v1/products, /api/v2/products. Asp.Versioning.Mvc paketi bu işi öznitelikler üzerinden düzenli hale getirir. Kural basit: alan eklemek kırıcı değildir, alan silmek veya anlamını değiştirmek kırıcıdır.
Dokümantasyon: OpenAPI ve Swagger
Dokümantasyonu olmayan bir API, kullanılmayan bir API'dir. ASP.NET Core, OpenAPI şemasını kodunuzdan otomatik üretebilir; Swagger UI veya Scalar gibi arayüzlerle bu şema denenebilir bir dokümana dönüşür. ProducesResponseType öznitelikleri ve XML yorumları eklediğinizde doküman, hangi durumda hangi yanıtın döneceğini de gösterir. Bu, frontend ekibiyle aranızdaki en iyi sözleşmedir.
Güvenlik ve performans için kısa kontrol listesi
- HTTPS zorunlu olsun, HSTS açık olsun.
- Kimlik doğrulama için JWT veya cookie; yetkilendirme için
[Authorize]ve politika (policy) tabanlı kurallar kullanın. - Rate limiting ile kötüye kullanıma sınır koyun (.NET 7+ ile yerleşik geliyor).
- CORS'u
*yerine sadece güvendiğiniz origin'lere açın. - Sık okunan ve az değişen veriler için output caching veya dağıtık cache kullanın.
- Veritabanı sorgularında sadece ihtiyaç duyduğunuz alanları seçin ve okuma sorgularında
AsNoTracking()kullanın. - Loglarınıza istek kimliği (correlation id) ekleyin; production'da bir hatayı takip ederken hayat kurtarır.
Sonuç
Profesyonel bir REST API; doğru URL tasarımı, anlamlı durum kodları, DTO ile korunan bir sözleşme, kapıda yapılan validasyon, tek merkezden yönetilen hatalar ve güncel bir dokümantasyonun toplamıdır. ASP.NET Core bunların neredeyse hepsini kutudan çıktığı haliyle destekliyor; bize düşen, bu araçları tutarlı şekilde kullanmak. Bir sonraki adım olarak API'nizi Entity Framework Core ile performanslı bir veri katmanına bağlamayı ve uçtan uca testlerle güvenceye almayı öneririm.
Sık sorulan sorular
Minimal API mi, controller mı kullanmalıyım?
Küçük ve odaklı servislerde Minimal API daha az kodla iş görür. Çok sayıda endpoint, filtre ve ekip üyesi olan projelerde controller yapısı düzeni korumayı kolaylaştırır. İkisi aynı projede birlikte de kullanılabilir.
PUT ile PATCH arasındaki fark nedir?
PUT kaynağın tamamını gönderilen haliyle değiştirir; PATCH ise sadece gönderilen alanları günceller. Kısmi güncellemelerde PATCH daha doğru bir tercihtir.
Hata yanıtlarında stack trace göstermeli miyim?
Hayır. Detaylı hata bilgisi sadece loglarda ve geliştirme ortamında olmalı. Production'da istemciye sadece genel bir başlık, durum kodu ve gerekiyorsa bir hata kimliği dönün.