---
title: 'Sürümleme Stratejileri'
source: 'https://academia.sh/tr/kurslar/api-tasarimi/surumleme-stratejileri'
course: 'Web API Tasarımı'
language: tr
updated: '2026-08-17T18:06:44+00:00'
license: 'CC BY-SA 4.0'
---

# Sürümleme Stratejileri

Aynı kaynağın iki sürümünün tek sunucuda yayımlanması, yol, başlık ve içerik tabanlı sürümlemenin istemci koduna ölçülen eşit maliyeti ve aracıların sürümü ayırt edip edemediğinden doğan gerçek fark.

Doğrulama şeması bir önceki derste sabit kabul edildi: kalem sayısı en çok üç, iade tarihi
en çok otuz gün ileri. Bu sayılar değişebilir; daha önemlisi, gövdenin **yapısı** değişebilir.
Ödünç kaydının `uye` alanı düz bir kimlik dizgisiyken bir ada da ihtiyaç duyulduğunda nesneye
dönüşür, tek `isbn` alanı çok kalemli ödünçler gerektiğinde diziye dönüşür, `iadeTarihi`
alanının adı `sonTarih` olur.

Bu değişikliklerin ortak özelliği, hâlihazırda çalışan istemcileri bozmalarıdır. Sürümleme,
bozmayı önlemez; **eski sözleşmenin yürürlükte kalmasını** sağlar. Bu ders üç sürümleme
biçemini aynı sunucuda yan yana çalıştırır ve ikisini ölçer: istemci kodunun değişen satır
sayısı ve aracıların sürümü ayırt edip edemediği.

## Aynı Kaynağın İki Sürümü

Aşağıdaki sunucu tek bir ödünç kaydını iki sürümde sunar. Sürümler aynı veriden türetilir;
farklı olan yalnızca dışa verilen gövdenin biçimidir. Üç biçem de aynı süreçte çalışır.

```js
// sunucu.mjs — ayni odunc kaydini iki surumde, uc ayri surumleme bicemiyle yayimlar
import { createServer } from "node:http";

const KAYIT = { id: "O-1", uye: "U-1001", ad: "Ayse Yilmaz", isbn: "978-0262033848", sonTarih: "2026-03-20" };

// v1: duz yapi, tek kitap.  v2: uye nesnesi, kalem dizisi, alan adi degismis.
const SURUMLER = {
  1: (k) => ({ id: k.id, uye: k.uye, isbn: k.isbn, iadeTarihi: k.sonTarih }),
  2: (k) => ({ id: k.id, uye: { kimlik: k.uye, ad: k.ad }, kalemler: [{ isbn: k.isbn }], sonTarih: k.sonTarih }),
};

const ORTAM_TIPI = "application/vnd.kutuphane.odunc+json";

// Accept basligindan surum parametresini cikarir: ...+json;surum=2
const acceptSurumu = (accept) => {
  const m = /surum=(\d+)/.exec(accept ?? "");
  return m ? Number(m[1]) : null;
};

const yaz = (yanit, surum, ekBaslik) => {
  yanit.writeHead(200, { "content-type": `${ORTAM_TIPI}; surum=${surum}`, ...ekBaslik });
  yanit.end(JSON.stringify(SURUMLER[surum](KAYIT)));
};

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const yol = istek.url.split("?")[0];

  // 1) Yol tabanli: surum adresin parcasi. Iki surum iki ayri kaynak adresidir.
  const m = /^\/v(\d+)\/odunc\/(.+)$/.exec(yol);
  if (m && SURUMLER[m[1]]) return yaz(yanit, Number(m[1]), {});

  if (yol === "/odunc/O-1") {
    // 2) Icerik tabanli: surum ortam turunun parametresi. Vary: Accept gerekir.
    const kabul = acceptSurumu(istek.headers.accept);
    if (kabul && SURUMLER[kabul]) return yaz(yanit, kabul, { vary: "Accept" });

    // 3) Baslik tabanli: surum ayri bir baslikta. Vary: o baslik.
    const baslik = Number(istek.headers["api-surum"] ?? 1);
    if (SURUMLER[baslik]) return yaz(yanit, baslik, { vary: "Api-Surum" });
  }

  yanit.writeHead(404, { "content-type": "application/problem+json; charset=utf-8" });
  yanit.end(JSON.stringify({ type: "https://ornek.kutuphane/sorunlar/kaynak-yok", title: "Kaynak bulunamadı", status: 404, detail: `${yol} yok.`, instance: "ol-0001" }));
}).listen(8434, "127.0.0.1", () => console.log("sunucu 127.0.0.1:8434"));
```

