REST'te Kaynak Modelleme ve İlişkileri Tasarımı
REST mimarisinin merkezinde kaynaklar (resources) vardır. Bir kaynak, bir varlığı (entity), bir koleksiyonu veya bir hizmeti temsil eder. REST API'nizin başarısı, büyük ölçüde kaynaklarınızı ne kadar doğru modellediğinize ve bu kaynaklar arasındaki ilişkileri ne kadar tutarlı tasarladığınıza bağlıdır. Yanlış bir kaynak modellemesi, API'nizi kullanılamaz hale getirebilir; doğru bir modelleme ise yıllar boyunca bakımı kolay, anlaşılır ve ölçeklenebilir bir API sunar. Bu yazıda, REST'te kaynak modelleme prensiplerini, isimlendirme kurallarını, ilişki türlerini ve .NET'te kaynak modelleme stratejilerini detaylıca ele alacağız.
1. Kaynak Nedir? Temel Kavramlar
REST'te kaynak, URI ile tanımlanabilen ve HTTP metotları ile üzerinde işlem yapılabilen herhangi bir şeydir. Kaynaklar somut (ör. bir kullanıcı, bir ürün) veya soyut (ör. bir süreç, bir oturum) olabilir.
Kaynak Türleri:
-
Tekil Kaynak (Singleton Resource): Sadece bir örneği olan kaynaklar. Ör.
/api/profile(kullanıcının kendi profili). -
Koleksiyon Kaynağı (Collection Resource): Birden fazla öğe içeren kaynaklar. Ör.
/api/products. -
Öğe Kaynağı (Item Resource): Koleksiyon içindeki tek bir öğe. Ör.
/api/products/123. -
Alt Kaynak (Sub-Resource): Başka bir kaynağa bağlı kaynak. Ör.
/api/users/123/orders. -
Kontrol Kaynağı (Controller Resource): Bir eylemi temsil eden kaynak. Ör.
/api/orders/123/cancel(eylem odaklı, tercihen kaçınılmalı).
2. Kaynak İsimlendirme Kuralları
A. Çoğul İsimler Kullanın
Koleksiyonlar her zaman çoğul isimlerle ifade edilmelidir.
✅ Doğru:
text
GET /api/users GET /api/products GET /api/orders
❌ Yanlış:
text
GET /api/user GET /api/product GET /api/order
İstisna: Tekil kaynaklar (ör. /api/profile) doğal olarak tekildir.
B. İsimler Fiil Değil, İsim Olmalıdır
URI'ler kaynağı tanımlar, eylemi değil. Eylem, HTTP metodunun kendisidir.
✅ Doğru:
text
GET /api/users → Kullanıcıları listele POST /api/users → Yeni kullanıcı oluştur GET /api/users/123 → Kullanıcı detayını getir PUT /api/users/123 → Kullanıcıyı güncelle DELETE /api/users/123 → Kullanıcıyı sil
❌ Yanlış:
text
GET /api/getUsers POST /api/createUser GET /api/getUserById?id=123 POST /api/updateUser/123 POST /api/deleteUser/123
C. Küçük Harf ve Tire (kebab-case) Kullanın
URI'ler küçük harf içermeli, kelime ayırıcı olarak tire (-) kullanılmalıdır.
✅ Doğru:
text
/api/order-items /api/user-profiles /api/payment-methods
❌ Yanlış:
text
/api/OrderItems /api/order_items /api/userProfiles
D. Dosya Uzantısı Kullanmayın
URI'de dosya uzantısı olmamalıdır; format Accept başlığı ile belirtilir.
✅ Doğru:
text
GET /api/products/123 Accept: application/json
❌ Yanlış:
text
GET /api/products/123.json GET /api/products/123.xml
E. Sürümleme (Versioning)
Sürüm bilgisi URI'ye eklenmelidir (en yaygın yaklaşım).
text
/api/v1/users /api/v2/users
3. Kaynak İlişkileri: Bire-Bir, Bire-Çok, Çoka-Çok
A. Bire-Bir (One-to-One) İlişki
Her iki kaynak da birbirine yalnızca bir kez bağlıdır. Örneğin, User ve Profile.
Yaklaşım 1: Alt Kaynak (Sub-Resource)
text
GET /api/users/123/profile POST /api/users/123/profile PUT /api/users/123/profile
Yaklaşım 2: Doğrudan Kaynak (önerilen)
Eğer ilişki zorunluysa ve profil kullanıcıdan bağımsız düşünülemezse:
text
GET /api/users/123 → Yanıtta profil bilgisi de döner
B. Bire-Çok (One-to-Many) İlişki
Bir kaynak, diğerinden birden fazlasına bağlıdır. Örneğin, User ve Order.
Yaklaşım 1: Alt Kaynak (Nested)
text
GET /api/users/123/orders → Kullanıcının tüm siparişleri POST /api/users/123/orders → Kullanıcı için yeni sipariş GET /api/users/123/orders/456 → Kullanıcının belirli bir siparişi
Yaklaşım 2: Filtreli Üst Düzey Kaynak (önerilen)
text
GET /api/orders?userId=123 → Kullanıcının siparişleri GET /api/orders/456 → Sipariş detayı (userId bilgisi içinde)
Hangi Yaklaşım Ne Zaman?
-
Alt Kaynak: İlişki zorunluysa (kullanıcı olmadan sipariş anlamsızsa).
-
Filtreli Kaynak: Kaynak bağımsız bir varlıksa ve farklı filtrelerle erişilecekse.
C. Çoka-Çok (Many-to-Many) İlişki
Her iki kaynak da diğerinden birden fazlasına bağlıdır. Örneğin, Product ve Category.
Yaklaşım 1: Ayrı İlişki Kaynağı
text
GET /api/products/123/categories → Ürünün kategorileri GET /api/categories/456/products → Kategorinin ürünleri
Yaklaşım 2: Ara Kaynak (Junction Resource)
İlişkinin kendisi bir kaynak olarak modellenir.
text
GET /api/product-categories?productId=123 → Ürün-kategori ilişkileri POST /api/product-categories → Yeni ilişki oluştur DELETE /api/product-categories/789 → İlişkiyi sil
Bu yaklaşım, ilişkiye ait ek veriler (ör. createdAt, priority) varsa idealdir.
4. Hiyerarşi Derinliği: Ne Kadar İç İçe?
URI hiyerarşisi, kaynaklar arasındaki ilişkiyi yansıtır. Ancak aşırı derin iç içe yapılar (deep nesting), API'yi karmaşık ve kullanılamaz hale getirir.
Kural: Maksimum 2-3 seviye derinlik
✅ Kabul Edilebilir:
text
GET /api/users/123/orders/456/items
❌ Kaçınılması Gereken:
text
GET /api/users/123/orders/456/items/789/reviews/101/comments
Bu durumda, daha üst düzey bir kaynak kullanmak daha iyidir:
text
GET /api/order-items/789/reviews GET /api/reviews/101/comments
İlke: Eğer bir alt kaynak, üst kaynağa bağımlı olmadan da anlamlıysa, onu üst düzey bir kaynak olarak modelleyin.
5. Kaynak Temsili ve Denormalizasyon
REST'te her kaynak, kendi temsiline (representation) sahiptir. İlişkili kaynakların nasıl sunulacağı önemli bir tasarım kararıdır.
A. Minimal Temsil (Reference by ID)
İlişkili kaynak, sadece ID ile referans edilir.
json
{
"id": 456,
"total": 250.00,
"status": "shipped",
"customerId": 123
}
Avantaj: Küçük payload, tutarlı veri.
Dezavantaj: İstemci, ilişkili veriyi almak için ek çağrı yapmalıdır (N+1 problemi).
B. Genişletilmiş Temsil (Embedded)
İlişkili kaynak, yanıtın içine gömülür.
json
{
"id": 456,
"total": 250.00,
"status": "shipped",
"customer": {
"id": 123,
"name": "Ahmet Yılmaz",
"email": "ahmet@example.com"
}
}
Avantaj: Tek çağrıda tüm veri, istemci kolaylığı.
Dezavantaj: Büyük payload, veri tekrarı, tutarlılık zorluğu.
C. Hibrit Yaklaşım (En İyi Pratik)
İstemciye, ilişkili veriyi nasıl alacağını seçme hakkı tanıyın.
text
GET /api/orders/456 → Minimal temsil (sadece customerId) GET /api/orders/456?expand=customer → Genişletilmiş temsil (customer detayları dahil)
Bu yaklaşım, GraphQL'in select mantığına benzer bir esneklik sunar.
6. Hypermedia ile İlişki Keşfi (HATEOAS)
HATEOAS kullanıyorsanız, ilişkili kaynakların link'lerini yanıta ekleyerek istemcinin ilişkileri keşfetmesini sağlayabilirsiniz.
json
{
"id": 456,
"total": 250.00,
"status": "shipped",
"links": [
{ "rel": "self", "href": "/api/orders/456", "method": "GET" },
{ "rel": "customer", "href": "/api/users/123", "method": "GET" },
{ "rel": "items", "href": "/api/orders/456/items", "method": "GET" },
{ "rel": "cancel", "href": "/api/orders/456/cancel", "method": "POST" }
]
}
7. Kaynak Modellemede Sık Yapılan Hatalar
| Hata | Çözüm |
|---|---|
Fiil içeren URI'ler (/getUser, /createOrder) |
Sadece isim kullanın; eylemi HTTP metodu belirler. |
| Aşırı derin iç içe yapılar | Maksimum 2-3 seviye derinlik; bağımsız kaynakları üst düzeye taşıyın. |
Tutarsız isimlendirme (/users, /product, /OrderItems) |
Tutarlı bir kural belirleyin (çoğul, küçük harf, tire). |
| Her ilişki için alt kaynak | Kaynak bağımsızsa, filtrelenmiş üst düzey kaynak kullanın. |
| Çoka-çok ilişkilerde ara kaynak eksikliği | İlişkiye ait veri varsa, ara kaynağı modelleyin. |
| İstemciye aşırı veri yükleme | Minimal temsil + expand parametresi ile hibrit yaklaşım kullanın. |
8. .NET'te Kaynak Modelleme Stratejileri
A. Controller Yapısı
csharp
// Koleksiyon ve öğe kaynakları
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class OrdersController : ControllerBase
{
// GET /api/v1/orders
[HttpGet]
public async Task<IActionResult> GetOrders([FromQuery] int? userId) { ... }
// GET /api/v1/orders/456
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id) { ... }
// GET /api/v1/users/123/orders
[HttpGet("/api/v1/users/{userId}/orders")]
public async Task<IActionResult> GetUserOrders(int userId) { ... }
}
B. DTO Tasarımı ile Hibrit Temsil
csharp
public class OrderDto
{
public int Id { get; set; }
public decimal Total { get; set; }
public string Status { get; set; }
public int CustomerId { get; set; }
// Sadece expand=customer olduğunda doldurulur
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public CustomerDto? Customer { get; set; }
}
// Controller'da
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id, [FromQuery] string? expand)
{
var order = await _orderService.GetByIdAsync(id);
var dto = _mapper.Map<OrderDto>(order);
if (expand?.Contains("customer") == true)
{
dto.Customer = _mapper.Map<CustomerDto>(order.Customer);
}
return Ok(dto);
}
C. GraphQL ile İlişki Modelleme (Alternatif)
GraphQL, ilişkileri modellemek için doğal bir yol sunar:
graphql
type Order {
id: ID!
total: Float!
status: String!
customer: Customer! # İlişki doğrudan tanımlı
}
type Customer {
id: ID!
name: String!
orders: [Order!]! # Ters ilişki
}
9. En İyi Pratikler
-
Kaynakları İş Alanına Göre Modelleyin: Veritabanı tablolarını değil, iş alanındaki varlıkları modelleyin.
-
Tutarlı İsimlendirme: Çoğul, küçük harf, tire ayırıcı.
-
İlişkileri Basitleştirin: Aşırı iç içe yapılardan kaçının, bağımsız kaynakları üst düzeye taşıyın.
-
Hibrit Temsil Kullanın: Minimal temsil +
expandparametresi. -
Bağımsız Kaynak İlkesi: Bir kaynak, üst kaynağa bağımlı olmadan da anlamlıysa, üst düzey kaynak olarak modelleyin.
-
Dokümantasyon: Her kaynağın ilişkilerini ve URI yapısını dokümante edin.
-
Tutarlılık: Tüm API'de aynı kuralları uygulayın.
Sonuç
REST'te kaynak modelleme ve ilişki tasarımı, API'nizin uzun vadeli başarısını belirleyen en kritik kararlardır. Kaynakları doğru isimlendirmek, ilişkileri tutarlı modellemek, aşırı iç içe yapılardan kaçınmak ve istemciye esneklik sunmak, bakımı kolay, anlaşılır ve ölçeklenebilir bir API'nin temelini oluşturur. Unutmayın: İyi bir REST API, bir veritabanı şemasının HTTP üzerinden yansıması değil, iş alanının doğal bir modellemesidir.