Bir üretim ERP’sinde çalışırken ya da kendi yan ürünlerimin backend’ini geliştirirken en sık karşılaştığım mimari kararlardan biri API versioning stratejisi seçimiydi. Bu kararın basit bir teknik detaydan öte, aslında projenin gelecekteki esnekliğini, bakım maliyetini ve geliştirici deneyimini doğrudan etkilediğini tecrübe ettim. Temelde hep bir ikilem vardı: Hızlıca ayağa kalkmak için basit bir yol mu, yoksa uzun vadede değişime dirençli, esnek bir yapı mı kurmalıydım?
Bu yazıda, yıllar içinde farklı projelerde denediğim API versioning yaklaşımlarını, bunların artılarını ve eksilerini kendi gözümden anlatacağım. Her bir stratejinin hangi durumlarda işe yaradığını, nerede başımın ağrıdığını somut örneklerle paylaşarak, sizin de bu kararı verirken daha bilinçli olmanıza yardımcı olmayı hedefliyorum. Çünkü biliyorum ki kağıt üzerindeki çözümlerle sahadaki gerçekler çoğu zaman birbirini tutmuyor.
Neden API Versioning’e İhtiyaç Duyarız?
API’ler, uygulamaların birbirleriyle konuşmasını sağlayan temel araçlar. Ama zamanla iş gereksinimleri değişiyor, yeni özellikler ekleniyor, veri modelleri evriliyor. Bu değişimler bazen mevcut API’lerin davranışını veya yapısını bozabiliyor. İşte tam da bu noktada versioning devreye giriyor.
Eğer bir API’de breaking change yaparsam, yani mevcut bir endpoint’in çıktısını, girdisini veya davranışını geri uyumlu olmayacak şekilde değiştirirsem, o API’yi kullanan tüm client uygulamaları (mobil, web, diğer servisler) anında bozulur. Bir veri modelindeki ufak bir değişikliğin raporların yanlış üretilmesine yol açması ya da mobil uygulamanın eski versiyonlarının backend’deki bir schema değişikliği yüzünden kilitlenmesi tipik senaryolardır. Versioning, bu tür felaketleri önlemek ve farklı client’ların kendi hızlarında güncellenmesine olanak tanımak için kritik bir mekanizma.
URI Path Versioning: En Yaygın Yaklaşım
URI (Uniform Resource Identifier) path üzerinden versioning, sanırım sektörde en çok gördüğüm ve en basit olan yaklaşımdır. Burada API versiyonu doğrudan URL’in bir parçası olarak belirtilir, örneğin /api/v1/users veya /api/v2/products.
Bu yaklaşımın en büyük artısı, anlaşılırlığı ve keşfedilebilirliğidir. Bir geliştirici, URL’e bakarak hangi versiyonla çalıştığını hemen anlar. Ayrıca, HTTP cache mekanizmalarıyla da gayet uyumludur, çünkü her versiyonun kendi benzersiz URL’i vardır. Birçok projede ilk tercihim bu olmuştur, özellikle hızlıca MVP çıkarmam gereken durumlarda. Nginx üzerinde basit bir location bloğu ile kolayca yönlendirme yapabiliyordum. Ancak, uzun vadede birden fazla versiyonu desteklemeye başladığımda, router konfigürasyonlarının ve kod tabanının biraz karmaşıklaştığını fark ettim. Her versiyon için ayrı kod blokları veya ayrı route dosyaları yönetmek gerekebiliyordu.
# Nginx ile URI Path Versioning örneği
server {
listen 80;
server_name api.example.com;
location /api/v1/ {
proxy_pass http://backend_v1_service;
# Diğer proxy ayarları
}
location /api/v2/ {
proxy_pass http://backend_v2_service;
# Diğer proxy ayarları
}
}
Yukarıdaki Nginx örneği, /api/v1 ile gelen istekleri backend_v1_service’e, /api/v2 ile gelenleri ise backend_v2_service’e yönlendirir. Bu yapı, özellikle farklı versiyonların farklı microservice’ler tarafından sunulduğu durumlarda oldukça kullanışlıdır. Ancak, eğer aynı microservice içinde birden fazla versiyonu yönetiyorsanız, kodunuz içinde if version == 'v1' gibi kontrol blokları görmeye başlayabilirsiniz ki bu da teknik borcu artırır. Birden fazla eski versiyonun (v1, v2, v3) aynı anda aktif kaldığı durumlarda kodun okunabilirliği hızla düşer; bu, URI versioning’in en sık görülen bedelidir.
Header Versioning: Esnekliğin Bedeli
Header versioning, API versiyonunu HTTP başlıkları (headers) aracılığıyla iletmektir. En yaygın iki yolu vardır: özel bir başlık kullanmak (X-API-Version: 1) veya Accept başlığını kullanıp özel bir media type belirtmek (Accept: application/vnd.myapi.v2+json).
Bu yaklaşımın en cazip yanı, URI’lerin temiz kalmasıdır. Kaynakların URL’i değişmez, sadece istediğiniz temsili başlıklar aracılığıyla istersiniz. Bu durum, özellikle RESTful mimarinin “kaynak odaklılık” prensibine daha uygun hissedilir. Farklı istemcilerin aynı kaynaklara farklı temsillerle erişmesi gerektiğinde bu yöntem esneklik açısından çok iş görür: aynı /users endpoint’inden, X-API-Version: 1 ile eski ve X-API-Version: 2 ile yeni formatta kullanıcı listesi alınabilir. Ancak, bu esnekliğin bir bedeli vardır.
Test etmesi ve hata ayıklaması URI versioning’e göre daha karmaşıktır. Ayrıca, bazı proxy sunucuları veya CDN’ler, Accept veya özel HTTP başlıklarını doğru şekilde işlemeyebilir, bu da önbellekleme veya yönlendirme sorunlarına yol açabilir. Kendi bir yan ürünümün backend’inde bunu denediğimde, basit bir curl komutuyla bile farklı versiyonları denemek URI kadar pratik gelmemişti. Geliştirici ekibinin bu yaklaşıma alışması ve doğru Accept başlığını sürekli hatırlaması zaman almıştı.
# Header Versioning (Custom Header) örneği
curl -H "X-API-Version: 2" https://api.example.com/products/123
# Header Versioning (Media Type) örneği
curl -H "Accept: application/vnd.myapi.v2+json" https://api.example.com/users
Query Parameter Versioning: Hızlı ve Kirli mi?
Query parameter versioning, API versiyonunu URL’in sorgu parametresi olarak iletmektir: /api/resource?version=1 veya /api/resource?v=2.
Bu yöntem, implementasyonu en hızlı olanlardan biridir. Özellikle bir prototip geliştirirken veya iç API’lerde, hızlıca bir versiyonu devreye almam gerektiğinde kullandığım oldu. Geçici bir raporlama API’sinin farklı çıktılarını test etmek için ?format=v1 ve ?format=v2 gibi parametreler kullanmak hızlı denemeler için pratiktir. Tarayıcıdan doğrudan URL’i değiştirerek farklı versiyonları görmek kolaydır, bu da hızlı test senaryoları için avantajlıdır.
Ancak, bu yaklaşımın ciddi dezavantajları var. Öncelikle, HTTP caching mekanizmaları için sorun teşkil edebilir. Farklı sorgu parametreleri, aynı kaynağın farklı versiyonları olsa bile ayrı ayrı önbellek girdileri oluşturabilir, bu da önbellek verimliliğini düşürür. İkincisi, URI’nin semantik anlamını bozar. Bir kaynağın versiyonu, kaynağın kendisinin bir özelliği olmaktan çok, o kaynağın nasıl bir temsilinin istendiğiyle ilgili olmalıdır. ?version=X kullanmak, sanki kaynağın kendisi versionlanmış gibi bir algı yaratır.
# FastAPI ile Query Parameter Versioning örneği
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(version: int = Query(1, description="API version")):
if version == 1:
return {"message": "Hello from V1"}
elif version == 2:
return {"message": "Greetings from V2!"}
return {"message": "Invalid version"}
Content Negotiation (Media Type Versioning): RESTful Yaklaşım
Content Negotiation, HTTP Accept başlığını kullanarak farklı veri formatlarını veya versiyonlarını talep etme prensibine dayanır. Bu, genellikle application/vnd.company.app.v1+json gibi özel bir media type belirtilerek yapılır. Bu yaklaşım, RESTful mimarinin temel prensiplerinden biri olan “aynı kaynağın farklı temsilleri” fikrine en uygun olanıdır.
Bu yöntemin en büyük avantajı, URI’nin tamamen sabit kalmasıdır. /users kaynağı her zaman /users olarak kalır, ancak client’ın Accept başlığında belirttiği media type’a göre farklı bir temsil (farklı bir veri şeması veya versiyon) döner. Bu durum, özellikle bir kaynağın yaşam döngüsü boyunca farklı ihtiyaçlara göre evrildiği durumlarda çok esneklik sağlar. Bir müşteri projesinde, aynı sipariş kaynağının hem detaylı bir muhasebe görünümüne hem de basit bir mobil uygulama görünümüne sahip olması gerektiğinde bu yaklaşımı değerlendirmiştik. Aynı /orders/{id} endpoint’inden, Accept: application/vnd.company.erp.v1+json ile ERP’ye özel detaylar, Accept: application/vnd.company.mobile.v1+json ile mobil uygulamaya özel özet veriler dönebiliyordu.
# Flask ile Content Negotiation örneği
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/products/<int:product_id>', methods=['GET'])
def get_product(product_id):
accept_header = request.headers.get('Accept', '')
if 'application/vnd.myapi.v2+json' in accept_header:
return jsonify({"id": product_id, "name": f"Product v2 {product_id}", "price_usd": 12.99})
elif 'application/vnd.myapi.v1+json' in accept_header:
return jsonify({"id": product_id, "product_name": f"Product v1 {product_id}", "current_price": "12.99 USD"})
else:
# Varsayılan veya hataya düşen durum
return jsonify({"id": product_id, "name": f"Product default {product_id}"})
if __name__ == '__main__':
app.run(debug=True)
Ancak, bu yaklaşımın da kendine göre zorlukları var. Implementasyonu diğerlerine göre daha karmaşık olabilir ve client’ların doğru media type’ları bilmesi ve kullanması gerekir. Geliştirici araçları ve kütüphaneleri arasında Accept başlığını doğru yönetme konusunda farklılıklar olabilir. Çoğu ekipte bu kadar esnek bir yapıya ihtiyaç olsa bile, karmaşıklığı yönetme maliyeti yüksek olacağı için daha basit bir URI versioning makul bir tercih olabilir. Her zaman basitlik ve esneklik arasında bir trade-off vardır.
Versioning Olmadan İleriye Yönelik Stratejiler: NoVersioning
“Versioning yapmayalım” fikri ilk duyulduğunda kulağa çılgınca gelebilir, ama bazı senaryolarda gayet mantıklı olabilir. Bu yaklaşım, API’yi her zaman geri uyumlu (backward-compatible) tutmaya çalışmak veya breaking change olduğunda tamamen yeni bir endpoint açmaktır.
Buradaki temel felsefe, API’nin evrimini breaking change’leri minimumda tutarak yönetmektir. Örneğin, mevcut bir JSON objesine yeni bir alan eklemek genellikle geri uyumludur, çünkü eski client’lar bu yeni alanı görmezden gelebilir. Ancak, mevcut bir alanı kaldırmak, adını değiştirmek veya veri tipini değiştirmek breaking change’dir. NoVersioning stratejisi izlerken, bu tür breaking change’lerden kesinlikle kaçınmak veya çok dikkatli olmak gerekir. Benzer bir durumu kendi finansal hesaplayıcılarımın backend’inde uygulamıştım. Kullanıcı sayım az olduğu ve API’yi sadece kendi uygulamalarım kullandığı için, breaking change olduğunda tüm client’ları aynı anda güncelleyebiliyordum.
Bu yaklaşımın avantajı, API’nin her zaman en güncel ve tek bir versiyonunun olmasıdır. Client’lar her zaman en son özelliklere erişebilir ve eski versiyonları destekleme yükü ortadan kalkar. Ancak, en büyük dezavantajı, breaking change yönetimidir. Eğer gerçekten breaking change yapmanız gerekiyorsa, eski bir endpoint’i tamamen sonlandırmanız veya yeni bir isimle tamamen yeni bir endpoint açmanız gerekir. Bu da URI versioning’e benzer bir durum yaratır.
-- Geri uyumlu veritabanı şema değişikliği örneği
-- Mevcut 'users' tablosuna yeni bir sütun ekleme
ALTER TABLE users
ADD COLUMN created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP;
-- Mevcut 'products' tablosunda sütun adını değiştirmek breaking change olurdu
-- ALTER TABLE products RENAME COLUMN price TO unit_price; -- Bu kaçınılmalı!
Bu strateji, özellikle iç API’ler, küçük ekipler veya hızlı değişen ürünler için uygun olabilir. Ancak, geniş bir client tabanına sahip, farklı sürümlerin eş zamanlı olarak çalışması gereken halka açık API’ler için genellikle risklidir. İç servisler arası iletişimde, tüm servisler aynı çatı altında ve eş zamanlı deploy ediliyorsa bu mantık iyi çalışır.
Hangi Stratejiyi Ne Zaman Seçmeli? Trade-off Analizi
API versioning stratejisi seçimi, projenin doğasına, client tabanına, geliştirme hızına ve sürdürülebilirlik hedeflerine göre değişir. “Tek doğru yol” diye bir şey yoktur; önemli olan, karşılaştığınız sorunlara en uygun dengeyi bulmaktır. Saha tecrübemde, her projenin kendi dinamiklerinin farklı bir kararı gerektirdiğini gördüm.
Bir üretim ERP’sinde, dışarıdan entegrasyonlar çok olduğu için URI path versioning’i tercih etmiştim. Basit ve anlaşılır olduğu için entegrasyon yapan diğer firmaların adaptasyonu kolay olmuştu. Mobil uygulamalarımın backend’inde ise, daha çok X-API-Version gibi header versioning’i kullandım, çünkü mobil client’ların güncelleme döngüleri web’e göre daha yavaş olabiliyor ve farklı versiyonları uzun süre desteklemem gerekiyordu. Kendi basit yan ürünlerimde ise, bazen query parameter versioning ile hızlıca prototip çıkarıp, stabil hale geldiğinde URI versioning’e geçiş yaptım.
Aşağıdaki tablo, farklı stratejilerin temel özelliklerini ve benim deneyimlerimdeki kullanım senaryolarını özetliyor:
| Strateji | Avantajlar | Dezavantajlar | Ne Zaman Kullanırım |
|---|---|---|---|
| URI Path Versioning | Basit, keşfedilebilir, cache uyumlu. | URI kirliliği, router karmaşası artabilir. | Çoğu dışa açık API, çok client’lı sistemler, microservice tabanlı projeler. |
| Header Versioning | URI temiz kalır, esnek, RESTful. | Keşfedilebilirlik zor, test karmaşık, proxy sorunları. | Karmaşık iç API’ler, aynı kaynağın farklı temsillerine ihtiyaç duyulan durumlarda. |
| Query Parameter Versioning | Hızlı implementasyon, tarayıcıda kolay test. | Cache sorunları, semantik bozukluk, güvenlik riski. | Hızlı prototipler, iç API’ler, çok basit ve az client’lı uygulamalar. |
| Content Negotiation | RESTful, URI temiz, yüksek esneklik. | Implementasyon karmaşık, tooling desteği değişir. | Çok katmanlı, büyük kurumsal sistemler, farklı client’ların çok farklı ihtiyaçları varsa. |
| NoVersioning | En basit, her zaman güncel API, bakım yükü az. | Breaking change yönetimi zor, sürekli geri uyumluluk. | İç API’ler, küçük ekipler, hızlı değişen ürünler, client’ların kontrolümde olduğu yerler. |
Bu tabloya bakarken, her bir stratejinin kendine özgü bir “fiyat etiketi” olduğunu unutmamak gerekir. Basitlik genellikle esneklikten ödün verirken, esneklik de beraberinde bir karmaşıklık ve geliştirme maliyeti getirir. Örneğin Content Negotiation gibi esnek bir yaklaşım başta geniş bir hareket alanı sağlasa da, ekip içindeki onboarding ve yeni geliştiricilerin adaptasyonu uzayabilir; bu da projenin toplam maliyetine yansır.
Sonuç: Karar Sürekli Bir Optimizasyon Meselesi
API versioning stratejisi seçimi, tek seferlik bir karar değildir. Projenin yaşam döngüsü boyunca evrilebilir ve ihtiyaçlar değiştikçe farklı yaklaşımlara geçiş yapmak gerekebilir. Önemli olan, seçtiğiniz stratejinin projenizin mevcut ihtiyaçlarına ve gelecekteki büyüme planlarına uygun olmasıdır. Kendi kariyerimde gördüğüm en büyük hatalardan biri, ilk kararın katı bir şekilde sürdürülmeye çalışılmasıydı, oysa esneklik ve pragmatizm çoğu zaman daha iyi sonuçlar veriyor.
Benim tavsiyem, her zaman en basit çözümle başlamak ve ancak gerçekten ihtiyaç duyulduğunda daha karmaşık stratejilere geçiş yapmaktır. Bu, gereksiz mühendislik yapmaktan kaçınmanızı ve “olur o kadar” felsefesiyle ilerlemenizi sağlar. Unutmayın, API’ler yaşayan organizmalar gibidir; zamanla büyür, değişir ve evrilirler. Önemli olan, bu evrimi kontrollü ve sürdürülebilir bir şekilde yönetmektir.