```bash
#!/usr/bin/env bash
# Ayni kaydin iki surumunu uc ayri surumleme bicemiyle ister.
node sunucu.mjs & sunucu=$!
sleep 0.5

echo "--- yol tabanli ---"
curl -sS -w '  [%{content_type}]\n' http://127.0.0.1:8434/v1/odunc/O-1
curl -sS -w '  [%{content_type}]\n' http://127.0.0.1:8434/v2/odunc/O-1

echo "--- baslik tabanli ---"
curl -sS -H 'Api-Surum: 1' -w '  [%{content_type}]\n' http://127.0.0.1:8434/odunc/O-1
curl -sS -H 'Api-Surum: 2' -w '  [%{content_type}]\n' http://127.0.0.1:8434/odunc/O-1

echo "--- icerik tabanli ---"
curl -sS -H 'Accept: application/vnd.kutuphane.odunc+json;surum=1' -w '  [%{content_type}]\n' http://127.0.0.1:8434/odunc/O-1
curl -sS -H 'Accept: application/vnd.kutuphane.odunc+json;surum=2' -w '  [%{content_type}]\n' http://127.0.0.1:8434/odunc/O-1

kill "$sunucu"; wait "$sunucu" 2>/dev/null
```

```
sunucu 127.0.0.1:8434
--- yol tabanli ---
{"id":"O-1","uye":"U-1001","isbn":"978-0262033848","iadeTarihi":"2026-03-20"}  [application/vnd.kutuphane.odunc+json; surum=1]
{"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ayse Yilmaz"},"kalemler":[{"isbn":"978-0262033848"}],"sonTarih":"2026-03-20"}  [application/vnd.kutuphane.odunc+json; surum=2]
--- baslik tabanli ---
{"id":"O-1","uye":"U-1001","isbn":"978-0262033848","iadeTarihi":"2026-03-20"}  [application/vnd.kutuphane.odunc+json; surum=1]
{"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ayse Yilmaz"},"kalemler":[{"isbn":"978-0262033848"}],"sonTarih":"2026-03-20"}  [application/vnd.kutuphane.odunc+json; surum=2]
--- icerik tabanli ---
{"id":"O-1","uye":"U-1001","isbn":"978-0262033848","iadeTarihi":"2026-03-20"}  [application/vnd.kutuphane.odunc+json; surum=1]
{"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ayse Yilmaz"},"kalemler":[{"isbn":"978-0262033848"}],"sonTarih":"2026-03-20"}  [application/vnd.kutuphane.odunc+json; surum=2]
```

Üç biçem de aynı iki gövdeyi üretir. Fark, sürümün nereye yazıldığındadır: yol tabanlı
biçemde adrese, başlık tabanlı biçemde ayrı bir isteğe özgü başlığa, içerik tabanlı biçemde
`Accept` başlığındaki ortam türünün parametresine.

İçerik tabanlı biçem, HTTP'nin kendi içerik pazarlığı düzeneğini kullanır. Sunucunun
döndürdüğü `Content-Type` da sürümü taşır, yani yanıt kendi sürümünü bildirir. Başlık
tabanlı biçemde bu bilgi ayrı bir başlıktadır ve HTTP'nin bildiği bir kavram değildir; işe
yaraması için istemci ile sunucunun aynı başlık adı üzerinde anlaşması gerekir.

