API versioning, yazılım dünyasında “ne zaman yapmalıyım?” sorusundan çok, “nasıl yapmalıyım?” sorusunun daha kritik olduğu bir konu. Bir üretim firmasının ERP’sinde mobil operatör ekranları tasarlarken veya bir bankanın iç platformları için entegrasyonlar geliştirirken, API’lerin zamanla değişeceği gerçeğiyle hep yüzleştim. Bu değişiklikleri yönetmek, hem geliştirici ekibinin iş yükünü hem de entegre olan üçüncü parti sistemlerin uyumunu doğrudan etkiliyor.
API’ler evrim geçirirken, eski istemcilerin çalışmaya devam etmesini sağlamak, yeni özellikler sunarken mevcut işlevselliği bozmamak temel amacımızdır. Bu süreçte karşıma çıkan farklı versioning stratejilerini ve bu stratejilerin pratikte ne gibi sonuçlar doğurduğunu, edindiğim tecrübelerle birlikte bu yazıda ele alacağım. Benim için her zaman önemli olan, seçilen yaklaşımın projenin dinamiklerine ve ekibin yetkinliklerine uygun olmasıdır.
API Versioning Neden Bir Zorunluluktur?
Bir API’yi canlıya aldığımızda, o API’nin bir “sözleşme” olduğunu unutmamak gerekiyor. Bu sözleşme, API’nin beklediği girdi formatını, döndüreceği çıktı yapısını ve hata kodlarını kapsar. Eğer bu sözleşmeyi değiştirirsek, bu API’yi kullanan tüm istemcilerin de bu değişikliğe uyum sağlaması gerekir. İşte bu uyum sürecini yönetmek için versioning devreye giriyor.
Örneğin, bir üretim ERP’sinde, ana ürün listesi API’sine productCode yerine sku adında yeni bir alan eklediğimizi düşünelim. Eğer bu değişikliği direkt canlı API’ye yansıtırsak, productCode bekleyen tüm eski mobil uygulamalar veya entegre iş ortakları anında hata almaya başlayacaktır. Bu, üretim hattının durmasına, sevkiyatların aksamasına veya faturalama süreçlerinin bozulmasına yol açabilir. Bu tarz bir durumla karşılaşmamak için, API’nin farklı versiyonlarını aynı anda destekleyebilmek, geçiş sürecini pürüzsüz hale getirmenin anahtarıdır. Benim deneyimimde, bu tür değişiklikler genellikle organizasyonel akıştaki bir dönüşümden veya yeni bir iş ihtiyacından kaynaklanıyor. Yazılım mimarisi çoğu zaman yazılım değil, organizasyonel akıştır, derken tam da bunu kastediyorum.
URI Versioning: En Yaygın, En Tartışmalı Yaklaşım
URI (Uniform Resource Identifier) versioning, API versiyonunu doğrudan URL yoluna dahil etme yöntemidir; örneğin /api/v1/products veya /api/v2/users. Bu yöntem, sadeliği ve tarayıcıda doğrudan test edilebilirliği nedeniyle oldukça popülerdir. RESTful prensiplere en yakın duran yaklaşımlardan biri olarak kabul edilir çünkü her versiyonu ayrı bir kaynak gibi ele alır.
Bir yan ürünümün backend’inde ilk API’leri geliştirirken ben de bu yöntemi kullanmıştım. Nginx reverse proxy ayarlarını yaparken, /v1/ ile başlayan istekleri belirli bir upstream sunucusuna, /v2/ ile başlayanları ise başka birine yönlendirmek oldukça kolaydı. Aşağıdaki Nginx konfigürasyonu, bu durumu basitçe örneklendiriyor:
server {
listen 80;
server_name api.example.com;
location /v1/ {
proxy_pass http://backend_v1_cluster;
# Diğer proxy ayarları...
}
location /v2/ {
proxy_pass http://backend_v2_cluster;
# Diğer proxy ayarları...
}
}
Ancak bu basitliğin bazı dezavantajları da var. API’nin her versiyonu için ayrı bir URL yolu tutmak, zamanla URL’lerin şişmesine ve routing mantığının karmaşıklaşmasına neden olabilir. Özellikle sadece küçük bir alanın değiştiği durumlarda yeni bir ana versiyon çıkarmak, gereksiz kod duplikasyonuna veya bakım maliyetine yol açabilir. Ayrıca, istemcilerin her versiyon değişikliğinde URL’lerini güncellemesi gerekir ki bu da entegrasyon maliyetini artırır. Bir e-ticaret sitesinde ürün katalog API’si üzerinde çalışırken, /v1/products, /v2/products, /v3/products şeklinde ilerleyen URL’lerin getirdiği yönetim yükünü bizzat yaşadım. Nginx ile Gelişmiş Yönlendirme Teknikleri yazısında daha detaylı değinmiştim.
Header Versioning: Esnekliğin Adresi
Header versioning, API versiyonunu HTTP başlıklarına ekleyerek yönetme yöntemidir; örneğin Accept-Version: v1 veya X-API-Version: 2. Bu yaklaşım, URL’lerin temiz kalmasını sağlar ve bir kaynağın farklı versiyonlarını aynı URL üzerinden sunma esnekliği sunar. Özellikle iç mikroservis iletişimlerinde veya gelişmiş istemciler tarafından kullanıldığında oldukça avantajlı olabilir.
Benim deneyimimde, büyük bir e-ticaret sitesinin iç entegrasyonlarında bu yöntemi sıkça kullandık. Backend servisleri arasında veri alışverişi yaparken, X-API-Version başlığını kullanarak hangi versiyonu istediğimizi belirtiyorduk. Bu sayede, API gateway veya yük dengeleyici, isteği doğru servis versiyonuna yönlendirebiliyordu. Aşağıdaki curl komutu, bir header ile versiyon belirtmeyi gösterir:
curl -H "X-API-Version: 2" https://api.example.com/products/123
Bu yöntemin dezavantajı ise, tarayıcıda doğrudan test etmenin daha zor olmasıdır çünkü tarayıcılar varsayılan olarak özel HTTP başlıklarını göndermezler. Ayrıca, standart CDN’ler tarafından doğrudan önbelleğe alınması URI versioning kadar kolay değildir, özel önbellekleme kuralları gerekebilir. Bir mobil uygulamamın backend’i için geliştirme yaparken, bazı mobil istemcilerin Accept header’ını doğru bir şekilde yönetmekte zorlandığını, bu yüzden X-API-Version gibi özel bir header kullanmanın daha pratik olduğunu gördüm. Bu, genellikle istemci tarafındaki SDK’ların veya kütüphanelerin karmaşıklığıyla da alakalıydı.
Query Parameter Versioning: Hızlı Çözümler ve Riskleri
Query parameter versioning, API versiyonunu URL’nin sorgu dizisine ekleyerek yapılır; örneğin /products?version=1 veya /users?api_version=2. Bu yöntem, genellikle hızlı ve kolay bir şekilde versiyonlama ihtiyacını karşılamak için tercih edilir. Özellikle prototipleme aşamasında veya çok sık değişmeyecek, basit API’lerde kullanılabilir.
Küçük bir raporlama servisi için hızlıca bir versiyonlama ihtiyacı doğduğunda bu yönteme başvurmak mantıklı olabilir. Ana API’yi değiştirmeden, sadece belirli bir raporun çıktısını farklı bir formatta sunmak için ?format=v2 gibi bir parametre eklemek yeterli olabilir. FastAPI ile basit bir örnek:
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(version: int = Query(1)):
if version == 1:
return {"message": "Items from API v1"}
elif version == 2:
return {"message": "Items from API v2 with new features"}
return {"message": "Unknown API version"}
Ancak bu yöntemin ciddi dezavantajları vardır. İlk olarak, URL’lerin okunabilirliğini ve temizliğini bozar. İkincisi, sorgu parametreleri genellikle bir kaynağın farklı temsillerini değil, farklı filtrelerini veya sıralama seçeneklerini belirtmek için kullanılır. Bu yüzden semantik olarak kafa karıştırıcı olabilir. Üçüncüsü, önbellekleme stratejileri açısından sorun yaratabilir; aynı URL’nin farklı versiyonları farklı önbellek girişleri gerektirebilir ve bu da önbellek isabet oranını düşürebilir. Benim kişisel tercihim, bu yöntemi kritik olmayan, dahili araçlar dışında mümkün olduğunca kullanmamaktır. Genellikle bir query parameter ile versiyonlamaya başladığımda, kısa sürede daha sağlam bir çözüme geçme ihtiyacı hissediyorum.
API Deprecation ve Sunset Politikaları: Kaçınılmaz Bir Süreç
Bir API versiyonunu canlıya almak kadar, onu günün birinde devre dışı bırakmak da sürecin önemli bir parçasıdır. Gelişen teknoloji, değişen iş gereksinimleri veya güvenlik açıkları, eski versiyonların artık desteklenemeyeceği noktaya gelmemize neden olabilir. Bu duruma “deprecation” (kullanımdan kaldırma) ve “sunset” (devre dışı bırakma) politikalarıyla yaklaşmak gerekir.
Bir bankanın iç platformunda, eski bir entegrasyon API’sini devre dışı bırakırken, aşağıdaki adımları izledim:
- Duyuru: Kullanıcılara (genellikle diğer dahili ekipler) API’nin ne zaman kullanımdan kalkacağını ve yeni versiyona geçiş için ne kadar süreleri olduğunu bildirdik. Bu duyuru genellikle 6 ay önceden yapılırdı.
- Uyarı Başlıkları: Eski versiyonu kullanan isteklere HTTP
Warningbaşlığı ekledik. Örneğin:Warning: 299 - "API v1 will be deprecated on 2027-01-01. Please migrate to v2." - Loglama ve Takip: Eski API versiyonunu kullanan istemcileri ve kullanım sıklıklarını detaylı olarak logladık. Bu, geçiş sürecini izlememize ve destek ihtiyacı olan ekipleri tespit etmemize yardımcı oldu.
journaldvePrometheusmetriklerini kullanarakv1API çağrılarının zamanla nasıl gerilediğini takip etmek, kapatma kararını veri üzerinden almamızı sağladı. - Devre Dışı Bırakma: Belirlenen tarihte eski versiyonu tamamen kapattık ve isteklere
410 GoneHTTP durumu döndürdük.
Bu süreçte önemli olan, şeffaf olmak ve kullanıcılara yeterli zaman tanımaktır. Özellikle mobil uygulama backend’lerinde, mağaza güncelleme süreçlerinin yavaşlığı nedeniyle deprecation süresini daha uzun tutmak gerekebilir; istemcilerin yeni versiyona geçişi sizin kontrolünüzde olmayan faktörlere bağlı olabilir.
Zero-Downtime Geçişler ve Rollback Stratejileri
API versioning, sistemlerimizi güncellerken kesintisiz hizmet vermenin (zero-downtime deployment) ve olası bir sorunda hızlıca geri dönebilmenin (rollback) temelini oluşturur. Yeni bir API versiyonunu canlıya alırken, genellikle mevcut versiyonla bir süre paralel çalıştırmak en güvenli yaklaşımdır.
Benim üretim ortamlarındaki favori stratejim, blue-green deployment veya canary deployment’ı versioning ile birleştirmektir. Örneğin, v1 API’sinin çalıştığı bir blue ortamı varken, v2 API’sinin çalıştığı green ortamını kurarız. Tüm testler bittikten sonra, yük dengeleyiciyi blue ortamından green ortama işaret ederiz. Eğer bir sorun olursa, hızlıca blue ortama geri dönebiliriz. Docker Compose ile bu tür bir paralel çalışmayı simüle etmek oldukça kolaydır:
# docker-compose.yml
services:
api_v1:
image: my-api:1.0.0
ports:
- "8001:80"
environment:
API_VERSION: v1
api_v2:
image: my-api:2.0.0
ports:
- "8002:80"
environment:
API_VERSION: v2
Bu yapılandırma ile Nginx gibi bir reverse proxy’yi kullanarak gelen isteğin X-API-Version başlığına göre api_v1 veya api_v2 servislerine yönlendirme yapabilirim. Bu, bana hem v1 hem de v2’yi aynı anda çalıştırıp trafik yönlendirme esnekliği sağlar. Yeni bir v2 API’sinde örneğin N+1 sorgu problemi gibi bir performans düşüşü ortaya çıktığında, loglarda anomaliyi fark edip yük dengeleyiciyi tekrar v1’e işaret ederek hızlıca geri dönebilirsiniz. Bu tür anlık geri dönüşler, API versioning ve doğru deployment stratejileri sayesinde mümkün oluyor.
Versionlamanın Ötesinde: API Evrimi ve Organizasyonel Akış
API versioning stratejileri teknik olarak önemli olsa da, asıl mesele API’lerin neden bu kadar sık değiştiği ve bu değişikliklerin nasıl yönetildiğidir. Benim gözlemime göre, sık sık major versiyon çıkarmak zorunda kalmak, çoğu zaman API tasarımında veya organizasyonel süreçlerde bir eksikliğin işaretidir. Event-sourcing, CQRS gibi mimari desenler veya idempotency gibi prensipler, API’lerin daha evrimsel ve daha az kırılgan olmasını sağlayabilir.
Örneğin, bir üretim ERP’sinde stok hareketlerini yöneten bir API düşünelim. Başlangıçta sadece productId ve quantity alıyorken, zamanla batchNumber, warehouseLocation gibi yeni alanlar eklenmesi gerekebilir. Eğer API’mizi esnek tasarladıysak ve yeni alanları opsiyonel olarak ekleyebiliyorsak, bu bir v2 gerektirmeyebilir. Ancak zorunlu alanlarda yapılan değişiklikler veya mevcut alanların veri tiplerindeki dönüşümler kaçınılmaz olarak yeni bir versiyonu tetikler. Benim için önemli olan, API’yi tasarlarken gelecekteki olası değişimleri öngörmeye çalışmak ve bu değişimlere minimum eforla uyum sağlayabilecek bir yapı kurmaktır.
Bu, sadece teknik bir konu değil; aynı zamanda ekipler arası iletişim, ürün yönetimi ve iş stratejilerinin bir yansımasıdır. Bir API’nin evrimi, aslında onu kullanan iş süreçlerinin evrimidir. Bu yüzden, API versioning’e sadece bir teknik uygulama olarak değil, organizasyonel bir süreç olarak yaklaşmak gerektiğini düşünüyorum. Daha önce Kurumsal Yazılımda Event Sourcing Deneyimlerim yazısında bu tür mimarilerin faydalarından bahsetmiştim.
Sonuç: Pragmatik Bir Yaklaşım Şart
API versioning, yazılım geliştirme sürecinin kaçınılmaz bir parçasıdır. Gördüğümüz gibi, her stratejinin kendine göre avantajları ve dezavantajları var. Benim net pozisyonum, projenin ölçeğine, istemci kitlesine ve ekibin teknik yetkinliğine göre en pragmatik çözümü seçmektir.
Eğer dış dünyaya açık, geniş bir geliştirici kitlesi tarafından kullanılacak bir API geliştiriyorsam, URI versioning’in basitliği ve cacheable olması genelde iyi bir başlangıç noktasıdır. Ancak iç mikroservis iletişimleri veya daha kontrollü ortamlarda, header versioning bana daha fazla esneklik sunar. Query parameter versioning ise genellikle geçici çözümler veya çok niş ihtiyaçlar için ayırdığım bir yaklaşımdır. Önemli olan, hangi yöntemi seçersek seçelim, deprecation ve sunset politikalarımızı net bir şekilde belirlemek ve bu süreçleri şeffaf bir şekilde yönetmektir. Aksi takdirde, API’lerimizin evrimi, bir kaos ortamına dönüşebilir. Bir sonraki yazıda, bu API’lerin performansını nasıl izlediğimi ve observability metriklerini nasıl kullandığımı anlatacağım.