Bir API’yi canlıya aldığınız anda, onunla ilgili en büyük kabuslardan biri versiyonlama kararlarıdır. Özellikle de bu API’nin farklı istemciler tarafından kullanıldığını veya uzun ömürlü olmasını beklediğinizi düşünürsek, işler daha karmaşık bir hal alır. Geçmişte bu konuyu hafife alıp başıma açtığım dertleri düşününce, bugünkü pragmatik yaklaşımımın temelinde o acı deneyimler yatıyor.
API versiyonlama, bir uygulamanın zaman içinde evrimleşirken, mevcut istemcilerin kesintiye uğramamasını sağlamanın kilit yollarından biridir. Yaptığım bir üretim ERP’sinde, küçük bir JSON alan adının değişmesi yüzünden sahada çalışan çok sayıda operatör ekranının bir anda hata vermesi, bu konunun ne kadar kritik olduğunu bana acı bir şekilde öğretmişti. Bu yazıda, API versiyonlama yaklaşımlarını, benim projelerimde edindiğim tecrübeleri ve bu seçimlerin getirdiği trade-off’ları anlatacağım.
API Versiyonlama Neden Gerekli? İlk Yanlış Seçimlerim
Bir API’yi geliştirirken, ilk başta her şey tek bir versiyon üzerinden gider, kolaydır. Ancak zamanla iş gereksinimleri değişir, yeni özellikler eklenir, eski özellikler güncellenir veya kaldırılır. Bu değişiklikler, API’nin tüketici tarafındaki uygulamaları (mobil uygulamalar, web frontend’ler, diğer servisler) doğrudan etkiler ve breaking change olarak adlandırılır. Bir breaking change, API’yi kullanan uygulamanın kodunu değiştirmeden çalışmamasına neden olur.
Yaptığım ilk projelerde bu konuyu göz ardı ettim. Bir finansal hesaplayıcı yan ürünümün backend’ini geliştirirken, “nasıl olsa ben kullanıyorum, gerek yok” düşüncesiyle versiyonlama yapmadım. Birkaç ay sonra yeni bir özellik eklerken, mevcut mobil uygulamamın backend’den aldığı veri formatını değiştirmek zorunda kaldım. Sonuç: hem backend hem de mobil uygulamayı aynı anda deploy edip, kullanıcının uygulamasını güncellemesini beklemek zorunda kaldım. Bu, sıfır downtime beklentisi olan bir sistem için kabul edilemez bir durumdu.
Bu erken hatalar, bana versiyonlamanın sadece bir “teknik detay” olmadığını, aynı zamanda bir ürün stratejisi ve operasyonel esneklik meselesi olduğunu öğretti. API’nizin ömrünü uzatmak ve istemcilerinizi mağdur etmemek için daha baştan bir versiyonlama stratejisi belirlemek şart. Aksi takdirde, her değişiklikte büyük bir regresyon riskiyle karşı karşıya kalırız.
Başlıca API Versiyonlama Yaklaşımları: Artıları ve Eksileri
API versiyonlamanın temelinde üç ana yaklaşım var: URL Path, Query Parameter ve Header (Content Negotiation). Her birinin kendine göre artıları ve eksileri bulunuyor ve projenin ihtiyaçlarına göre doğru seçimi yapmak gerekiyor. Ben de farklı projelerde farklı yaklaşımları deneyimledim ve her birinin ne zaman işe yaradığını, ne zaman baş ağrısı yarattığını gördüm.
| Yaklaşım | Artıları | Eksileri | Ne Zaman Tercih Ettim |
|---|---|---|---|
| URL Path | - Basit, anlaşılır, keşfedilebilir | - URL bloat’a neden olabilir | - Public API’ler, basit projeler (kendi yan ürünlerim) |
| Query Parameter | - URL’leri nispeten temiz tutar | - Kötüye kullanıma açık, keşfedilebilirlik düşük | - Nadiren, genellikle bir “fallback” olarak |
| Header (Custom) | - URL’leri en temiz tutar | - Keşfedilebilirlik düşük, client desteği gerekir | - Şirket içi API’ler, sıkı kontrolün olduğu yerler (ERP) |
| Header (Accept) | - HTTP standardına uygun, Content Negotiation | - Client implementasyonu daha karmaşık | - Farklı çıktı formatları gerektiren durumlarda |
Genel bir kural olarak, harici ve public API’ler için genellikle URL Path yöntemini tercih ettim çünkü basitliği ve anlaşılırlığı, dış geliştiriciler için en az sürtünmeyi sağlıyor. İç API’lerde ise daha esnek ve URL’leri temiz tutan Header tabanlı yaklaşımlara yönelebiliyorum.
URL Path Versiyonlama: Doğrudan ve Anlaşılır
URL Path versiyonlama, belki de en yaygın ve en anlaşılır yöntemdir. API’nin versiyon numarasını doğrudan URL yoluna dahil edersiniz, örneğin /v1/users veya /api/v2/products. Bu yöntem, tarayıcıda bile kolayca test edilebilir ve dokümantasyonu basittir.
Bir üretim ERP’si geliştirirken, dışarıya açık olan birkaç entegrasyon API’si için bu yöntemi kullandım. Müşterilerimiz, hangi versiyonu çağırdıklarını URL’den açıkça görebiliyordu. Bununla birlikte, iki farklı versiyonu aynı anda canlıda tuttuğumda, backend kodunda if version == "v1" blokları görmeye başlamıştım ki bu, kod kalitesi açısından pek hoş bir durum değildi. Nginx gibi bir reverse proxy ile bu durumu biraz daha yönetilebilir hale getirebiliriz.
# Nginx config ornegi
server {
listen 80;
server_name api.example.com;
location ~ ^/(v1|v2)/ {
# v1 veya v2 path'leri icin yonlendirme
proxy_pass http://backend_api_cluster/$request_uri;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /v1/ {
# v1 icin ozel bir backend veya logic
proxy_pass http://v1_backend_cluster;
proxy_set_header Host $host;
}
location /v2/ {
# v2 icin ozel bir backend veya logic
proxy_pass http://v2_backend_cluster;
proxy_set_header Host $host;
}
}
Yukarıdaki Nginx örneğinde, farklı versiyonları farklı backend’lere yönlendirebilirsiniz. Bu, bana hem farklı versiyonları ayrı ayrı deploy etme esnekliği verdi hem de eski versiyonları desteklerken yeni versiyonları bağımsızca geliştirmemi sağladı. Ancak bu yaklaşım, URL’lerin uzamasına ve her yeni versiyonda yeni bir path tanımlama ihtiyacına yol açar. Bir de v3, v4 diye giderken URL’lerin “bloat” etmesi kaçınılmaz oluyor.
Header ve Content Negotiation Versiyonlama: Daha Esnek Yöntemler
Header tabanlı versiyonlama, API versiyonunu HTTP Request Header’ları üzerinden belirtir. Bu, URL’leri temiz tutar ve API’nin temel kaynak URI’sının değişmemesini sağlar. Örneğin, X-API-Version: 2 gibi özel bir header kullanabiliriz.
Content Negotiation ile yapılan versiyonlama ise HTTP standardına daha uygun bir yaklaşımdır. Accept header’ını kullanarak istemci, belirli bir medya türü veya API versiyonunu talep ettiğini belirtir. Örneğin, Accept: application/vnd.myapi.v2+json gibi bir değerle, istemci “myapi’nin 2. versiyon JSON formatını istiyorum” demiş olur. Ben bu yaklaşımı, bir müşteri projesinde, aynı endpoint’in farklı veri modelleriyle çalışması gerektiği durumlarda kullandım.
# FastAPI ornegi: Accept header'i ile versiyonlama
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/items")
async def get_items(request: Request):
accept_header = request.headers.get("Accept")
if "application/vnd.myapi.v1+json" in accept_header:
# v1 modeline gore veri dondur
return JSONResponse({"version": "v1", "data": [{"id": 1, "name": "Item A"}]})
elif "application/vnd.myapi.v2+json" in accept_header:
# v2 modeline gore veri dondur (ornegin, ek bir alanla)
return JSONResponse({"version": "v2", "data": [{"uuid": "abc", "item_name": "Item A", "price": 100}]})
else:
raise HTTPException(status_code=406, detail="Unsupported Accept header")
Bu yaklaşım, özellikle şirket içi servisler arası iletişimde veya mobil uygulamalar gibi sıkı kontrolün olduğu istemcilerde işime yaradı. Geliştiriciler, Accept header’ını doğru şekilde ayarladıklarında, API’nin hangi versiyonunu kullandıkları belli oluyordu. Ancak, keşfedilebilirlik açısından URL Path’e göre daha düşüktür; API’yi ilk kez kullanacak birisi için header’ı doğru ayarlamak biraz daha fazla dokümantasyon okumayı gerektirebilir. Ayrıca, bazı client kütüphaneleri veya araçları bu tür custom header’ları veya Content Negotiation’ı otomatik olarak desteklemeyebilir, bu da ek geliştirme maliyeti demektir.
Projelerimde Versiyonlama Stratejisi Seçimlerim
API versiyonlama stratejisi seçimi, projenin doğasına ve hedef kitlesine göre değişir. Benim deneyimimde, “tek bir doğru yol” diye bir şey yok; her zaman trade-off’lar söz konusu. Halka açık bir yan üründe aldığım kararlarla, sıkı kontrollü bir kurumsal iç platformda aldığım kararlar birbirinden farklı olabiliyor.
Örneğin, kendi finansal hesaplayıcı yan ürünümün halka açık API’si için URL Path versiyonlamasını tercih ettim. Sebebi basitti: dış geliştiricilerin API’yi kolayca kullanabilmesi ve hızlıca entegre olabilmesi. Karmaşık bir header yapısı yerine, /v1/calculate gibi basit bir URL, onboarding sürecini hızlandırıyor. Şu anda v1 ve v2 versiyonlarını canlıda tutuyorum ve v1’i kullanan eski istemcilerin sorunsuz çalışmaya devam ettiğinden emin oluyorum. Ancak bu durum, backend’de her iki versiyonun da kodunu maintain etmek gibi bir yük getiriyor.
Diğer yandan, sıkı kontrollü bir kurumsal iç platform için geliştirilen servislerde Accept header’ı üzerinden versiyonlamak daha mantıklı olabiliyor. Bu tür platformlarda istemciler genellikle kurumsal uygulamalar olduğu için, geliştiricilerin HTTP header’larını doğru yönetme yeteneği daha yüksek olur. Ayrıca URL’lerin temiz kalması, güvenlik ve denetim ekipleri için daha düzenli bir görünüm sağlar. Burada “URL’lerin net ve değişmez kalması, güvenlik duvarı kurallarını ve izleme konfigürasyonlarını basitleştirir” düşüncesi etkili oluyor. Özellikle bir servisin çok sayıda endpoint’i varsa, /v1/users, /v2/users, /v3/users şeklinde gitmektense Accept: application/vnd.myapi.users.v3+json daha zarif bir çözüm olabiliyor.
Bu seçimlerimde, her zaman “ileride ne kadar esnek olmam gerekecek?” ve “bu yaklaşımın operasyonel maliyeti ne olacak?” sorularını kendime sordum. Bazen basitlik adına teknik borçları göze aldım, bazen de uzun vadeli sürdürülebilirlik için biraz daha karmaşık bir başlangıç yaptım. Önemli olan, bu trade-off’ların farkında olmak ve bilinçli kararlar vermektir.
Versiyon Geçişlerinde Sıfır Downtime ve Deprecation Yönetimi
API versiyonlamanın en zorlu kısımlarından biri, eski versiyonlardan yeni versiyonlara sorunsuz geçişi sağlamaktır. “Sıfır downtime” hedefiyle çalışırken, hem eski hem de yeni versiyonların bir süre aynı anda canlıda olması gerekir. Bu, genellikle “graceful deprecation” denen bir süreçle yönetilir.
Bir uretim firmasının ERP’sinde, v1 API’den v2’ye geçiş yaparken, eski operatör ekranlarının v1’i kullanmaya devam ettiğini, yeni geliştirilen mobil uygulamaların ise v2’yi kullanmaya başladığını gördüm. Bu geçiş sürecini yönetmek için uzunca bir deprecation periyodu belirledik. Bu süre zarfında, v1 API’sine yapılan çağrıları izleyerek, hangi istemcilerin hala eski versiyonu kullandığını tespit ettik. v1 endpoint’lerine bir uyarı header’ı (X-API-Deprecated: true; Deprecation-Date: 2026-12-31) ekleyerek istemcilere bildirimde bulundum.
HTTP/1.1 200 OK
Content-Type: application/json
X-API-Deprecated: true; Deprecation-Date: 2026-12-31
Link: <https://api.example.com/v2/docs>; rel="sunset"; type="text/html"
{
"message": "Bu API versiyonu (v1) 31 Aralık 2026 tarihinde kullanımdan kaldırılacaktır. Lütfen v2 versiyonuna geçiş yapınız."
// ... v1 response data
}
Bu tür bir deprecation bildirimi, istemci tarafındaki geliştiricilere yeterli zaman tanır ve geçişi planlamalarına yardımcı olur. Deprecation periyodu boyunca, v1 endpoint’lerine gelen request sayısının zamanla azaldığını görmek, v1’i güvenle kapatabileceğimizi gösteren önemli bir metrikti. Eğer bu izlemeyi yapmasaydım, belki de v1’i kapatıp hala onu kullanan bir müşteriyi zor durumda bırakabilirdim. observability stratejileri üzerine daha önce yazmıştım, bu da onun bir parçası.
Hata yapmaktan da çekinmedim. Bir keresinde, v1’i kapatmadan önce son kontrolleri yaparken, “nasıl olsa kimse kullanmıyor” düşüncesiyle apar topar kapatma kararı aldım. Ancak, gözden kaçan bir raporlama aracı hala v1’i kullanıyormuş ve o aracın raporları bir anda “boş” gelmeye başladı. Bu, bana “asla varsayımda bulunma, her zaman veriye bak” dersini verdi. Bu tür durumlarda, rollback mekanizmalarının hazır olması hayat kurtarır. CI/CD reliability üzerine yazım da bu konulara değinir.
API Versiyonlamanın Geliştirme ve Operasyon Maliyeti
API versiyonlama, beraberinde ciddi geliştirme ve operasyonel maliyetler getirir. Bu maliyetleri göz önünde bulundurmadan bir strateji belirlemek, ileride büyük sorunlara yol açabilir. Benim deneyimimde, bu maliyetler genellikle üç ana başlık altında toplanır: dokümantasyon, test ve deployment.
Dokümantasyon: Her yeni API versiyonu, güncellenmiş veya yeni bir dokümantasyon seti gerektirir. Swagger/OpenAPI gibi araçlar bu süreci kolaylaştırsa da, iki veya daha fazla aktif versiyon için ayrı ayrı dokümantasyon tutmak, sürekli bir çaba ister. Özellikle v1’deki bir alanın v2’de değiştiği veya kaldırıldığı durumlarda, dokümantasyonun net ve güncel olması, istemci geliştiricilerin kafasını karıştırmamak için hayati önem taşır. Kendi yan ürünümün API dokümantasyonunu güncel tutmak bile başlı başına düzenli bir çaba istiyor.
Test: Her yeni versiyon, mevcut test setinin genişletilmesi anlamına gelir. v1 için yazdığınız testler, v2 için de geçerli olmayabilir veya v2’ye özel yeni senaryolar eklemeniz gerekebilir. Bir ERP projesinde, v1 ve v2 API’lerini aynı anda test etmek için CI/CD pipeline’ımızı güncelledik. Bu, test süresini gözle görülür şekilde uzattı ama regresyonları yakalamak için gerekliydi. Özellikle otomatik testlerde, her versiyon için ayrı entegrasyon testleri yazmak zorundaydım.
# Ornek CI/CD test adimi (pseudo-code)
# Gitlab CI/CD veya Github Actions'ta benzer adimlar kullanilabilir
test_api_versions:
stage: test
script:
- echo "Running tests for API v1"
- docker-compose -f docker-compose.v1.yml up -d
- pytest tests/v1/
- docker-compose -f docker-compose.v1.yml down
- echo "Running tests for API v2"
- docker-compose -f docker-compose.v2.yml up -d
- pytest tests/v2/
- docker-compose -f docker-compose.v2.yml down
Deployment ve Operasyon: Birden fazla API versiyonunu canlıda tutmak, deployment stratejilerini karmaşıklaştırır. Farklı versiyonların farklı kod tabanlarında olması (örn. ayrı Git branch’leri) veya aynı kod tabanında conditional logic içermesi, bakım yükünü artırır. Bir müşteri projesinde, iki farklı API versiyonunu farklı Docker container’larında çalıştırıp, Nginx ile trafiği yönlendirdim. Bu, her versiyon için ayrı kaynak (CPU, RAM) ayrılması anlamına geliyordu, bu da operasyonel maliyeti artırdı. Nginx reverse proxy yapılandırmaları ile ilgili deneyimlerimi daha önce paylaşmıştım.
Bu maliyetleri düşürmek için, gereksiz versiyonlamadan kaçınmak ve breaking change olmayan değişiklikleri (örn. yeni alan ekleme) mevcut versiyona dahil etmek önemlidir. Ayrıca, uzun süreli deprecation periyotları belirleyerek istemcileri yeni versiyonlara geçmeye teşvik etmek, eski versiyonların bakım yükünü azaltmaya yardımcı olur.
Sonuç: Versiyonlama Bir Stratejidir, Bir Özellik Değil
API versiyonlama, teknik bir özellikten ziyade, bir ürün ve operasyonel stratejidir. Benim 20 yıllık saha tecrübemde öğrendiğim en önemli derslerden biri, bu konuyu “sonra hallederiz” diye ertelememektir. Bir API’yi canlıya almadan önce, versiyonlama stratejinizin net olması ve ekibinizin bu konuda hemfikir olması, ileride yaşanabilecek birçok baş ağrısının önüne geçer.
Doğru versiyonlama yaklaşımı, projenizin doğasına, hedef kitlenize ve operasyonel kapasitenize göre değişir. URL Path’in basitliği, Header tabanlı versiyonlamanın esnekliği veya Query Parameter’ın nadir kullanım alanları… Hepsini deneyimledim ve her birinin kendine göre artıları ve eksileri olduğunu gördüm. Önemli olan, bu trade-off’ların farkında olmak ve bilinçli bir karar vermektir.
Unutmayın, API’niz geliştikçe, istemcileriniz de gelişmek zorunda kalacaktır. Versiyonlama, bu geçişi mümkün olduğunca sorunsuz ve şeffaf hale getirmek için elimizdeki en güçlü araçlardan biridir. Benim net pozisyonum: Erken başlayın, izleyin, iletişim kurun ve asla varsayımda bulunmayın.