## İstemci Kodunun Maliyeti

Biçem seçiminin ilk gerekçesi genellikle "istemcinin daha az kod değiştirmesi" diye
sunulur. Bu, ölçülebilir bir iddiadır. Aşağıdaki program üç çağrı yeri olan bir istemcinin
kaynağını üç biçem için üretir ve iki senaryoda değişen satır sayısını sayar: bütün çağrı
yerlerinin v2'ye geçmesi ve yalnız birinin geçmesi.

```js
// istemci-maliyeti.mjs — uc surumleme bicemi icin istemci kodunun degisen satir sayisi
// Uc cagri yeri, uc senaryo: [1,1,1] hepsi v1 | [2,2,2] tam gecis | [2,1,1] kismi gecis
// Sabit adlari kararli tutulur: cogunlugun surumu ana sabitte kalir, azinlik icin sabit eklenir.
const TIP = "application/vnd.kutuphane.odunc+json";
const YOLLAR = [["oduncGetir  ", "(id)", "/odunc/${id}"], ["oduncListesi", "()  ", "/odunc"], ["uyeGetir    ", "(id)", "/uyeler/${id}"]];
const cogunluk = (s) => (s.filter((v) => v === 1).length >= 2 ? 1 : 2);
const cagri = (k, adres, ek) => `export const ${k[0]} = ${k[1]} => iste(\`${adres}\`${ek});`;

const BICEMLER = {
  "yol tabanlı": (s) => {
    const c = cogunluk(s), ad = (v) => (v === c ? "TABAN" : `TABAN_V${v}`);
    return [
      `const TABAN = "http://127.0.0.1:8434/v${c}";`,
      ...[...new Set(s)].filter((v) => v !== c).map((v) => `const TABAN_V${v} = "http://127.0.0.1:8434/v${v}";`),
      ...YOLLAR.map((k, i) => cagri(k, "${" + ad(s[i]) + "}" + k[2], "")),
    ];
  },
  "başlık tabanlı": (s) => {
    const c = cogunluk(s), ad = (v) => (v === c ? "SURUM" : `SURUM_V${v}`);
    return [
      'const TABAN = "http://127.0.0.1:8434";',
      `const SURUM = { "api-surum": "${c}" };`,
      ...[...new Set(s)].filter((v) => v !== c).map((v) => `const SURUM_V${v} = { "api-surum": "${v}" };`),
      ...YOLLAR.map((k, i) => cagri(k, "${TABAN}" + k[2], `, ${ad(s[i])}`)),
    ];
  },
  "içerik tabanlı": (s) => {
    const c = cogunluk(s), ad = (v) => (v === c ? "SURUM" : `SURUM_V${v}`);
    return [
      'const TABAN = "http://127.0.0.1:8434";',
      `const SURUM = { accept: "${TIP};surum=${c}" };`,
      ...[...new Set(s)].filter((v) => v !== c).map((v) => `const SURUM_V${v} = { accept: "${TIP};surum=${v}" };`),
      ...YOLLAR.map((k, i) => cagri(k, "${TABAN}" + k[2], `, ${ad(s[i])}`)),
    ];
  },
};

const degisen = (onceki, sonraki) => sonraki.filter((x) => !onceki.includes(x)).length;

console.log("biçem            satır  tam geçiş  kısmi geçiş");
for (const [ad, f] of Object.entries(BICEMLER)) {
  const v1 = f([1, 1, 1]);
  console.log(`${ad.padEnd(16)} ${String(v1.length).padStart(5)} ` +
    `${String(degisen(v1, f([2, 2, 2]))).padStart(10)} ${String(degisen(v1, f([2, 1, 1]))).padStart(12)}`);
}

for (const ad of ["yol tabanlı", "başlık tabanlı"]) {
  console.log(`\n— kısmi geçişte ${ad} istemci —`);
  for (const s of BICEMLER[ad]([2, 1, 1])) console.log(s);
}
```

```
biçem            satır  tam geçiş  kısmi geçiş
yol tabanlı          4          1            2
başlık tabanlı       5          1            2
içerik tabanlı       5          1            2

— kısmi geçişte yol tabanlı istemci —
const TABAN = "http://127.0.0.1:8434/v1";
const TABAN_V2 = "http://127.0.0.1:8434/v2";
export const oduncGetir   = (id) => iste(`${TABAN_V2}/odunc/${id}`);
export const oduncListesi = ()   => iste(`${TABAN}/odunc`);
export const uyeGetir     = (id) => iste(`${TABAN}/uyeler/${id}`);

— kısmi geçişte başlık tabanlı istemci —
const TABAN = "http://127.0.0.1:8434";
const SURUM = { "api-surum": "1" };
const SURUM_V2 = { "api-surum": "2" };
export const oduncGetir   = (id) => iste(`${TABAN}/odunc/${id}`, SURUM_V2);
export const oduncListesi = ()   => iste(`${TABAN}/odunc`, SURUM);
export const uyeGetir     = (id) => iste(`${TABAN}/uyeler/${id}`, SURUM);
```

Üç biçem de aynı sayıları veriyor: tam geçişte bir satır, kısmi geçişte iki. Beklenen sonuç
bu değildi; yol tabanlı sürümlemenin daha pahalı olduğu yaygın bir kanıdır. Kanının kaynağı,
adresin çağrı yerlerine dağılmış olduğu varsayımıdır. Adres tek bir sabitte toplandığında
sürüm de o sabitte toplanır ve maliyet başlıktakiyle eşitlenir.

Ölçümün gösterdiği asıl şey şudur: **sürüm bilgisi istemcide kaç yerde duruyorsa maliyet
odur, biçem değil.** Üç biçemde de tek bir yerde durabiliyorsa üçü de aynı fiyattadır. Bu
durumda seçim, istemci kodunun dışında bir gerekçeye dayanmak zorundadır.

## Aracıların Gördüğü

O gerekçe, isteğin istemci ile sunucu arasındaki yolda geçtiği aracılardadır. Aşağıdaki
program küçük bir önbellektir ve iki kiple çalışır: anahtarı yalnız adresten üreten kip ve
yanıttaki `Vary` başlığında bildirilen istek başlıklarını da anahtara katan kip.

```js
// onbellek.mjs — sunucunun onunde duran kucuk bir onbellek
// Kullanim: node onbellek.mjs adres     (anahtar yalniz istek adresi)
//           node onbellek.mjs vary      (anahtar adres + yanittaki Vary'de bildirilen basliklar)
import { createServer } from "node:http";

const KIP = process.argv[2] ?? "adres";
const YUKARI = "http://127.0.0.1:8434";
const kayitlar = new Map();      // anahtar -> { govde, tip }
const varyBilgisi = new Map();   // adres -> yanitin Vary'sinde bildirilen baslik adlari

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const istekBasliklari = { accept: istek.headers.accept ?? "", "api-surum": istek.headers["api-surum"] ?? "" };
  const anahtarla = (adlar) => istek.url + adlar.map((a) => `|${a}=${istekBasliklari[a]}`).join("");

  const adlar = KIP === "vary" ? (varyBilgisi.get(istek.url) ?? []) : [];
  const bulunan = kayitlar.get(anahtarla(adlar));
  if (bulunan) {
    yanit.writeHead(200, { "content-type": bulunan.tip, "x-onbellek": "isabet" });
    return yanit.end(bulunan.govde);
  }

  const cevap = await fetch(YUKARI + istek.url, { headers: istekBasliklari });
  const govde = await cevap.text();
  const tip = cevap.headers.get("content-type");
  const vary = (cevap.headers.get("vary") ?? "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);

  varyBilgisi.set(istek.url, vary);
  kayitlar.set(anahtarla(KIP === "vary" ? vary : []), { govde, tip });

  yanit.writeHead(200, { "content-type": tip, "x-onbellek": "iska" });
  yanit.end(govde);
}).listen(8435, "127.0.0.1", () => console.log(`onbellek 127.0.0.1:8435 kip=${KIP}`));
```

```bash
#!/usr/bin/env bash
# Onbellegin arkasindan: once v2 isteyen istemci, sonra v1 isteyen istemci.
node sunucu.mjs & s=$!
sleep 0.4

