API sürümleme: kırmadan değiştirmenin yolları
Yeni bir majör sürüm bir özellik değil, ertelenmiş bir borç. Kırıcı değişikliğin tanımı, genişleterek çözme kalıpları, sürümün nereye yazılacağı ve v1'i kapatma planı.
Sürümleme tartışması genelde yanlış soruyla başlıyor: "URL'de mi, başlıkta mı?" Asıl soru şu: bir API'yi bozmadan ne kadar değiştirebilirsiniz? Cevabı bilirseniz v2 açma ihtiyacınızın çoğu ortadan kalkar, çünkü yeni bir majör sürüm bir özellik değil, bir borç.
Neyin kırıcı olduğunu önce tanımlayın
Kırıcı değişikliğin tanımı üründen ürüne değişiyor ama pratikte işleyen kural şu: istemcinin bugün gönderdiği geçerli bir istek yarın hata dönüyorsa ya da bugün okuduğu bir alan yarın kayboluyorsa, kırdınız.
Kırıcı olan: alan silmek, alan adını değiştirmek, bir alanı zorunlu yapmak, enum'dan değer çıkarmak, tipi daraltmak (string → int), varsayılan davranışı değiştirmek, hata kodunu değiştirmek, sayfalama boyutunun üst sınırını düşürmek.
Kırıcı olmayan: yeni opsiyonel alan eklemek, yanıt gövdesine yeni alan eklemek, enum'a yeni değer eklemek (istemci bilinmeyen değerleri tolere ediyorsa), yeni uç nokta eklemek.
Son maddedeki parantez önemli. Enum'a değer eklemek ancak istemciler bilinmeyeni yutuyorsa güvenli. Bunu şansa bırakmayın, sözleşmeye yazın:
{
"status": "settled",
"_note": "status alanına ileride yeni değerler eklenebilir; bilinmeyen değeri hata olarak işlemeyin"
}
Ve kendi istemci kütüphanenizde varsayılanı tolerans yapın:
type Status = "pending" | "settled" | "failed" | { unknown: string }
function parseStatus(raw: string): Status {
return raw === "pending" || raw === "settled" || raw === "failed"
? raw
: { unknown: raw }
}
Genişletme, sürümlemenin yerine geçer
Değişikliklerin büyük kısmı yeni sürüm gerektirmeden yapılabilir. Üç kalıp neredeyse her yerde işe yarıyor.
Yeni alanı eskisinin yanına koyun, yerine değil. amount alanının para birimi taşıması gerekiyorsa amount'u nesneye çevirmeyin; amount_money diye ikinci bir alan ekleyin ve eskisini bir süre yazmaya devam edin.
{
"amount": 4200,
"amount_money": { "value": 4200, "currency": "TRY" }
}
Davranış değişikliğini istemcinin talebine bağlayın. Yeni sıralama mantığını varsayılan yapmak yerine parametreyle açın; varsayılanı bir sonraki majörde değiştirin.
GET /orders?sort=created_at # varsayılan, eski davranış
GET /orders?sort=updated_at # yeni
Silmeyi iki adıma bölün. Önce alanı yanıttan çıkarmak yerine boşaltın ve uyarı başlığı gönderin; kimin hâlâ okuduğunu telemetriden görün. Kullanan kalmayınca çıkarın.
Sürüm nereye yazılır
Üç yaygın yol var ve seçim teknik olmaktan çok operasyonel.
| Yöntem | Örnek | Artısı | Eksisi |
|---|---|---|---|
| URL yolu | /v2/orders |
Tarayıcıdan, curl'den, logdan görünür | Kaynak kimliği sürüme yapışır, önbellek anahtarı çoğalır |
| Başlık | Accept: application/vnd.api.v2+json |
URL temiz kalır, içerik pazarlığına uyar | Log ve hata ayıklamada görünmez, proxy'ler düşürebilir |
| Tarih damgası | API-Version: 2026-03-01 |
Küçük adımlarla ilerler, majör sayısı patlamaz | Sunucuda dönüşüm katmanı gerektirir |
URL yolu en çok kullanılan, çünkü hata ayıklaması ucuz. Tarih damgası yaklaşımı ise büyük ölçekte tercih ediliyor: istemci bir tarihe sabitleniyor, sunucu o tarihten bugüne kadarki dönüşümleri sırayla uyguluyor.
Bu dönüşüm zinciri şöyle görünüyor:
TRANSFORMS = [
("2026-03-01", split_amount_into_money),
("2026-06-15", rename_state_to_status),
]
def to_client_version(payload, client_date):
for date, fn in reversed(TRANSFORMS):
if client_date < date:
payload = fn.downgrade(payload)
return payload
Buradaki asıl kazanç, iş mantığının tek bir güncel şemayla çalışması. Eski sürümler kodun her yerine dağılmıyor, tek bir dosyada geriye dönüşüm fonksiyonu olarak duruyor. Maliyeti de açık: her dönüşümün testi olmak zorunda, yoksa altı ay sonra kimse zincire dokunamıyor.
v2 açtıysanız v1'in ölüm tarihini de yazın
Sürüm açmanın kolay, kapatmanın zor olduğu yer burası. Kapanış planı olmadan açılan her majör sürüm kalıcıdır.
Yayına almadan önce şu üçü belli olsun: v1'in destekleneceği tarih, o tarihe kadar hangi düzeltmelerin v1'e de gideceği (genelde yalnız güvenlik), ve kimin hâlâ v1 kullandığını gösteren ölçüm. Üçüncüsü olmadan ilk ikisi temenni.
Ölçümü uç nokta bazında değil istemci bazında tutun. "v1'e günde 40 bin istek geliyor" bilgisi işe yaramaz; "v1'i üç müşteri kullanıyor, ikisi tek bir uç nokta için" bilgisi kapatma planı üretir.
Kapatmaya yaklaşırken yanıta uyarı ekleyin, sonra kısa kesintiler planlayın:
Sunset: Sat, 01 Aug 2026 00:00:00 GMT
Deprecation: true
Link: <https://docs.example.com/migrate-v2>; rel="deprecation"
Planlı kısa kesinti, ilan edilen tarihte servisi birkaç dakikalığına kapatıp geri açmak demek. Entegrasyonunu okumayan ekipler ancak böyle haberdar oluyor, ama bunu duyurmadan yapmayın; sürpriz kesinti kırıcı değişikliğin kendisinden daha çok güven kaybettirir.
Kırıcı değişikliği testle yakalayın, incelemeyle değil
Kod incelemesi kırıcı değişikliği kaçırır, çünkü inceleyen kişi şemanın tamamını akılda tutmak zorunda kalır. Bunu makineye yaptırın: şemanın önceki sürümünü depoda tutun ve yeni şemayla karşılaştıran bir adım ekleyin. OpenAPI kullanıyorsanız hazır karşılaştırıcılar var; kullanmıyorsanız kendi şemanızdan üretilen bir anlık görüntü yeter.
# CI adımı: yayınlanan şemaya göre fark al, kırıcıysa çık
npm run schema:dump > /tmp/new.json
diff-schema baseline/openapi.json /tmp/new.json --fail-on breaking
İkinci ağ olarak sözleşme testleri işe yarıyor: en büyük üç istemcinin gerçekten gönderdiği isteklerin kaydını alıp her derlemede tekrar oynatın. Şema karşılaştırması alan silmeyi yakalar, sözleşme testi ise davranış değişikliğini yakalar. İkisi farklı hataları tutuyor, biri diğerinin yerine geçmiyor.
Kısa özet
Sürümleme, kırıcı değişikliği ucuzlatan bir araç değil, ertelenmiş bir maliyet. Önce genişleterek çözmeyi deneyin, sürümü yalnız veri modeli gerçekten değiştiğinde açın, açtığınız gün kapanış tarihini ve kimin kullandığını ölçmeye başlayın.
- api
- sürümleme
- backend
- uyumluluk