API tasarımında sürümleme (versioning) kararları
İstemcileri kırmadan bir API'yi nasıl evrimleştirirsiniz? Sürümleme stratejilerini trade-off'larıyla karşılaştırıyor, gerçek deneyimlerimi paylaşıyorum.
Bir API yayınladıktan sonra dünyaya “bu sözleşme değişmeyecek” demiş gibi oluyorsunuz. Değiştirirseniz, sizi kullanan istemciler kırılıyor. Değiştirmezseniz, tasarım hatalarıyla sonsuza dek yaşıyorsunuz. API versioning, bu gerilimi yönetmenin yolu — ama “doğru” bir yolu yok; trade-off’lardan birini seçmek var.
Neden sürümleme gerekli?
Bir kaynağın yanıt yapısını değiştirmeniz gerektiğini düşünelim. Kullanıcı objesi şu an şöyle dönüyor:
{
"id": 1,
"name": "Muhammet Şafak",
"created_at": "2019-01-15T10:00:00Z"
}
Yeni versiyonda name yerine first_name ve last_name ayrı alanlara bölünecek. Bunu tek sürümde değiştirirseniz, eski name alanını kullanan her istemci anında kırılır. Sürümleme, eski ve yeni yapının bir süre yan yana yaşamasına olanak tanır.
Üç yaygın strateji
URI sürümlemesi
GET /api/v1/users
GET /api/v2/users
En yaygın yaklaşım. İstemcinin isteğe bakarak hangi sürümde olduğunu anlamak çok kolay; log’larda, proxy kurallarında, dokümantasyonda net görünüyor. Dezavantajı: URL’leri çoğaltıyor ve “her kaynak için v1/v2 mi ayırmalıyım?” sorusunu doğuruyor.
Header sürümlemesi
GET /api/users
Accept: application/vnd.myapi.v2+json
veya özel bir header:
X-API-Version: 2
URL temiz kalıyor. Ama istemci bu header’ı eksik gönderirse ne olur? Varsayılan sürüm döner mi? Hata mı verilir? Bu belirsizlik, sözleşmenin bir parçası haline geliyor.
Query parametresi
GET /api/users?version=2
Pratik bir başlangıç noktası ama uzun vadede bakımı zor. Cache davranışı tahmin edilmesi zor; proxy’ler ve CDN’ler query parametresini görmeyebilir.
Ben ne tercih ediyorum?
Birkaç projede URI sürümlemesine yerleştim: /api/v1/, /api/v2/. Gerekçelerim net:
-
Okunabilirlik. Hangi sürümü çağırdığınız URL’de açıkça görünüyor. Hata ayıklarken log satırına bakıp anlamak kolay.
-
Dokümantasyon sınırı. Her sürümün kendi dokümantasyonu ayrı tutulabilir. “Bu endpoint v2’de nasıl davranıyor?” sorusunu yanıtlamak için sürümü bulmak gerekmez.
-
Dağıtım esnekliği. v1 ve v2’yi ayrı controller’lara ya da rota gruplarına yönlendirebilirsiniz. Kod tabanını iç içe geçirmek zorunda kalmıyorsunuz.
Header tabanlı sürümlemenin teorik zarafeti var ama pratikte istemcileri yanlış yapılandırılmış header ile karşılaştığımda sorunu bulmak daha uzun sürüyor.
Geriye dönük uyumlu değişiklikler
Her değişiklik sürüm gerektirmez. Backward-compatible değişiklikler mevcut sürümde yapılabilir:
- Yanıta yeni bir alan eklemek.
- İsteğe bağlı bir parametre eklemek.
- Hata mesajlarını zenginleştirmek.
Breaking change’ler ise sürüm gerektirir:
- Var olan bir alanı kaldırmak veya yeniden adlandırmak.
- Zorunlu bir parametre eklemek.
- Yanıt yapısını temel düzeyde değiştirmek.
- Durum kodunu değiştirmek.
Eski sürümleri ne zaman kaldırırsınız?
Bu sorunun yanıtı istemci tabanınıza göre değişiyor. İç kullanım için bir API’de v1’i bırakmak ve v2’ye geçişi hızlıca zorunlu kılmak mümkün. Dış istemcilere açık bir API’de en az 6-12 aylık bir deprecation süreci makul.
Deprecation sürecini iletmek için HTTP başlıklarını kullanabilirsiniz:
Deprecation: true
Sunset: Sat, 31 Jan 2021 23:59:59 GMT
Sunset header’ı, tarayıcılar veya istemci kütüphaneleri henüz standart olarak işlemese de, istemci geliştiricilere bildirim olarak anlamlı.
API sürümlemesinde mükemmel bir strateji yok. Kendi projelerimde URL tabanlı sürümlemeyi tercih ediyorum; çünkü hem istemci geliştirici için en görünür yöntem hem de bir isteğe bakıp hangi sürümde olduğunu anlamak kolay. Header tabanlı sürümleme daha zarif görünse de, hata ayıklarken görünmez kalması beni birkaç kez yordu. Hangi yöntemi seçerseniz seçin, önemli olan onu tutarlı uygulamak ve istemcilerinizle net iletişim kurmak — bir sürüm stratejisi ancak öngörülebilir olduğunda işe yarar.