sor() {   # sor <etiket> <curl-argumanlari...>
  printf '%-34s ' "$1"; shift
  curl -sS -D /tmp/b -o /tmp/g "$@"
  printf '%-9s surum=%s  %s\n' \
    "$(grep -i '^x-onbellek' /tmp/b | tr -d '\r' | cut -d' ' -f2)" \
    "$(grep -io 'surum=[0-9]' /tmp/b | head -1 | cut -d= -f2)" \
    "$(head -c 46 /tmp/g)"
}

for kip in adres vary; do
  node onbellek.mjs "$kip" & o=$!
  sleep 0.4
  echo "== onbellek kipi: $kip =="
  echo "-- baslik tabanli (tek adres) --"
  sor "v2 isteyen istemci" -H 'Api-Surum: 2' http://127.0.0.1:8435/odunc/O-1
  sor "v1 isteyen istemci" -H 'Api-Surum: 1' http://127.0.0.1:8435/odunc/O-1
  echo "-- yol tabanli (iki adres) --"
  sor "v2 isteyen istemci" http://127.0.0.1:8435/v2/odunc/O-1
  sor "v1 isteyen istemci" http://127.0.0.1:8435/v1/odunc/O-1
  kill "$o"; wait "$o" 2>/dev/null
done

kill "$s"; wait "$s" 2>/dev/null
```

```
sunucu 127.0.0.1:8434
onbellek 127.0.0.1:8435 kip=adres
== onbellek kipi: adres ==
-- baslik tabanli (tek adres) --
v2 isteyen istemci                 iska      surum=2  {"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ays
v1 isteyen istemci                 isabet    surum=2  {"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ays
-- yol tabanli (iki adres) --
v2 isteyen istemci                 iska      surum=2  {"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ays
v1 isteyen istemci                 iska      surum=1  {"id":"O-1","uye":"U-1001","isbn":"978-0262033
onbellek 127.0.0.1:8435 kip=vary
== onbellek kipi: vary ==
-- baslik tabanli (tek adres) --
v2 isteyen istemci                 iska      surum=2  {"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ays
v1 isteyen istemci                 iska      surum=1  {"id":"O-1","uye":"U-1001","isbn":"978-0262033
-- yol tabanli (iki adres) --
v2 isteyen istemci                 iska      surum=2  {"id":"O-1","uye":{"kimlik":"U-1001","ad":"Ays
v1 isteyen istemci                 iska      surum=1  {"id":"O-1","uye":"U-1001","isbn":"978-0262033
```

Kritik satır, `adres` kipindeki ikinci satırdır. `Api-Surum: 1` isteyen istemci önbellek
isabeti almış ve kendisine **v2 gövdesi** dönmüştür: `uye` alanı düz bir kimlik yerine
nesnedir. Önbellek yalnız adrese baktığı için iki isteği aynı istek saymış, ilk isteğin
yanıtını ikinciye vermiştir. Bu sessiz bir tutarsızlıktır — istemci beklediği yapıyı
alamaz ve **hiçbir hata yanıtı üretilmez**; durum kodu 200'dür. Yol tabanlı biçemde aynı
senaryo iki ayrı ıska verir; iki adres iki kayıttır, karışma olanağı yoktur.

`vary` kipinde başlık tabanlı biçem de doğru çalışır. Yani sorun biçemde değil, aracının
`Vary` başlığını doğru işleyip işlemediğindedir. Ama karar verirken önemli olan budur:
başlık ve içerik tabanlı biçemler, doğruluk için yol üzerindeki **her** aracının `Vary`
başlığını doğru işlemesine bağımlıdır; yol tabanlı biçem hiçbir aracıdan bir şey
beklemez. Aynı gerekçe kütükler, ölçüm panoları ve hata ayıklama için de geçerlidir: yol
tabanlı biçemde hangi sürümün ne kadar çağrıldığı adres kütüğünden okunur, diğerlerinde
başlık kaydedilmemişse okunamaz.

İçerik tabanlı biçem bu ölçekte başlık tabanlı biçemle aynı davranır, ama bir üstünlüğü
vardır: `Accept` başlığı HTTP'nin tanıdığı bir başlıktır ve önbelleklerin `Vary: Accept`
ile karşılaşması olağandır. Kendi uydurduğu bir başlık adına göre değişen bir yanıt,
aracılar açısından daha az beklenen bir durumdur.

## Neyin Sürümlendiği

Üç biçemin hiçbiri, sürümün **neye** iliştiği sorusunu yanıtlamaz. İki seçenek vardır. API
bütünüyle sürümlenirse tek bir sürüm numarası bütün kaynakları kapsar; bir kaynaktaki kırıcı
değişiklik hepsinin sürümünü yükseltir ve hiçbir şeyi değişmemiş istemciler de yeni sürüme
geçmek zorunda kalır. Kaynak başına sürümlenirse her kaynak kendi hızında ilerler, ama
istemcinin izlemesi gereken sürüm sayısı kaynak sayısı kadar olur ve yanıtlar arasındaki
bağlantılar hangi sürüme gideceğini ayrıca belirtmek zorunda kalır.

Seçim, kaynakların birbirine ne kadar bağlı olduğuna bakar: ödünç kaydı üyeye ve kitaba
gömülü alanlarla bağlıysa üçünü ayrı sürümlemek, tutarsız bileşimlere yol açar. Tasarım
Sistemleri kursundaki tek paket ile bileşen başına paket seçimi aynı ödünleşimin başka bir
alandaki karşılığıdır.

## Özet

- Sürümleme kırıcı değişikliği önlemez; eski sözleşmenin yürürlükte kalmasını sağlar, yani
  iki sözleşme bir süre yan yana yaşar.
- Yol, başlık ve içerik tabanlı biçemler aynı iki gövdeyi üretir; fark sürümün adrese,
  ayrı bir başlığa ya da ortam türünün parametresine yazılmasıdır.
- İstemci kodunun maliyeti üç biçemde de aynıdır — tam geçişte bir satır, kısmi geçişte iki;
  belirleyici olan biçem değil, sürüm bilgisinin istemcide kaç yerde durduğudur.
- Gerçek fark aracılardadır: adres anahtarlı bir önbellek, başlık ve içerik tabanlı
  biçemlerde v1 isteyen istemciye v2 gövdesi döndürür ve hiçbir hata üretmez.
- Başlık ve içerik tabanlı biçemler doğruluk için yol üzerindeki her aracının `Vary`
  başlığını işlemesine bağımlıdır; yol tabanlı biçem hiçbir aracıdan bir şey beklemez.
- Sürümün API'ye mi kaynağa mı iliştiği ayrı bir karardır ve kaynakların birbirine
  bağlılığına göre verilir.

## Sonraki Adım

Bu ders iki sürümü yan yana yayımladı ama sürüm numarasının **neden** ikiye çıktığını
sormadı: `uye` alanının nesneye dönmesi, `isbn` alanının diziye dönmesi ve `iadeTarihi`
adının değişmesi kırıcıdır diye kabul edildi. Peki `sube` alanına yeni bir seçenek eklemek
kırıcı mıdır? Yanıtı boş bırakılabilen bir alanı zorunlu yapmak? Yanıta yeni bir alan
eklemek? Bu soruların yanıtı sezgiye bırakıldığında her ekip başka yanıt verir ve sürüm
numarası tartışma konusu olur. Sonraki ders iki şemayı karşılaştırıp kırıcı değişiklikleri
saptayan bir betik yazar ve sürüm numarasını tartışmadan, farktan türetir.
