API'nizi değiştirdiğinizde entegrasyonlar neden kırılıyor? Sürümleme ve geriye dönük uyumluluk
API'ler sürüm numarası kötü seçildiği için kırılmaz. Üç şey eksik olduğu için kırılır: neyin kırıcı değişiklik (breaking change) sayıldığının yazılı bir tanımı, hangi istemcinin hangi sürümü hâlâ çağırdığını gösteren bir veri, ve ilan edilmiş bir kapanış tarihi. Bu üçü varsa sürüm numarasını nereye koyduğunuz teknik bir tercih olarak kalır. Yoksa her değişiklikte aynı sahne tekrar eder: kimse itiraz etmediği için değişiklik yayına alınır, iki hafta sonra bir müşterinin gece çalışan entegrasyonu sessizce durur ve durumu siz değil o müşteri fark eder.
Neyin "kırıcı" olduğunu yazmadıysanız tartışma her sürümde baştan başlar
Bu konuda en somut yazılı kaynak Google'ın API tasarım önerileri arasındaki AIP-180. Kural listesi net: mevcut bileşenler (arayüz, metot, mesaj, alan, enum ve enum değeri) kaldırılamaz, yeniden adlandırma "kaldır ve ekle" sayıldığı için o da yasaktır, alan tipi tel üzerinde uyumlu olsa bile değiştirilemez, mevcut bir isteğe zorunlu alan eklenemez, bir alanın varsayılan değeri değiştirilemez, mevcut bir alanın değerinin üretildiği format veya algoritma değiştirilemez, kaynak isimleri değiştirilemez. Daha ince bir madde de var: bir metin alanının üst uzunluk sınırını yükseltmek bile uyumsuz değişiklik sayılır, çünkü istemciler kolonlarını eski sınıra göre boyutlandırmıştır. Hepsinin arkasındaki tek ölçüt şu: eski istemci yeni sunucuya karşı çalışmaya devam etmek zorundadır.
Pratikte çoğu ekibin AIP-180'i olduğu gibi benimsemesi gerekmez. Gereken şey tek sayfalık bir belge: "şu değişiklikleri her an yayınlayabiliriz, şunlar yeni sürüm gerektirir" listesi ve kararı kimin verdiğinin adı. Bu belge yoksa "bu kırıcı mı" sorusu her seferinde bir pull request yorumunda, o an en kendinden emin olan kişi tarafından cevaplanır.
Sağlayıcının kırıcı saymadığı değişiklik sizi yine kırabilir
Stripe geriye dönük uyumlu saydığı değişiklikleri açıkça yayınlıyor: yeni API kaynağı eklemek, mevcut metotlara yeni opsiyonel parametre eklemek, mevcut cevaplara yeni alan eklemek, cevaptaki alanların sırasını değiştirmek, nesne kimlikleri ve hata mesajları gibi anlamı dışa kapalı metinlerin uzunluğunu veya formatını değiştirmek (kimliklerdeki ch_ gibi sabit önekleri eklemek ya da kaldırmak dahil) ve yeni olay tipi eklemek. Yani sözleşmeye göre bunların hepsi haber verilmeden gelebilir.
Bu listedeki iki madde dikkatsiz istemciyi doğrudan kırar. Stripe nesne kimliklerinin 255 karaktere kadar çıkabileceğini söylüyor ve kolonu VARCHAR(255) tanımlamanızı öneriyor; kolonu bugünkü kimlikler sığdığı için VARCHAR(32) yapan entegrasyon, uzun bir kimliğin geldiği gün kırılır. Yeni olay tipi de default dalında istisna fırlatan her webhook işleyicisini düşürür, ki Stripe bunu da yazıyor: dinleyicinizin tanımadığı olay tiplerini sorunsuz karşılamasını sağlayın.
Buradan iki taraflı bir kural çıkıyor. Tüketici tarafında toleranslı okuyucu olun: tanımadığınız alanları yok sayın, tanımadığınız enum değerinde patlamayın, sağlayıcının hiç taahhüt etmediği bir uzunluğu doğrulamayın. Sağlayıcı tarafında ise aynı listeyi siz yayınlayın, ki tüketicileriniz neye tolerans göstermeleri gerektiğini bilsin.
Sürüm numarasını nereye koymalı
Üç yaygın yer var: yol (/v1/orders), özel bir başlık ya da Accept medya tipi, ve sorgu parametresi. Genele açık API'ler için pratik varsayılan yoldur. Her sürümün kendi adresi olduğu için önbellekleme sorunsuz çalışır, sürüm ağ geçidi loglarında ve CDN kurallarında görünür, yönlendirmesi kolaydır. Başlık ve medya tipi yaklaşımı URL'leri sabit tutar ve REST'in özüne daha yakındır, ama sürüm yalnızca URL'e bakan her araçtan kaybolur.
Seçimden daha önemli olan kısım şu: hangisini seçerseniz seçin, sürüm erişim loglarınıza ve metriklerinize bir boyut olarak düşmek zorunda. Trafiğini sayamadığınız bir sürümü emekliye ayıramazsınız. Ağ geçidiniz yolu logluyor ama sürüm kimsenin kaydetmediği bir başlıkta duruyorsa, kapanış gününe bir sayıyla değil bir tahminle gidersiniz.
Semantik sürüm tek model değil: tarih ve sürüm treni
GitHub'ın REST API'si tarih bazlı sürüm kullanıyor. Tarih bazlı sürümlemeye geçtikten sonraki ilk sürüm 2022-11-28 ve politika şu: yeni bir sürüm çıktığında öncekini en az 24 ay daha desteklerler.
Stripe ikisini birleştiriyor, tarih artı bir kod adı: 2025-03-31.basil, güncel sürüm ise 2026-07-29.dahlia. İşin ilginç kısmı sürüm treni. Aylık sürümler yalnızca geriye dönük uyumlu değişiklik içeriyor, kırıcı değişiklikler ise sadece yılda kabaca iki kez çıkan ana sürümlerde (Acacia, Basil, Clover, Dahlia) geliyor. Hesabınız bir sürüme sabitleniyor, yükseltmeyi kendiniz seçiyorsunuz ve yükseltmeyi geri almak için 72 saatlik bir pencereniz oluyor. Çok daha küçük bir API'ye taşınabilecek iki ders var burada: kırıcı değişiklikleri yıla dağıtmak yerine takvime bağlayın, ve tüketiciye hem önceden deneme hem geri dönme yolu bırakın.
En iyi sürümleme, yeni sürüm çıkarmamaktır
Yayınladığınız her sürüm, artık bakımını yaptığınız bir daldır. v2'yi açmadan önce değişikliğin eklemeli kümeye sığıp sığmadığına bakın: yeni bir opsiyonel parametre, cevapta yeni bir alan, eskisinin yanına yeni bir uç nokta. Bunu yıllarca sürdürülebilir kılan iki kural var. Birincisi, mevcut bir alanın anlamını asla yeniden kullanmayın. "Artık total alanına net tutarı yazıyoruz" en kötü türden kırıcı değişikliktir, çünkü tip aynı kalır, hiçbir hata oluşmaz, sadece rakam yanlışlaşır. İkincisi, girişteki doğrulamayı sonradan sıkılaştırmayın; dün kabul ettiğiniz bir yükü bugün reddetmek, sürüm numarası değişmese de kırıcı bir değişikliktir.
Değişiklik kaçınılmaz görünüyorsa genişlet-daralt (expand-contract) deseni onu uyumlu adımlara böler: yeni alanı ekleyin, bir süre ikisine de yazın, okuyucuları taşıyın, tüketiciler bittikten sonra eskisini kaldırın. Bu, veritabanı şema değişikliklerinin kesintisiz yayına alınmasını sağlayan aynı numaradır; ayrıntısını sıfır kesintili yayına alma yazımızda anlattık.
"Kırıcı" bir görüş değil, bir derleme hatası olmalı
Depoda bir OpenAPI tanımınız varsa bu kontrol otomatikleşir. oasdiff iki tanımı karşılaştırıp tüketiciyi kıracak değişiklikleri işaretliyor ve GitHub Actions eklentisi bunları pull request'in değişen satırlarına doğrudan not düşüyor. gRPC ve protobuf tarafında buf breaking aynı işi önceki bir Git etiketine veya kayıt defterindeki modüle karşı yapıyor; kural setleri tel uyumluluğundan üretilen kaynak kodu uyumluluğuna kadar farklı sıkılıkta.
Buradaki değer araçta değil, kararın yerini değiştirmesinde. Kontrol derlemeyi düşürdüğünde "bu kırıcı mı" sorusu tartışma olmaktan çıkıp ortak ve kayıtlı bir cevaba dönüşür. Üstüne sözleşme testleri (contract tests) ekleyin, böylece tüketicinin beklentisi sağlayıcının hattında koşar. Entegrasyon ağırlıklı ürünlerde test piramidinin en çok getiri veren katmanı genelde burasıdır ve çalışan bir CI hattınız varsa kurulumu birkaç satır.
Kaç ay destekleyeceğinizi önceden yazın
Kubernetes'in yazılı kuralı şu: GA seviyesindeki API'ler duyurulan emeklilikten sonra en az 12 ay veya 3 sürüm (hangisi uzunsa) desteklenir, beta API'ler 9 ay veya 3 sürüm, alfa için süre yok. GitHub 24 ay diyor. Sayılar farklı ve olması gereken de bu; önemli olan sayının ilk emeklilik duyurusundan önce var olması, bir müşteri masaya vurduktan sonra belirlenmemesi.
Zalando'nun açık kaynak REST API kılavuzu bunu bir sürece bağlıyor. 185 numaralı kural API'yi kapatmadan önce istemcilerin onayını almanızı, 186 dış partnerlerden emeklilik süresi konusunda mutabakat toplamanızı, 188 emekliye ayrılacak API'nin kullanımını izlemenizi zorunlu tutuyor. Tüketicileriniz adı bilinen birkaç entegratörse "onay" gerçekçi ve ucuz bir adımdır. Anonim genel istemcilerse onayın yerini daha uzun bir pencere ve telemetri alır.
Emeklilik bir duyuru e-postası değil, bir protokol
Mart 2025'ten beri bunun standartlaşmış bir cevabı var. RFC 9745 Deprecation yanıt başlığını tanımlıyor. Değer, Unix zamanı biçiminde yapılandırılmış bir tarih, yani telde Deprecation: @1688169599 gibi görünüyor. Aynı RFC bir deprecation bağlantı tipi de kaydediyor, böylece cevabın içinden geçiş dokümanını gösterebiliyorsunuz. Bu başlık, daha eski olan RFC 8594'ün Sunset başlığıyla birlikte çalışıyor; o ise HTTP tarihi kullanıyor: Sunset: Thu, 31 Dec 2026 23:59:59 GMT. İkisi beraber pencerenin başını ve sonunu işaretliyor ve kapanış tarihi emeklilik tarihinden önce olamıyor.
Yanlarına changelog ve geçiş rehberini gösteren bir Link başlığı koyun. Bir de RFC 9745'in kendi uyarısını atlamayın, alışılmadık biçimde dürüst: bu bilgi yalnızca bir ipucudur, güvenilecek bir şey değildir ve istemciler onu hiç okumadan da çalışabilecek şekilde yazılmalıdır. Başlıklar, izleyen tüketiciler için emekliliği makine tarafından okunabilir kılar. İzlemeyenlere hiçbir şey haber vermez. Sonraki iki adım tam bunun için.
v1'i kim çağırıyor? Bilmiyorsanız kapatamazsınız
Her istek sürümü ve çağıranın kimliğini (API anahtarı, istemci kimliği, kiracı) loglamalı. Bunu haftalık bir sayıya dönüştürün: sürüm başına ve tüketici başına çağrı adedi. O andan sonra emeklilik bir genel duyuru olmaktan çıkar, yanında rakam yazan bir isim listesine dönüşür. Kimin gerçekten etkileneceğini bilmek, hem duyurunun tonunu hem takvimi değiştirir.
Sonra gerçekten işe yarayan araç geliyor: brownout. Emekliye ayrılan uç noktayı önceden ilan edilmiş, sınırlı bir süre boyunca hizmet dışı bırakırsınız. Başlıkları görmezden gelen tüketici, düzeltmek için hâlâ zaman varken görünür ve geri dönüşü olan bir arıza yaşar. GitHub, sorgu parametresiyle API kimlik doğrulamasını ve OAuth Applications API'sini kaldırmadan önce bunu kademeli olarak yaptı: Mayıs 2021'de 12 saat, Haziran'da 24 saat, Ağustos 2021'de son kez 48 saat. Açıkladıkları amaç da tam buydu, tüketicilerin sistemlerinde alarm ürettirmek ve taşınmamış çağrıları ortaya çıkarmak. Sessizlik onay değildir; brownout, sessizliği hâlâ çözebileceğiniz bir destek kaydına çeviren şeydir.
Kapanış günü: 404 değil 410
Doğru durum kodu 410 Gone. "Bu vardı ve bilerek kaldırıldı" der. 404 ise "böyle bir adres tanımıyorum" der ve karşı tarafı olmayan bir yazım hatasını aramaya gönderir. Salesforce platform API'sinin emekli edilen sürümlerinde REST tarafı bunu böyle yapıyor, 410 dönüyor.
Aynı emeklilik tarih konusunda da uyarıcı bir örnek. 21.0 ile 30.0 arası sürümlerin kapatılması ilk olarak Summer '23 sürümü için duyuruldu, sonra Summer '25'e ertelendi ve orada tamamlandı. İki yıllık kayma, çünkü tüketiciler taşınmamıştı. Tarih koymak yetmiyor; tarihi tutturan şey telemetri ve brownout. Bir de hata gövdesi: içine geçiş bağlantısını ve devam eden sürümün adını koyun, gece yarısı logu okuyan kişi dokümanı aramak zorunda kalmasın.
Bu haftaya sığan ilk adım
En çok kullanılan API'nizi seçin ve dört soruyu tek cümleyle cevaplamayı deneyin. Bir: sürüm erişim loglarında bir alan olarak duruyor mu, geçen hafta hangi tüketici hangi sürümü kaç kez çağırdı? İki: "kırıcı değişiklik" tanımınız yazılı mı ve kararı kim veriyor? Üç: bu tanım CI'da otomatik kontrol ediliyor mu, yoksa kod incelemesinde mi konuşuluyor? Dört: bir sürümü emekliye ayırdığınızda kaç ay destekleyeceğiniz yazılı mı ve brownout takviminiz var mı?
Hangi soruya tek cümleyle cevap veremiyorsanız, bir sonraki kırılma oradan çıkacak. Arayüz sözleşmelerini baştan kurmanın diğer faydaları için API-first yaklaşımı, teslimat garantileri ve mükerrer kayıt tarafı için entegrasyonlarda veri senkronizasyonu, yeni sürümün kimlik doğrulama ve yetki tasarımı için API güvenliği yazımıza bakabilirsiniz. Mobil istemciler bu işin en zor vakası: kullanıcının telefonundaki eski sürümü yükseltmeye zorlayamazsınız, o yüzden mobile bakan API'lerde pencereyi uzun tutun ve zorunlu güncelleme mekanizmasını ilk sürümde kurun (native mi cross-platform mı).
Bu konuda yardıma mı ihtiyacınız var?