2026-08-22mimari4 dk

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.