Bir API’ı canlıya aldığımda veya mevcut bir API üzerinde yeni bir özellik geliştirmeye başladığımda, aklıma gelen ilk sorunlardan biri her zaman versiyonlama olur. Geliştirdiğim yazılımlarda veya bir müşteri projesinde, yeni bir özellik eklediğimde ya da mevcut bir field’ı değiştirdiğimde, eski versiyonu kullanan mobil uygulamaların veya entegre sistemlerin çalışmaya devam etmesi kritik önem taşır. Bu, sadece teknik bir tercih değil, aynı zamanda operasyonel bir zorunluluktur.
Bu yazıda, yıllar içinde edindiğim deneyimlerle REST ve GraphQL API’larında versiyonlama stratejilerini, karşılaştırmalı olarak ele alacağım. Her iki yaklaşımın da kendine has avantajları ve dezavantajları var. Hangi durumda hangi stratejiyi seçtiğimi, nedenleriyle birlikte anlatacağım.
API Versioning Neden Gerekli?
API’lar, yazılım dünyasının ana arterleri gibidir. Bir backend servisi, bir mobil uygulama veya başka bir mikroservis, bu API’lar üzerinden iletişim kurar. Ancak yazılım dünyası dinamiktir, gereksinimler değişir, yeni özellikler eklenir, bazen de eski özellikler tamamen kaldırılır. İşte tam da bu noktada API versiyonlama devreye giriyor.
Bir üretim ERP’sinde çalışırken, tedarik zinciri entegrasyonu için kullandığımız bir üçüncü parti API’ın aniden değiştiğini ve eski versiyonun desteğinin çekildiğini gördük. Bu durum, çok sayıda ürün hareketinin durmasına, sevkiyatların aksamasına ve ciddi operasyonel aksaklıklara yol açtı. Bu tarz durumların önüne geçmek için versiyonlama şart. Versiyonlama sayesinde, API’ın arayüzünde yapılan değişiklikler izole edilir ve farklı istemcilerin farklı versiyonları kullanmasına olanak tanınır. Böylece, yeni bir özellik yayınlandığında veya bir field’ın tipi değiştiğinde, eski istemciler kesintisiz çalışmaya devam ederken, yeni istemciler güncel versiyonu kullanabilir. Benim deneyimimde, bu geçiş sürecini yönetmek, çoğu zaman yeni özellik yazmaktan daha zorlayıcı olmuştur.
Versiyonlama yapmazsak ne olur? Bir gün users endpoint’inden dönen isActive alanını status olarak değiştirmeye karar verdiğimi düşünün. Eğer versiyonlama yoksa, bu değişiklik tüm mevcut istemcileri etkiler ve muhtemelen onların hata vermesine neden olur. Özellikle mobil uygulamalar gibi istemcilerin anında güncellenemediği senaryolarda bu durum tam bir kabus olabilir. Uygulama mağazalarındaki onay süreçleri, kullanıcıların güncellemeleri yükleme alışkanlıkları derken, aylar sürebilecek bir uyumsuzluk dönemi yaşayabiliriz. Bu yüzden, API tasarımına başlarken versiyonlama stratejisini belirlemek, ileride yaşanabilecek birçok baş ağrısının önüne geçer.
REST API Versioning Stratejileri ve Uygulamaları
REST API’lar için birçok farklı versiyonlama stratejisi mevcut. Her birinin kendine göre avantajları ve dezavantajları var. Bir projemde, bu stratejilerden birkaçını farklı bağlamlarda kullandım ve her birinin operasyonel etkilerini bizzat deneyimledim.
URI Versioning (Path Versioning)
Bu, benim en sık kullandığım ve en basit bulduğum versiyonlama yöntemidir. API versiyonunu doğrudan URI (Uniform Resource Identifier) içine dahil ederiz. Örneğin, /api/v1/users veya /api/v2/products. Bu yöntem, versiyonun açıkça görünür olmasını sağlar ve istemciler için anlaşılması kolaydır.
Avantajları:
- Kolay Anlaşılır: API’ın hangi versiyonunu kullandığınız URL’den hemen bellidir.
- Keşfedilebilirlik: Tarayıcıda veya cURL ile test ederken kolayca versiyon değiştirebilirsiniz.
- Cache Dostu: Her versiyonun kendi URL’si olduğu için, CDN’ler ve proxy’ler tarafından kolayca cache’lenebilir.
Dezavantajları:
- URI Bloat: Her yeni versiyonla URL’ler uzar ve tekrar eden
/vXkısımları oluşur. - Yönlendirme Karmaşası: Farklı versiyonları farklı backend servislerine yönlendirmek için bir API Gateway veya reverse proxy kullanıyorsanız, konfigürasyon dosyaları karmaşıklaşabilir.
Bir üretim firmasının ERP’sinde, dışarıya açtığımız tedarikçi entegrasyon API’larında bu yöntemi kullandım. Çünkü tedarikçilerin IT ekiplerinin API’ı anlaması ve entegre etmesi bizim için öncelikliydi. URI’daki versiyon numarası, onlara net bir yol haritası sunuyordu.
# Nginx ile URI Versioning Örneği
server {
listen 80;
server_name api.example.com;
location /api/v1/ {
proxy_pass http://backend_v1_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /api/v2/ {
proxy_pass http://backend_v2_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Bu Nginx konfigürasyonunda, /api/v1/ ile başlayan istekler backend_v1_service’e, /api/v2/ ile başlayanlar ise backend_v2_service’e yönlendirilir. Bu durum, farklı versiyonların farklı kod tabanlarında veya farklı Docker container’larında çalışmasına olanak tanır. Bir keresinde, /api/v1/ endpoint’i üzerinden gelen isteklerin büyük bölümünün zamanla kesildiğini gördük ve bu sayede backend_v1_service’i güvenle kapatabildik.
Query Parameter Versioning
Bu yöntemde, API versiyonu URI’daki bir sorgu parametresi olarak belirtilir: /users?version=1 veya /products?api-version=2. URI’yi temiz tutma açısından avantajlı görünse de, bazı durumlar için uygun olmayabilir.
Avantajları:
- Temiz URI’lar: Versiyon bilgisi URI’nin kendisini kirletmez.
- Esneklik: İstemciler, aynı URL üzerinden farklı versiyonları kolayca talep edebilir.
Dezavantajları:
- Cache Sorunları: Sorgu parametreleri, bazı cache mekanizmalarında ayrı bir kaynak olarak algılanmayabilir ve bu da hatalı cache davranışlarına yol açabilir.
- RESTful Değil: REST prensiplerine göre, URI’ler kaynağı benzersiz bir şekilde tanımlamalıdır. Sorgu parametreleri genellikle kaynağın bir özelliğini filtrelemek veya sıralamak için kullanılır, versiyonlama için değil.
- Görünürlük Az: URI Versioning kadar açık değildir.
Kendi yan ürünümün finansal hesaplayıcılarının backend’inde bu yöntemi kısa bir süre kullandım. Ancak, CDN cache’leri ile yaşadığım sorunlar ve log analizinde versiyon takibinin zorlaşması nedeniyle URI versioning’e geri döndüm. Özellikle api-version parametresi ile gelen isteklerin azımsanmayacak bir kısmının yanlış versiyonu talep ettiğini fark ettim ve bu durum, müşteri hizmetleri ekibimizin iş yükünü artırdı.
# FastAPI ile Query Parameter Versioning Örneği
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(version: int = Query(..., description="API Version")):
if version == 1:
return {"message": "Items from API v1"}
elif version == 2:
return {"message": "Items from API v2"}
return {"message": "Invalid API version"}
Bu Python örneğinde, version sorgu parametresi üzerinden gelen değere göre farklı yanıtlar dönülüyor. Bu, backend logic’ini versiyonlara göre dallandırmak için kullanılabilir. Ancak, aynı kod tabanında çok fazla versiyon mantığı birikmesi, teknik borcu artırabilir.
Header Versioning
Bu strateji, API versiyonunu HTTP başlıkları (Headers) aracılığıyla iletir. En yaygın kullanılan başlık X-Api-Version veya Accept başlığının özel bir formatı olabilir.
Avantajları:
- Temiz URI’lar: URI’ler tamamen versiyon bilgisinden arındırılmış olur.
- RESTful: HTTP standartlarına daha uygun kabul edilir, çünkü başlıklar meta veri taşımak için tasarlanmıştır.
Dezavantajları:
- Tarayıcı Testi Zor: Tarayıcı üzerinden doğrudan test etmek zordur, çünkü tarayıcılar özel başlıkları kolayca ayarlamanıza izin vermez. Genellikle cURL veya Postman gibi araçlar gerekir.
- Cache Sorunları:
Varybaşlığı doğru ayarlanmazsa cache sorunları yaşanabilir.
Bir bankanın iç platformunda, mikroservisler arası iletişimde bu yöntemi tercih ettim. Çünkü servisler arası iletişimde URL’lerin temiz kalması ve versiyon bilgisinin isteğin meta verisi olarak taşınması daha uygun geliyordu. Ayrıca, internal servislerin tarayıcı üzerinden direkt test edilme ihtiyacı da yoktu. Yoğun trafik alan bu platformda, X-Api-Version başlığı sayesinde, her bir servis kolayca kendi versiyonunu yönetebiliyordu.
# cURL ile Header Versioning Örneği
curl -H "X-Api-Version: 2" https://api.example.com/users
Bu cURL komutu, X-Api-Version başlığı ile API’ın ikinci versiyonunu talep eder. Backend, bu başlığı okuyarak isteği ilgili versiyona yönlendirir.
Content Negotiation (Accept Header)
Bu, RESTful API’lar için en “doğru” kabul edilen yaklaşımlardan biridir. Versiyon bilgisi, HTTP Accept başlığında, medya tipi içinde belirtilir. Örneğin, Accept: application/vnd.myapi.v1+json.
Avantajları:
- RESTful Standardlara Uygun: HTTP content negotiation mekanizmasını kullanır.
- Esneklik: İstemci, farklı versiyonların farklı medya tiplerini desteklemesine izin verebilir.
Dezavantajları:
- Karmaşık Medya Tipleri: Medya tipleri karmaşıklaşabilir ve okunabilirliği azalabilir.
- Uygulama Zorluğu: Backend tarafında bu medya tiplerini doğru bir şekilde ayrıştırmak ve yönlendirmek, diğer yöntemlere göre daha fazla çaba gerektirebilir.
Bu yöntemi daha çok, çok sayıda farklı istemci ve veri formatını desteklemesi gereken açık kaynak API’lar için düşündüm. Ancak, kendi projelerimde veya kurumsal yazılımlarda, uygulamanın getirdiği karmaşıklık nedeniyle daha basit yöntemleri tercih ettim. Zira backend tarafında, gelen Accept header’ını ayrıştırıp doğru Content-Type ile yanıt vermek, özellikle Python/FastAPI tarafında ek bir custom middleware veya decorator gerektiriyordu ve bu, geliştirme hızımızı gözle görülür biçimde yavaşlatıyordu.
# HTTP Request ile Content Negotiation Örneği
GET /users HTTP/1.1
Host: api.example.com
Accept: application/vnd.myapi.v2+json
Bu istek, application/vnd.myapi.v2+json medya tipini kabul ettiğini belirtir ve API, bu medya tipine uygun, versiyon 2’ye ait bir yanıt döner.
GraphQL’de Versioning: Farklı Bir Yaklaşım
GraphQL, REST’ten tamamen farklı bir felsefeye sahip olduğu için, versiyonlama yaklaşımı da doğal olarak farklıdır. GraphQL’in temel gücü, istemcilerin tam olarak ihtiyaç duydukları veriyi tek bir istekte alabilmeleridir. Bu durum, geleneksel REST versiyonlama stratejilerini büyük ölçüde gereksiz kılar.
GraphQL dünyasında “no-versioning” mantra’sı oldukça yaygındır. Bunun yerine, “schema evolution” denilen bir yaklaşımla, API’ın zaman içinde nasıl değiştiği yönetilir. Yani, bir GraphQL API’ı genellikle tek bir versiyon olarak kabul edilir ve istemciler, schema’daki değişikliklere uyum sağlamak için sorgularını kendileri günceller.
Peki, bu nasıl oluyor?
- Alan Ekleme: Schema’ya yeni alanlar veya tipler eklemek, mevcut istemcileri etkilemez, çünkü onlar bu yeni alanları talep etmezler.
- Alan Kaldırma (Deprecation): Bir alanın artık kullanılmaması gerektiğinde, schema’da
@deprecateddirektifi ile işaretlenir. Bu, geliştiricilere o alanın gelecekte kaldırılacağını bildirir. İstemciler bu uyarıyı görür ve zamanla sorgularını güncelleyebilirler. - Alan Yeniden Adlandırma: Bir alanı yeniden adlandırmak gerektiğinde, hem eski hem de yeni adıyla bir süre schema’da tutulur ve eski alan
@deprecatedolarak işaretlenir.
Kendi Android spam uygulamamın backend’inde GraphQL kullandım. Buradaki istemci sayısı çok fazlaydı ve her bir istemcinin yeni bir versiyonu indirmesini beklemek çok zordu. GraphQL’in esnekliği sayesinde, yeni özellikler eklerken veya mevcut alanları iyileştirirken, eski istemcilerin sorunsuz çalışmaya devam etmesini sağladım. Örneğin, bir phoneNumber alanını String’den PhoneNumberObject tipine dönüştürdüğümde, eski istemciler hala phoneNumber alanını String olarak beklerken, yeni istemciler yeni objeyi kullanabiliyordu. Bu geçiş süreci boyunca hem eski hem de yeni schema sorunsuz bir şekilde bir arada çalıştı.
# GraphQL Schema Deprecation Örneği
type User {
id: ID!
name: String!
email: String @deprecated(reason: "Use 'contactInfo.email' instead")
contactInfo: ContactInfo # Yeni eklenen alan
}
type ContactInfo {
email: String
phone: String
}
Bu GraphQL schema’sında email alanı @deprecated olarak işaretlenmiş. Bu, istemci tarafındaki geliştirme ortamlarında bir uyarı olarak görünür ve geliştiricilere yeni contactInfo.email alanını kullanmaları gerektiğini bildirir. Bu sayede, eski istemciler hala email alanını kullanmaya devam edebilirken, yeni geliştirilen istemciler güncel schema’ya uygun olarak contactInfo.email’i kullanmaya başlar. Bu geçiş sürecini izlemek için, GraphQL sunucumda deprecate edilmiş alanların ne sıklıkla talep edildiğini gösteren özel bir metrik tutuyorum. Zamanla, email alanına gelen isteklerin büyük çoğunluğunun yeni contactInfo.email’e geçtiğini gözlemledim.
REST ve GraphQL Versioning Yaklaşımlarının Karşılaştırılması
REST ve GraphQL’in versiyonlama yaklaşımları, felsefeleri gereği birbirinden oldukça farklıdır. Hangi yaklaşımın projenize daha uygun olduğunu belirlerken, her ikisinin de temel özelliklerini ve operasyonel etkilerini anlamak önemlidir. Yıllar içindeki deneyimlerim, bu karşılaştırmayı yaparken bana sağlam bir temel oluşturdu.
| Özellik | REST API Versioning | GraphQL API Versioning |
|---|---|---|
| Temel Mekanizma | URI, Query Parametreleri, HTTP Başlıkları, Content Negotiation | Schema Evolution (alan ekleme, deprecate etme) |
| İstemci Etkileşimi | İstemciler belirli bir API versiyonunu talep eder. Yeni versiyonlar genellikle breaking changes içerir. | İstemciler mevcut schema’dan istedikleri veriyi talep eder. Geriye dönük uyumluluk genellikle korunur. |
| Dağıtım Karmaşası | Farklı versiyonlar için farklı endpoint’ler veya backend servisleri gerekebilir. Dağıtım ve routing karmaşıktır. | Genellikle tek bir GraphQL endpoint’i vardır. Schema değişiklikleri tek bir yerde yönetilir. Daha az dağıtım karmaşası. |
| Geriye Dönük Uyumluluk | Sınırlıdır. Genellikle yeni versiyonlar eski istemcileri bozabilir. | Yüksek düzeyde geri dönük uyumluluk. Alan eklemek veya deprecate etmek mevcut istemcileri bozmaz. |
| İleriye Dönük Uyumluluk | Zordur. Yeni bir istemci, eski bir API versiyonunu kullanamayabilir. | Kolaydır. Yeni bir istemci, eski bir schema’yı kullanarak da veri talep edebilir. |
| Dokümantasyon | Her versiyon için ayrı dokümantasyon gerekir. | Otomatik schema introspection sayesinde tek bir güncel dokümantasyon. |
| Öğrenme Eğrisi | Daha düşük (genel HTTP bilgisi yeterli). | Daha yüksek (GraphQL query dili ve schema yapısı öğrenilmeli). |
Trade-off Analizi:
- REST’in Gücü: Eğer API’ınız dış dünyaya, farklı ve bağımsız ekiplerin geliştirdiği çok sayıda istemciye hizmet veriyorsa, REST’in URI tabanlı versiyonlama stratejileri, istemciler için net bir sözleşme sunar. Bir bankanın dışa açık API’ları gibi, farklı partnerlerin entegrasyonlarını yönetirken,
/v1,/v2gibi açık versiyon numaraları, iletişimi ve yönetimi kolaylaştırır. Bir keresinde, yeni bir ödeme sistemi entegrasyonu için/api/v3/paymentsendpoint’ini devreye aldığımızda, eski/api/v2/paymentsendpoint’ini kullanan çok sayıda partnerimiz vardı. Bu net ayrım sayesinde, yeni entegrasyonu sorunsuz bir şekilde yaparken eski partnerlerimizin sistemlerinin çalışmaya devam ettiğini garantiledik. - GraphQL’in Gücü: Eğer API’ınız daha çok kendi mobil veya web uygulamalarınız gibi kontrollü istemciler tarafından kullanılıyorsa, veya hızla değişen bir UI’a hizmet veriyorsa, GraphQL’in esnek schema yaklaşımı büyük avantaj sağlar. Tek bir endpoint üzerinden, istemcilerin ihtiyaç duyduğu veriyi dinamik olarak seçebilmesi, özellikle hızlı ürün geliştirme döngülerinde ve A/B testlerinde bize çok zaman kazandırdı. Kendi yan ürünümün mobil uygulamasında, bir UI değişikliği için yeni bir veri setine ihtiyacım olduğunda, REST API’da yeni bir endpoint veya versiyon açmam gerekirdi. GraphQL’de ise sadece sorguyu güncelleyerek, mevcut API üzerinden anında yeni veriyi çekebildim. Bu, yeni UI özelliklerini belirgin biçimde daha hızlı devreye almamızı sağladı.
Her iki yaklaşımın da kendine özgü kullanım durumları var. Önemli olan, projenizin ihtiyaçlarını, ekibinizin yetkinliklerini ve API’ın yaşam döngüsünü göz önünde bulundurarak doğru seçimi yapmaktır.
Benim Deneyimlerim ve Önerilerim
Yirmi yılı aşkın süredir sistemler ve yazılımlarla haşır neşir olurken, API versiyonlama konusu her zaman masamda önemli bir yer tutmuştur. Bazen doğru stratejiyi seçmek, bazen de yanlış seçimin sonuçlarıyla uğraşmak durumunda kaldım. Bu süreçte edindiğim bazı somut tecrübeler ve öneriler var.
1. Erken Planlama ve Dokümantasyon
API tasarımına başlarken versiyonlama stratejisini belirlemek, ileride yaşanacak sorunların önüne geçer. Hangi yöntemi seçeceğinize karar verdikten sonra, bu stratejiyi çok net bir şekilde dokümante edin. Dokümantasyon sadece endpoint’leri değil, aynı zamanda versiyonlama kurallarını, deprecation politikalarını ve breaking change yönetimi süreçlerini de içermeli. Bir müşteri projesinde, API dokümantasyonunu yeterince detaylandırmadığımız için, farklı geliştirici ekipleri farklı versiyonları yanlış yorumladı ve bu durum, günlerce süren entegrasyon sorunlarına yol açtı. Bu süreçte, ekibimizin ciddi bir zamanını sadece bu sorunları gidermek için harcadığını gördüm.
2. İstemcilerle İletişim
Eğer API’ınız dış istemciler tarafından kullanılıyorsa, değişiklikleri ve yeni versiyonları proaktif bir şekilde iletin. E-posta listeleri, blog yazıları veya bir geliştirici portalı üzerinden duyurular yapın. Deprecate edilecek bir versiyon için net bir zaman çizelgesi belirleyin ve buna sadık kalın. Bir üretim ERP’sinde, dışarıya açtığımız API’larda, bir versiyonun ömrünü baştan net bir süre olarak belirledik ve bu sürenin sonunda eski versiyonu kapatacağımızı önceden bildirdik. Bu sayede, istemcilerin büyük çoğunluğu zamanında yeni versiyona geçiş yapabildi. Geri kalan az sayıda istemci için ise özel geçiş planları oluşturmak zorunda kaldık.
3. İzleme ve Analiz
Hangi API versiyonlarının ne kadar kullanıldığını sürekli olarak izleyin. Log’larınızı ve metriklerinizi kullanarak eski versiyonların kullanım oranlarını takip edin. Bu veriler, eski bir versiyonu ne zaman güvenle kapatabileceğinize karar vermenizde size yardımcı olur. Kendi yan ürünümün backend’inde, her API isteği için versiyon bilgisini logluyorum. /v1/data endpoint’ine gelen isteklerin zamanla ihmal edilebilir bir seviyeye düştüğünü gördüğümde, bu versiyonu güvenle kaldırma kararı aldım. Bu, sunucu kaynaklarından tasarruf etmemizi sağladı.
# Nginx Access Log'undan API Versiyonu İzleme Örneği
# Log formatında X-Api-Version başlığını yakalamak için
log_format combined_version '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent" '
'"$http_x_api_version"'; # X-Api-Version başlığı
access_log /var/log/nginx/access.log combined_version;
# Log'ları parse ederek versiyon kullanımını bulma
# Örnek: grep '"X-Api-Version: 2"' /var/log/nginx/access.log | wc -l
Bu Nginx log formatı ve komut örneği, X-Api-Version başlığını loglara dahil ederek, hangi versiyonun ne kadar kullanıldığını basit bir şekilde takip etmemizi sağlar.
4. Geriye Dönük Uyumluluk (Backward Compatibility)
Breaking changes yapmaktan mümkün olduğunca kaçının. Eğer bir değişiklik yapmak zorundaysanız, eski ve yeni versiyonun bir süre paralel çalışmasına izin verin. Bu, istemcilerin yeni versiyona geçiş yapması için zaman tanır. Bir mobil uygulamamın backend’inde, bir User objesindeki address alanını String’den AddressObject’e çevirmem gerektiğinde, /v1/users hala address’i String olarak dönerken, /v2/users yeni AddressObject’i dönüyordu. Bu çiftli yapı bir süre devam etti ve geçişi sorunsuz hale getirdi.
5. API Gateway Kullanımı
Özellikle REST API’lar için, bir API Gateway kullanmak versiyonlama yönetimini büyük ölçüde basitleştirebilir. Gateway, gelen isteğin versiyonuna göre farklı backend servislerine yönlendirme yapabilir veya versiyon bilgisini başlıklar aracılığıyla backend’e iletebilir. Bu, backend servislerinin daha temiz kalmasını sağlar. Daha önce API Gateway ile mikroservis mimarisi deneyimim yazımda bu konuya değinmiştim.
Sonuç
API versiyonlama, yazılım mimarisinin önemli bir parçasıdır ve doğru stratejiyi seçmek, uygulamanızın uzun ömürlü ve sürdürülebilir olmasını sağlar. URI, header ya da gateway tabanlı yaklaşımlardan hangisini seçerseniz seçin; mevcut istemcileri kırmadan evrim sağlamak için breaking change disiplini ve sürüm geçişi için açık bir takvim kritik. Pratikte en sağlam yol, küçük adımlarla yeni sürümü çıkarmak, eski sürümü belirli bir süre paralel tutmak ve veriye dayalı olarak deprecation tarihini ilan etmek oldu.