.NET Core API — Bölüm 1: RESTful API Nasıl Yazılır?
.NET Core ile RESTful bir API yazmanın temellerini inceliyoruz. Controller tabanlı ve Minimal API yaklaşımları, Dependency Injection ile servis kurulumu, HTTP verb'leri, status kodları ve model validation — üç bölümlük serinin ilk durağı.
21 Nisan 2026 7 dk okuma 5 0
Merhaba arkadaşlar, bu makaleyle birlikte .NET Core API serisinin ilk bölümüne başlıyoruz. Üç bölümlük bu dizide önce RESTful bir API nasıl yazılır adım adım göreceğiz, sonraki makalelerde JWT ile güvenlik ve Swagger ile dokümantasyon konularına eğileceğiz. Başlamadan bir kahve alın, çünkü bu seriyi bitirdiğinizde tek başınıza tam donanımlı bir API yazabilir hale geleceksiniz.
API Nedir, Neden .NET Core?
API (Application Programming Interface), iki uygulamanın birbiriyle konuşabilmesini sağlayan bir arayüz. Modern web'de en yaygın API türü RESTful HTTP API'dır — istemci HTTP ile istek atar, sunucu JSON döndürür. .NET Core (bugünkü adıyla sadece .NET) bu tarz API'leri yazmak için dünyada en güçlü platformlardan biri. Neden tercih ediyoruz:
- Performans: Kestrel web server'ı saniyede milyonlarca istek karşılayabiliyor (TechEmpower benchmark'larda sürekli üst sıralarda).
- Cross-platform: Linux, Mac, Windows — her yerde aynı kod çalışır.
- Yerleşik DI, Configuration, Logging: Ayrı paket yüklemeden baştan gelir.
- C# dilinin gücü: Strongly-typed, modern, zengin ekosistem.
İlk Projemizi Oluşturalım
.NET 9 SDK'nın kurulu olduğundan emin olun (dotnet --version), sonra terminali açın:
dotnet new webapi -n SemGoksuApi
cd SemGoksuApi
dotnet run
Template size iki seçenek sunar: Controller tabanlı ve Minimal API. Ben bu yazıda ikisini de göstereceğim çünkü farklı senaryolarda farklı yaklaşımlar mantıklı.
dotnet run çalıştırdığınızda terminal size bir URL gösterir (örn. https://localhost:7147). Tarayıcıda açtığınızda Swagger UI karşılar. Template'ten gelen /weatherforecast endpoint'ini test edebilirsiniz.
Klasik Controller Tabanlı API
Büyük projelerde Controller tabanlı yaklaşım tercih ediliyor çünkü daha organize ve MVC dünyasından tanıdık. Basit bir örnek yapalım — blog yazılarını yöneten bir controller:
// Models/Yazi.cs
public class Yazi
{
public int Id { get; set; }
public required string Baslik { get; set; }
public required string Icerik { get; set; }
public DateTime Tarih { get; set; } = DateTime.UtcNow;
}
// Controllers/YazilarController.cs
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class YazilarController : ControllerBase
{
private static readonly List<Yazi> _yazilar = new()
{
new() { Id = 1, Baslik = "İlk yazım", Icerik = "Merhaba dünya" },
new() { Id = 2, Baslik = "API'ye Giriş", Icerik = "Bugün API yazıyoruz" }
};
[HttpGet]
public IActionResult Tumu() => Ok(_yazilar);
[HttpGet("{id:int}")]
public IActionResult Getir(int id)
{
var yazi = _yazilar.FirstOrDefault(y => y.Id == id);
return yazi is null ? NotFound() : Ok(yazi);
}
[HttpPost]
public IActionResult Ekle([FromBody] Yazi yeni)
{
yeni.Id = _yazilar.Max(y => y.Id) + 1;
_yazilar.Add(yeni);
return CreatedAtAction(nameof(Getir), new { id = yeni.Id }, yeni);
}
[HttpPut("{id:int}")]
public IActionResult Guncelle(int id, [FromBody] Yazi guncel)
{
var mevcut = _yazilar.FirstOrDefault(y => y.Id == id);
if (mevcut is null) return NotFound();
mevcut.Baslik = guncel.Baslik;
mevcut.Icerik = guncel.Icerik;
return NoContent();
}
[HttpDelete("{id:int}")]
public IActionResult Sil(int id)
{
var yazi = _yazilar.FirstOrDefault(y => y.Id == id);
if (yazi is null) return NotFound();
_yazilar.Remove(yazi);
return NoContent();
}
}
Visual Studio 2022 - yeni proje, ASP.NET Core Web API sablonu.
IDE tarafinda: Visual Studio tarafinda Dosya > Yeni > Proje diyip 'ASP.NET Core Web API' sablonunu secerek basliyoruz. .NET 9.0'i hedefleyip Authentication yok ile devam ediyoruz; controller'lari kendimiz ekleyecegiz.
Burada birkaç önemli detay var:
- [ApiController] attribute'u otomatik model validation, 400 Bad Request dönmesi, parameter binding iyileştirmesi gibi sihirli işlemler yapar.
- [Route("api/[controller]")] URL şablonunu belirler. [controller] placeholder'ı sınıf adındaki "Controller" eki kaldırılarak gelir — yani /api/yazilar olur.
- HTTP verb attribute'ları ([HttpGet], [HttpPost], [HttpPut], [HttpDelete]) metodun hangi HTTP metoduna cevap vereceğini söyler.
- Ok(), NotFound(), CreatedAtAction() gibi helper'lar doğru HTTP status kodlarını döndürür.
Minimal API: Aynı İş Daha Az Kod
.NET 6 ile gelen Minimal API yaklaşımı küçük servis veya microservice'ler için çok pratik. Aynı mantığı Program.cs içinde tek yerde toplayabiliyoruz:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var yazilar = new List<Yazi>
{
new() { Id = 1, Baslik = "İlk yazım", Icerik = "Merhaba dünya" }
};
app.MapGet("/api/yazilar", () => yazilar);
app.MapGet("/api/yazilar/{id:int}", (int id) =>
yazilar.FirstOrDefault(y => y.Id == id) is { } y
? Results.Ok(y)
: Results.NotFound());
app.MapPost("/api/yazilar", (Yazi yeni) =>
{
yeni.Id = yazilar.Max(y => y.Id) + 1;
yazilar.Add(yeni);
return Results.Created($"/api/yazilar/{yeni.Id}", yeni);
});
app.MapPut("/api/yazilar/{id:int}", (int id, Yazi guncel) =>
{
var mevcut = yazilar.FirstOrDefault(y => y.Id == id);
if (mevcut is null) return Results.NotFound();
mevcut.Baslik = guncel.Baslik;
mevcut.Icerik = guncel.Icerik;
return Results.NoContent();
});
app.MapDelete("/api/yazilar/{id:int}", (int id) =>
{
var yazi = yazilar.FirstOrDefault(y => y.Id == id);
if (yazi is null) return Results.NotFound();
yazilar.Remove(yazi);
return Results.NoContent();
});
app.Run();
Minimal API daha az boilerplate ama büyük projelerde routing'in her yere dağılması bir sorun. Benim kuralım şu: 5-10 endpoint'ten fazlaysa Controller, az ise Minimal.
Dependency Injection ile Gerçek Servis
Hafızada tutmak demo için uygun ama gerçek hayatta veritabanı, cache, HTTP client gibi servislere ihtiyacımız var. .NET Core'un built-in DI sistemi bunu çok zarif çözüyor. Önce bir interface ve implementasyon tanımlıyoruz:
public interface IYaziServisi
{
Task<IEnumerable<Yazi>> TumunuGetirAsync();
Task<Yazi?> GetirAsync(int id);
Task<Yazi> EkleAsync(Yazi yazi);
Task GuncelleAsync(Yazi yazi);
Task SilAsync(int id);
}
public class YaziServisi : IYaziServisi
{
private readonly List<Yazi> _yazilar = new();
public Task<IEnumerable<Yazi>> TumunuGetirAsync() =>
Task.FromResult(_yazilar.AsEnumerable());
public Task<Yazi?> GetirAsync(int id) =>
Task.FromResult(_yazilar.FirstOrDefault(y => y.Id == id));
public Task<Yazi> EkleAsync(Yazi yazi)
{
yazi.Id = _yazilar.Count == 0 ? 1 : _yazilar.Max(y => y.Id) + 1;
_yazilar.Add(yazi);
return Task.FromResult(yazi);
}
public Task GuncelleAsync(Yazi yazi)
{
var m = _yazilar.FirstOrDefault(y => y.Id == yazi.Id);
if (m is null) return Task.CompletedTask;
m.Baslik = yazi.Baslik;
m.Icerik = yazi.Icerik;
return Task.CompletedTask;
}
public Task SilAsync(int id)
{
var y = _yazilar.FirstOrDefault(x => x.Id == id);
if (y != null) _yazilar.Remove(y);
return Task.CompletedTask;
}
}
- AddSingleton: Uygulama boyunca tek instance. In-memory cache, konfigürasyon gibi state tutan servisler için.
- AddScoped: Her HTTP request için yeni instance. DbContext için standart tercih.
- AddTransient: Her injection'da yeni instance. Stateless, hafif servisler için.
Controller'da kullanım:
[ApiController]
[Route("api/[controller]")]
public class YazilarController(IYaziServisi servis) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> Tumu() => Ok(await servis.TumunuGetirAsync());
[HttpGet("{id:int}")]
public async Task<IActionResult> Getir(int id) =>
await servis.GetirAsync(id) is { } yazi
? Ok(yazi)
: NotFound();
}
Dikkat ettiniz mi, C# 12 ile gelen primary constructor özelliğini kullanarak servis'i direkt class parametresi olarak aldık. Daha kısa, daha temiz.
Model Validation — Otomatik Koruma [ApiController] attribute'u sayesinde model validation otomatik çalışır. Data Annotations ekleyip istek gönderildiğinde doğrulama yapabiliriz:
using System.ComponentModel.DataAnnotations;
public class Yazi
{
public int Id { get; set; }
[Required(ErrorMessage = "Başlık gerekli")]
[StringLength(200, MinimumLength = 3, ErrorMessage = "Başlık 3-200 karakter olmalı")]
public required string Baslik { get; set; }
[Required, MinLength(10)]
public required string Icerik { get; set; }
public DateTime Tarih { get; set; } = DateTime.UtcNow;
}
Eğer istemci geçersiz veri gönderirse otomatik olarak 400 Bad Request döner:
{
"errors": {
"Baslik": ["Başlık 3-200 karakter olmalı"],
"Icerik": ["The Icerik field is required."]
},
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400
}
HTTP Status Code Rehberi
API yazarken doğru status kodunu döndürmek çok önemli. Bir rehber:
- 200 OK: Başarılı GET, PUT (body dönüyorsa).
- 201 Created: Başarılı POST, yeni kaynak oluştu. Location header'ı ile.
- 204 No Content: Başarılı PUT/DELETE, body döndürmeye gerek yok.
- 400 Bad Request: İstek hatalı (validation).
- 401 Unauthorized: Kimlik doğrulama yok/hatalı.
- 403 Forbidden: Kimlik var ama bu işleme yetkisi yok.
- 404 Not Found: Kaynak yok.
- 409 Conflict: İş kuralı ihlali (örn. duplicate email).
- 500 Internal Server Error: Sunucu hatası.
Özet
Bu ilk makalede .NET Core API'nin temellerini kurduk — Controller ve Minimal API yaklaşımları, DI ile servis enjeksiyonu, HTTP metotları, status kodları ve model validation. Elinizde artık çalışan bir CRUD API var.
Bir sonraki bölümde bu API'nin güvenliğine odaklanacağız: JWT ile Authentication nasıl kurulur, rolleri nasıl yönetiriz, token nasıl üretir ve doğrularız. Herkese kolay gelsin, yorumlarda sorularınızı bekliyorum...