---
title: 'Web API Tasarımı'
source: 'https://academia.sh/tr/kurslar/api-tasarimi'
language: tr
updated: '2026-08-17T04:38:42+00:00'
license: 'CC BY-SA 4.0'
lessons: 34
outcomes:
    - 'Kaynak modeli ve URI tasarımıyla tutarlı bir REST arayüzü kurar'
    - 'API sürümleme ve hata sözleşmesi kararlarını gerekçelendirir'
    - 'Sözleşmeyi makine okunur biçimde belgeler ve sınar'
---

# Web API Tasarımı

Programların birbirine bağlandığı sözleşmenin kurulması: API biçemlerinin ödünleşimleri, kaynak modeli ve URI tasarımı, durum kodu ve gövde kararları, hata sözleşmesi ile sürümleme ve sözleşmenin makine okunur biçimde belgelenip sınanması.

## API Biçemleri

Aynı işi karşılayan farklı arayüz biçemleri: sözleşme ve uyumluluk kavramı, kaynak temelli ile uzak yordam çağrısı ve sorgu temelli yaklaşımların istek sayısı ve taşınan bayt üzerinden karşılaştırılması, REST ilkeleri, ikili protokoller ve istek–yanıt dışı iletişim.

### API Nedir

Source: https://academia.sh/tr/kurslar/api-tasarimi/api-nedir

Bir arayüzü sözleşme yapan şey: tüketicinin yazılı olmayan varsayımları, uyumlu ve kırıcı değişimin tüketici davranışıyla tanımlanması, hoşgörülü okuyucu ile katı okuyucunun aynı değişime verdiği farklı yanıt ve sözleşmenin sahibinin kim olduğu.

Önceki kurs sunucunun dışarıya bir dosya ya da bir belge verdiği durumları kurdu: insanın
okuyacağı içerik. Kütüphane ödünç servisinin asıl tüketicileri ise programlardır. Şubedeki
raf ekranı kitabın durumunu sorar, gece çalışan rapor işi yazar başına döküm alır, denetim
betiği kayıtların biçimini sınar. Üçü de aynı ucu çağırır ve üçü de o uçtan farklı bir şey
bekler.

Bu noktada ortaya çıkan kavram **uygulama programlama arayüzüdür** (application programming
interface): bir programın başka programlara açtığı çağrılabilir yüzey. Bu ders o yüzeyin
neden bir **sözleşme** (contract) sayıldığını ve sözleşmenin ne zaman bozulduğunu kurar.

#### Sözleşme, Gövdeden Fazlasıdır

Bir ucun döndürdüğü JSON gövdesine bakmak, sözleşmenin tamamını görmek değildir. Sözleşme,
iki tarafın birbirinden bekleyebileceği her şeyi kapsar:

- **Yüzey:** hangi adresler vardır, hangi yöntemleri kabul ederler, hangi parametreleri
  tanırlar.
- **Biçim:** gövdedeki alanların adları, tipleri, zorunluluk durumları ve değer kümeleri.
- **Anlam:** bir alanın neyi gösterdiği. `durum` alanının `rafta` değeri, kitabın fiziksel
  yerini mi yoksa ödünç verilebilirliğini mi bildirir?
- **Davranış:** aynı isteğin iki kez yapılmasının sonucu, hataların hangi durum koduyla
  bildirildiği, sıralamanın garanti edilip edilmediği.

Bunların yalnız bir kısmı yazılır. Yazılmayan kısım da sözleşmenin parçasıdır, çünkü
tüketici ona dayanarak çalışır. Bir listenin her seferinde aynı sırayla döndüğünü fark eden
bir tüketici, o sırayı varsayarak kod yazar; sıra değiştiğinde arayüzün "belgelenmiş" hiçbir
kısmı değişmemiş olsa da tüketici kırılır. Sözleşmenin sınırını belgenin kapsamı değil,
**tüketicinin bağlandığı her şey** çizer.

#### Uyumluluk Bir Ölçüm Sorusudur

Bir değişikliğin **kırıcı** (breaking) olup olmadığı, değişikliğin büyüklüğüyle değil,
var olan tüketicilerin o değişiklikten sonra çalışıp çalışmadığıyla tanımlanır. Bu tanım
ölçülebilir: aynı tüketicileri, yayımlanan farklı sözleşme sürümlerine karşı çalıştırmak
yeterlidir.

Kütüphane servisinin kitap ucu üç ayrı sürüm yayımlayabilecek biçimde yazılır.

```js
// sunucu.mjs — kitap kaydini yayimlayan uc. SOZLESME degiskeni yayimlanan surumu secer.
import { createServer } from "node:http";

const SOZLESME = process.env.SOZLESME ?? "v1";
const KAYIT = { isbn: "978-0262033848", ad: "Algoritmalara Giris", yazar: "Cormen", durum: "rafta" };

const govdeUret = () => {
  if (SOZLESME === "v1") return KAYIT;
  if (SOZLESME === "v2-ekleme") return { ...KAYIT, sayfa: 1312 };            // alan eklendi
  return { isbn: KAYIT.isbn, ad: KAYIT.ad,                                   // alan adi ve tipi degisti
           yazar: { ad: KAYIT.yazar }, oduncDurumu: KAYIT.durum, sayfa: 1312 };
};

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const govde = JSON.stringify(govdeUret());
  yanit.setHeader("Content-Type", "application/json; charset=utf-8");
  yanit.setHeader("Content-Length", Buffer.byteLength(govde));
  yanit.writeHead(200).end(govde);
}).listen(8441, "127.0.0.1");
```

Üç tüketici, üç ayrı okuma alışkanlığını temsil eder. Raf ekranı yalnız gereksindiği iki
alanı okur. Rapor işi de az alan okur ama bir alanın **tipine** bağlıdır. Denetim betiği
gövdeyi bütün olarak sınar ve tanımadığı alanı hata sayar.

```js
// tuketici.mjs — uc ayri tuketici. Kullanim: node tuketici.mjs <uctermin|rapor|denetci>
const rol = process.argv[2];
const kayit = await (await fetch("http://127.0.0.1:8441/kitaplar/978-0262033848")).json();

const ROLLER = {
  // Hosgorulu okuyucu: yalniz gereksindigi alanlari okur, tanimadigi alanlari yok sayar.
  uctermin: (k) => {
    if (typeof k.durum !== "string") throw new Error("durum alani yok");
    return `raf ekrani: ${k.ad} -> ${k.durum}`;
  },
  // Hosgorulu, ama bir alanin tipine bagli.
  rapor: (k) => {
    if (typeof k.yazar !== "string") throw new Error("yazar alani dizgi degil");
    return `rapor satiri: ${k.yazar.toUpperCase()} / ${k.ad}`;
  },
  // Kati okuyucu: sozlesmede sayilmayan her alani reddeder.
  denetci: (k) => {
    const BEKLENEN = ["isbn", "ad", "yazar", "durum"];
    const fazla = Object.keys(k).filter((a) => !BEKLENEN.includes(a));
    if (fazla.length) throw new Error(`beklenmeyen alan: ${fazla.join(", ")}`);
    return `denetim: ${k.isbn} gecerli`;
  },
};

try { console.log(`  ${rol.padEnd(9)} OK    ${ROLLER[rol](kayit)}`); }
catch (e) { console.log(`  ${rol.padEnd(9)} KIRIK ${e.message}`); }
```

```bash
#!/usr/bin/env bash
# Uc sozlesme surumunu uc tuketiciyle sinar.
A=http://127.0.0.1:8441/kitaplar/978-0262033848
for s in v1 v2-ekleme v2-degistirme; do
  SOZLESME=$s node sunucu.mjs & p=$!
  echo "== yayimlanan sozlesme: $s =="
  printf '  govde: '
  curl -s --retry 20 --retry-all-errors --retry-delay 0 --retry-connrefused "$A"; echo
  for rol in uctermin rapor denetci; do node tuketici.mjs $rol; done
  kill $p 2>/dev/null; wait $p 2>/dev/null || true
done
```

```
== yayimlanan sozlesme: v1 ==
  govde: {"isbn":"978-0262033848","ad":"Algoritmalara Giris","yazar":"Cormen","durum":"rafta"}
  uctermin  OK    raf ekrani: Algoritmalara Giris -> rafta
  rapor     OK    rapor satiri: CORMEN / Algoritmalara Giris
  denetci   OK    denetim: 978-0262033848 gecerli
== yayimlanan sozlesme: v2-ekleme ==
  govde: {"isbn":"978-0262033848","ad":"Algoritmalara Giris","yazar":"Cormen","durum":"rafta","sayfa":1312}
  uctermin  OK    raf ekrani: Algoritmalara Giris -> rafta
  rapor     OK    rapor satiri: CORMEN / Algoritmalara Giris
  denetci   KIRIK beklenmeyen alan: sayfa
== yayimlanan sozlesme: v2-degistirme ==
  govde: {"isbn":"978-0262033848","ad":"Algoritmalara Giris","yazar":{"ad":"Cormen"},"oduncDurumu":"rafta","sayfa":1312}
  uctermin  KIRIK durum alani yok
  rapor     KIRIK yazar alani dizgi degil
  denetci   KIRIK beklenmeyen alan: oduncDurumu, sayfa
```

Dokuz sonuç üç kural verir.

**Alan eklemek kendiliğinden uyumlu değildir.** `sayfa` alanının eklenmesi iki tüketiciyi
etkilemedi, denetim betiğini kırdı. Sağlayıcı tarafında "yalnızca ekledim" cümlesi, tüketici
tarafında bir hata olarak göründü. Eklemenin uyumlu sayılabilmesi, tüketicilerin tanımadıkları
alanları yok saymasına bağlıdır.

**Ad değiştirmek alan silmekle aynı şeydir.** `durum` alanı `oduncDurumu` olarak yeniden
adlandırıldığında, raf ekranı için o alan yok olmuştur. Sağlayıcının niyeti "adı düzeltmek"
olsa da tüketicinin gördüğü şey silinmiş bir alandır.

**Tip değiştirmek adı korusa bile kırıcıdır.** `yazar` alanı yerinde durmaktadır, ama artık
bir dizgi değil bir nesnedir. Alan adına bakan bir denetim bu değişikliği yakalamaz; rapor
işi çalışma anında kırılır.

#### Uyumluluğun İki Yönü

Ölçüm tek bir yönü sınadı: sunucu yeni, tüketiciler eski. Bu yöne **geriye dönük uyumluluk**
(backward compatibility) denir ve sağlayıcının yayım kararını belirleyen ölçüt budur, çünkü
sunucu tüketicilerden önce güncellenir.

Ters yön de vardır ve gözden kaçar: tüketici yeni, sunucu eski. **İleriye dönük uyumluluk**
(forward compatibility), henüz yayımlanmamış bir sözleşmeye göre yazılmış tüketicinin eski
sunucuyla çalışabilmesidir. Kütüphane servisinde bu durum, uçbirimler merkezden önce
güncellendiğinde ortaya çıkar: yeni uçbirim `sayfa` alanını okumaya çalışır, eski sunucu o
alanı hiç göndermez. Alan eksik olduğunda uçbirimin çökmesi ile boş göstermesi arasındaki
fark, sözleşmenin değil tüketicinin kararıdır.

İki yön birlikte bir yayım kuralı verir: **önce alanı ekleyen sunucu, sonra alanı okuyan
tüketici yayımlanır; alan kaldırılırken sıra terstir.** Sözleşme değişimi tek bir olay değil,
iki taraf arasında sıralı bir geçiştir.

#### Hoşgörülü Okuyucu

Ölçümdeki denetim betiği, kırılmasına kendi kuralıyla yol açtı: gövdede sayılmayan bir alan
görünce hata verdi. Bu davranışın karşıtı **hoşgörülü okuyucudur** (tolerant reader):
tüketici yalnız gereksindiği alanları okur, tanımadığı her şeyi yok sayar.

Hoşgörülü okuma bir tüketici alışkanlığıdır ama sonucu sağlayıcıyı ilgilendirir: bir arayüz,
tüketicileri hoşgörülü okuduğu ölçüde geliştirilebilir kalır. Katı okuyan tüketicilerin
bulunduğu bir sistemde her ekleme sürüm atlamayı gerektirir; hoşgörülü okuyan tüketicilerin
bulunduğu bir sistemde eklemeler sessizce yayılır.

Bunun ters yönde bir sınırı vardır. Hoşgörü, **girdi** doğrulamasına uygulanmaz. Sunucu
kendisine gelen bir gövdede tanımadığı alanları yok saydığında, tüketicinin yazım yanlışı
sessizce göz ardı edilir: `oduncSuresi` yerine `oduncSure` yazan bir istemci hata almaz,
varsayılan değerle çalışır ve yanlışı ancak sonuçtan anlar. Kural bu yüzden iki yönde
farklıdır: **gönderirken kesin ol, alırken hoşgörülü ol.**

#### Sözleşmenin Sahibi Tüketicidir

Bir arayüzün sahibi onu yayımlayan taraftır, ama sözleşmenin geçerliliğini belirleyen taraf
tüketicidir. Bu ayrımın iki pratik sonucu vardır.

Birincisi, kırıcı değişikliğin maliyeti tüketici sayısıyla artar. Tek tüketicisi olan bir iç
arayüzde `durum` alanını yeniden adlandırmak, iki tarafı birlikte yayımlamak demektir.
Şubelerdeki uçbirimlere dağıtılmış bir arayüzde aynı değişiklik, güncellenmemiş her uçbirimin
kırılması demektir. Aynı teknik değişiklik, iki bağlamda iki ayrı karardır.

İkincisi, sözleşmenin yazılı kısmı ne kadar genişse tüketicinin yazısız varsayımlara dayanma
payı o kadar azalır. Alanların tipleri, zorunlulukları ve değer kümeleri açıkça belirtilmişse,
tüketici "gözlemleyerek" değil "okuyarak" bağlanır. Sıralamanın garanti edilmediği yazılıysa,
sıralamaya dayanan tüketici hatalıdır. Makine okunur bir tanımın değeri buradan gelir ve bu
kursun ilerleyen bölümünde ele alınır.

#### Özet

- Bir API'nin sözleşmesi gövde biçiminden geniştir: yüzey, biçim, anlam ve davranış
  birlikte sözleşmeyi kurar; tüketicinin bağlandığı yazısız varsayımlar da bu kapsamdadır.
- Bir değişikliğin kırıcı olup olmadığı, var olan tüketicilerin değişiklikten sonra çalışıp
  çalışmadığıyla tanımlanır ve tüketicileri yeni sürüme karşı çalıştırarak ölçülür.
- Ölçümde alan eklemek katı okuyan tüketiciyi kırdı, ad değiştirmek alanı silmekle aynı
  etkiyi yaptı, tip değiştirmek ise ad korunduğu hâlde çalışma anında kırılma üretti.
- Hoşgörülü okuyucu tanımadığı alanları yok sayar ve arayüzün eklemelerle geliştirilebilir
  kalmasını sağlar; aynı hoşgörü gelen isteklerin doğrulanmasına uygulanmaz.
- Sözleşmenin maliyeti tüketici sayısıyla artar; yazılı olmayan her ayrıntı, tüketicinin
  gözleme dayanarak bağlanacağı bir yüzeydir.

#### Sonraki Adım

Bu ders sözleşmenin ne olduğunu kurdu ama sözleşmenin **biçimini** açık bıraktı: kitap kaydı
neden bir adresten okunuyor, neden bir yordam çağrısıyla istenmiyor, neden istemci hangi
alanları istediğini söylemiyor? Aynı iş üç ayrı yaklaşımla karşılanabilir — kaynak temelli,
uzak yordam çağrısı biçimli ve sorgu temelli — ve bu üç yaklaşım arasındaki fark bir üslup
tercihi değildir. Sonraki ders üçünü de aynı senaryo için yazar ve aralarındaki farkı üç
ölçüyle gösterir: kaç istek gitti, kaç bayt taşındı, taşınan verinin ne kadarı kullanıldı?

### API Biçemlerinin Karşılaştırması

Source: https://academia.sh/tr/kurslar/api-tasarimi/api-bicemlerinin-karsilastirmasi

Aynı işi karşılayan üç biçem: kaynak temelli, uzak yordam çağrısı biçimli ve sorgu temelli yaklaşımlar aynı senaryoda yazılıp istek sayısı, tel üzerindeki bayt ve taşınan alanın kullanılan oranı ölçülerek karşılaştırılır; gereksinim değiştiğinde hangi tarafın değişmek zorunda kaldığı gösterilir.

Önceki ders bir arayüzü sözleşme yapan şeyi kurdu ama sözleşmenin biçimini açık bıraktı.
Kütüphane servisinden istenen şu iş üç ayrı yaklaşımla karşılanabilir: **bir üyenin
ödünçteki kitaplarını, kitap adları ve son teslim tarihleriyle birlikte listele.**

Üç yaklaşım şudur. **Kaynak temelli** biçemde her kavramın kendi adresi vardır ve istemci
gerekli parçaları tek tek toplar. **Uzak yordam çağrısı** biçeminde tek bir adres vardır,
istemci gövdede bir yordam adı gönderir ve sunucu o iş için hazırlanmış yanıtı döndürür.
**Sorgu temelli** biçemde yine tek adres vardır, ama istemci gövdede hangi alanları istediğini
yazar. Aradaki fark bir üslup tercihi değildir: yanıtın biçimini **kimin belirlediği**
değişir ve bunun ölçülebilir sonuçları vardır.

#### Ortak Veri ve Üçüncü Biçemin Çözümleyicisi

Üç biçem de aynı veri üzerinde çalışır.

```js
// veri.mjs — uc bicemin de uzerinde calistigi ortak kutuphane verisi
export const UYELER = [{ id: 1, ad: "Ayse Kaya", eposta: "ayse@ornek.test", telefon: "0000000000",
  adres: "Merkez Sube, Kat 2", kayit: "2024-01-05", ceza: 0, uyelikTuru: "tam" }];
export const KITAPLAR = {
  "978-0262033848": { isbn: "978-0262033848", ad: "Algoritmalara Giris", yazar: "Cormen",
    sayfa: 1312, yayinevi: "MIT", basim: 2009, raf: "R-12", ozet: "Algoritma tasarimi ve cozumlemesi." },
  "978-0201896831": { isbn: "978-0201896831", ad: "Bilgisayar Programlama Sanati", yazar: "Knuth",
    sayfa: 650, yayinevi: "Addison", basim: 1997, raf: "R-03", ozet: "Temel algoritmalar cildi." },
  "978-0131103627": { isbn: "978-0131103627", ad: "C Programlama Dili", yazar: "Kernighan",
    sayfa: 272, yayinevi: "Prentice", basim: 1988, raf: "R-07", ozet: "Dilin tanimi ve ornekleri." },
};
export const ODUNCLER = [
  { id: 11, uyeId: 1, isbn: "978-0262033848", alinma: "2024-05-01", sonTarih: "2024-05-15" },
  { id: 12, uyeId: 1, isbn: "978-0201896831", alinma: "2024-05-03", sonTarih: "2024-05-17" },
  { id: 13, uyeId: 1, isbn: "978-0131103627", alinma: "2024-05-06", sonTarih: "2024-05-20" },
];
```

Sorgu temelli biçem bir çözümleyici gerektirir: istemcinin yazdığı seçim metni bir ağaca
dönüştürülür, sonra bu ağaç veri üzerinde yürütülür. Aşağıdaki otuz satır bu işi yapar.

```js
// sorgu.mjs — kucuk secim cozumleyicisi. Girdi: "uye(1) { ad oduncler { sonTarih kitap { ad } } }"
export const cozumle = (metin) => {
  const simgeler = metin.match(/[A-Za-z_][A-Za-z0-9_]*|\d+|[{}()]/g) ?? [];
  let i = 0;
  const secimKumesi = () => {                       // { alan alan(arg) { ... } ... }
    const alanlar = [];
    i++;                                            // acilis suslu ayraci
    while (simgeler[i] !== "}") {
      const ad = simgeler[i++];
      let arg = null;
      if (simgeler[i] === "(") { arg = simgeler[i + 1]; i += 3; }
      const alt = simgeler[i] === "{" ? secimKumesi() : null;
      alanlar.push({ ad, arg, alt });
    }
    i++;                                            // kapanis suslu ayraci
    return alanlar;
  };
  simgeler.unshift("{"); simgeler.push("}");
  return secimKumesi();
};

// Secim agacini veri uzerinde yurutur: istemci hangi alanlari istediyse yalniz onlar doner.
export const yurut = (alanlar, kok, cozucu) => {
  const cikti = {};
  for (const alan of alanlar) {
    const deger = cozucu(kok, alan.ad, alan.arg);
    if (!alan.alt) { cikti[alan.ad] = deger; continue; }
    cikti[alan.ad] = Array.isArray(deger)
      ? deger.map((d) => yurut(alan.alt, d, cozucu))
      : yurut(alan.alt, deger, cozucu);
  }
  return cikti;
};
```

Üç biçem tek bir sunucuda yan yana durur; böylece karşılaştırma aynı veri, aynı süreç ve
aynı bağlantı üzerinde yapılır.

```js
// sunucu.mjs — ayni isi uc bicemle karsilayan sunucu: kaynak temelli, uzak yordam, sorgu temelli
import { createServer } from "node:http";
import { UYELER, KITAPLAR, ODUNCLER } from "./veri.mjs";
import { cozumle, yurut } from "./sorgu.mjs";

const govdeOku = (istek) => new Promise((coz) => {
  let m = ""; istek.on("data", (p) => (m += p)); istek.on("end", () => coz(m));
});
const json = (yanit, kod, govde) => {
  const metin = JSON.stringify(govde);
  yanit.setHeader("Content-Type", "application/json; charset=utf-8");
  yanit.setHeader("Content-Length", Buffer.byteLength(metin));
  yanit.writeHead(kod).end(metin);
};

// Sorgu temelli bicem icin cozucu: her alan adi bir veri erisimine karsilik gelir.
const cozucu = (kok, ad, arg) => {
  if (ad === "uye") return UYELER.find((u) => u.id === Number(arg));
  if (ad === "oduncler") return ODUNCLER.filter((o) => o.uyeId === kok.id);
  if (ad === "kitap") return KITAPLAR[kok.isbn];
  return kok[ad];
};

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const yol = new URL(istek.url, "http://yerel").pathname;

  // 1) Kaynak temelli: her kavram ayri bir adres, her adres kaydin tamamini dondurur.
  let e;
  if ((e = /^\/uyeler\/(\d+)$/.exec(yol))) return json(yanit, 200, UYELER.find((u) => u.id === +e[1]));
  if ((e = /^\/uyeler\/(\d+)\/oduncler$/.exec(yol)))
    return json(yanit, 200, ODUNCLER.filter((o) => o.uyeId === +e[1]));
  if ((e = /^\/kitaplar\/(.+)$/.exec(yol))) return json(yanit, 200, KITAPLAR[e[1]]);

  // 2) Uzak yordam cagrisi: tek adres, govdede yordam adi; yanit bicimini sunucu belirler.
  if (yol === "/rpc") {
    const { yordam, parametre } = JSON.parse(await govdeOku(istek));
    if (yordam !== "uyeOduncOzeti") return json(yanit, 200, { hata: "bilinmeyen yordam" });
    const uye = UYELER.find((u) => u.id === parametre.uyeId);
    return json(yanit, 200, { sonuc: { ad: uye.ad, oduncler: ODUNCLER
      .filter((o) => o.uyeId === uye.id)
      .map((o) => ({ sonTarih: o.sonTarih, kitapAdi: KITAPLAR[o.isbn].ad })) } });
  }

  // 3) Sorgu temelli: tek adres, govdede secim kumesi; yanit bicimini istemci belirler.
  if (yol === "/sorgu") {
    const { sorgu } = JSON.parse(await govdeOku(istek));
    return json(yanit, 200, { veri: yurut(cozumle(sorgu), null, cozucu) });
  }
  return json(yanit, 404, { hata: "bulunamadi" });
}).listen(8451, "127.0.0.1", () => console.log("uc bicem 127.0.0.1:8451"));
```

#### Üç Ölçü

Karşılaştırma üç sayıya dayanır. **İstek sayısı** ağ turlarını verir. **Tel üzerindeki bayt**
başlıklar dahil gerçekte taşınan veriyi verir; bunu ölçmek için istemci, bağlantıyı saran
bir katman kullanır. **Taşınan alan sayısı** ise yanıtlardaki skaler değerlerin toplamıdır;
bunun kaçının kullanıldığı **aşırı veri çekmeyi** (over-fetching) sayısallaştırır.

```js
// olc.mjs — uc bicemi ayni senaryoda olcer: istek sayisi, tel uzerindeki bayt, kullanilan alan orani
import { Agent, request } from "node:http";
import { connect } from "node:net";

let gonderilen = 0, alinan = 0, istekSayisi = 0;
const ajan = new Agent({ keepAlive: true, maxSockets: 1 });
ajan.createConnection = (secenek) => {                 // tel uzerindeki baytlari sayan baglanti
  const s = connect(secenek);
  s.on("data", (p) => { alinan += p.length; });
  const yaz = s.write.bind(s);
  s.write = (parca, ...k) => { gonderilen += Buffer.byteLength(parca); return yaz(parca, ...k); };
  return s;
};
const iste = (yol, govde) => new Promise((coz, hata) => {
  istekSayisi++;
  const r = request({ agent: ajan, host: "127.0.0.1", port: 8451, path: yol,
    method: govde ? "POST" : "GET", headers: govde ? { "Content-Type": "application/json" } : {} },
    (y) => { let m = ""; y.on("data", (p) => (m += p)); y.on("end", () => coz(JSON.parse(m))); });
  r.on("error", hata);
  r.end(govde ? JSON.stringify(govde) : undefined);
});

// Yanitlarda tasinan skaler alanlarin toplami
const yaprakSay = (d) => (typeof d !== "object" || d === null
  ? 1 : Object.values(d).reduce((t, v) => t + yaprakSay(v), 0));
const sifirla = () => { gonderilen = alinan = istekSayisi = 0; };
const rapor = (ad, tasinan, kullanilan, not) => console.log(
  `${ad.padEnd(15)} istek=${String(istekSayisi).padStart(2)}  ` +
  `gonderilen=${String(gonderilen).padStart(4)} B  alinan=${String(alinan).padStart(4)} B  ` +
  `tasinan alan=${String(tasinan).padStart(2)}  kullanim=` +
  (tasinan ? `%${Math.round((100 * kullanilan) / tasinan)}`.padStart(5) : "    -") +
  (not ? `\n${" ".repeat(16)}${not}` : ""));

const kaynakTemelli = async () => {
  sifirla();
  let tasinan = 0;
  const uye = await iste("/uyeler/1");               tasinan += yaprakSay(uye);
  const oduncler = await iste("/uyeler/1/oduncler"); tasinan += yaprakSay(oduncler);
  for (const o of oduncler) tasinan += yaprakSay(await iste(`/kitaplar/${o.isbn}`));
  return tasinan;
};

// --- Senaryo 1: uye adi + her odunc icin kitap adi ve son tarih (7 alan) ---
console.log("-- senaryo 1: uye adi, kitap adi, son tarih --");
rapor("kaynak temelli", await kaynakTemelli(), 7);
sifirla();
const rpc = await iste("/rpc", { yordam: "uyeOduncOzeti", parametre: { uyeId: 1 } });
rapor("uzak yordam", yaprakSay(rpc), 7, JSON.stringify(rpc.sonuc).slice(0, 78) + " ...");
sifirla();
const sonuc = await iste("/sorgu", { sorgu: "uye(1) { ad oduncler { sonTarih kitap { ad } } }" });
rapor("sorgu temelli", yaprakSay(sonuc), 7, JSON.stringify(sonuc.veri).slice(0, 78) + " ...");

// --- Senaryo 2: istemcinin gereksinimi degisti, yazar adi da isteniyor (10 alan) ---
console.log("\n-- senaryo 2: yanita yazar adi eklendi --");
rapor("kaynak temelli", await kaynakTemelli(), 10, "sunucu degismedi: yazar zaten tasiniyordu");
sifirla();
const rpc2 = await iste("/rpc", { yordam: "uyeOduncOzetiYazarli", parametre: { uyeId: 1 } });
rapor("uzak yordam", 0, 10, `sunucu yaniti: ${JSON.stringify(rpc2)} -> yeni yordam gerekiyor`);
sifirla();
const sonuc2 = await iste("/sorgu",
  { sorgu: "uye(1) { ad oduncler { sonTarih kitap { ad yazar } } }" });
rapor("sorgu temelli", yaprakSay(sonuc2), 10, "sunucu degismedi: sorguya yazar alani eklendi");
ajan.destroy();
```

```bash
node sunucu.mjs & p=$!
curl -s --retry 20 --retry-connrefused --retry-delay 0 -o /dev/null http://127.0.0.1:8451/uyeler/1
node olc.mjs
kill $p
```

```
uc bicem 127.0.0.1:8451
-- senaryo 1: uye adi, kitap adi, son tarih --
kaynak temelli  istek= 5  gonderilen= 414 B  alinan=1602 B  tasinan alan=47  kullanim=  %15
uzak yordam     istek= 1  gonderilen= 170 B  alinan= 361 B  tasinan alan= 7  kullanim= %100
                {"ad":"Ayse Kaya","oduncler":[{"sonTarih":"2024-05-15","kitapAdi":"Algoritmala ...
sorgu temelli   istek= 1  gonderilen= 182 B  alinan= 380 B  tasinan alan= 7  kullanim= %100
                {"uye":{"ad":"Ayse Kaya","oduncler":[{"sonTarih":"2024-05-15","kitap":{"ad":"A ...

-- senaryo 2: yanita yazar adi eklendi --
kaynak temelli  istek= 5  gonderilen= 414 B  alinan=1602 B  tasinan alan=47  kullanim=  %21
                sunucu degismedi: yazar zaten tasiniyordu
uzak yordam     istek= 1  gonderilen= 177 B  alinan= 161 B  tasinan alan= 0  kullanim=    -
                sunucu yaniti: {"hata":"bilinmeyen yordam"} -> yeni yordam gerekiyor
sorgu temelli   istek= 1  gonderilen= 188 B  alinan= 433 B  tasinan alan=10  kullanim= %100
                sunucu degismedi: sorguya yazar alani eklendi
```

Bayt sayıları başlıkları da içerdiği için çalışma zamanının varsayılan başlık kümesine
bağlıdır; alan sayıları ve istek sayıları ise veriye bağlıdır, makineden bağımsızdır.

#### Ölçümün Söyledikleri

**Eksik veri çekme** (under-fetching) kaynak temelli biçemde istek sayısı olarak görünür.
Üye kaydı bir istek, ödünç listesi bir istek, sonra her ödünç için bir kitap isteği: toplam
beş. Bu sayı sabit değildir, ödünç sayısına bağlıdır. Bir kayıt listesi getirip listedeki
her satır için ayrı istek yapma örüntüsü **N+1 sorunu** (N+1 problem) olarak anılır ve veri
büyüdükçe ağ turu sayısı da büyür.

**Fazla veri çekme** aynı biçemde alan sayısı olarak görünür. Kırk yedi alan taşınmış, yedisi
kullanılmıştır. Üyenin adresi, telefonu, ceza tutarı; kitabın özeti, yayınevi, raf numarası —
hiçbiri istenmemişti. Kaynak temelli biçemde kaydın tamamının dönmesi bir kusur değil, o
biçemin tanımıdır: adres bir **kayda** karşılık gelir, bir **görünüme** değil.

Diğer iki biçem her iki sorunu da tek istekte çözer: yedi alan taşınır, yedisi kullanılır.
Aradaki fark ikinci senaryoda ortaya çıkar.

#### Yanıtın Biçimini Kim Belirler

İkinci senaryoda gereksinim değişti: yazar adı da isteniyor. Üç biçem üç ayrı yerden
değişmek zorunda kaldı.

**Kaynak temelli biçemde hiçbir şey değişmedi.** Yazar adı zaten taşınıyordu; kullanım oranı
%15'ten %21'e çıktı, o kadar. Fazla veri çekmenin beklenmedik bir yararı budur: istemcinin
sonradan gereksinim duyduğu alan çoğu zaman elinde zaten vardır.

**Uzak yordam biçeminde sunucunun değişmesi gerekti.** İstemcinin çağırdığı yordam yoktur ve
sunucu bunu bildirir. Yeni alanı almak için ya var olan yordamın yanıtı genişletilir ya da
yeni bir yordam eklenir; iki durumda da sunucu yeniden yayımlanmadan istemci ilerleyemez.
Bu biçemde yanıtın biçimi sunucunun kararıdır, dolayısıyla biçim değişikliği de sunucunun
işidir.

**Sorgu temelli biçemde yalnız istemci değişti.** Seçim kümesine `yazar` eklendi, sunucu
aynı kaldı, taşınan alan yediden ona çıktı ve kullanım oranı %100'de kaldı. Yanıtın biçimini
istemci belirlediği için biçim değişikliği de istemcinin işidir.

Bu, biçemler arasındaki asıl ödünleşimdir ve maliyeti sunucu tarafına yansır. Sorgu temelli
bir arayüzde sunucu, istemcinin isteyebileceği her alan bileşimini karşılayabilmek zorundadır;
gelen sorgunun ne kadar veri erişimi doğuracağı önceden bilinmez. Uzak yordam biçeminde ise
sunucu her yanıtın ne getireceğini kesin bilir, çünkü biçimi kendisi sabitlemiştir.

#### Biçem Seçimi Neye Bağlıdır

Ölçüm bir biçemin diğerinden iyi olduğunu göstermez; her ölçünün bir sahibi olduğunu
gösterir.

Tüketici sayısı azsa ve gereksinimleri birbirine benziyorsa, sunucunun biçimi sabitlemesi
en az sürprizi üretir. Tüketiciler çoksa ve her biri farklı alan bileşimi istiyorsa,
biçimi istemciye bırakmak sunucuyu her yeni ekran için değişmekten kurtarır. Tüketici
tarafı önbelleklemeye dayanıyorsa, her kaydın kendi adresinin olması bir avantaja dönüşür:
aynı kitap ikinci kez istendiğinde ağa hiç çıkılmaz. Bu son nokta bir sonraki dersin
konusudur.

Bir de ölçüme girmeyen bir kalem vardır: **anlaşılabilirlik**. Kaynak temelli bir arayüzde
adresler kavram adlarıdır; sistemi tanımayan biri adres listesine bakarak alanı öğrenebilir.
Tek adresli biçemlerde bu bilgi gövdenin içindedir ve ancak sözleşme okunarak görülür.

#### Özet

- Üç biçem aynı işi karşılar ama yanıtın biçimini farklı taraf belirler: kaynak temelli
  biçemde kaydın kendisi, uzak yordam biçeminde sunucu, sorgu temelli biçemde istemci.
- Kaynak temelli biçemde ölçüm beş istek ve kırk yedi taşınan alan verdi; yedi alan
  kullanıldı. Eksik veri çekme istek sayısında, aşırı veri çekme kullanım oranında görünür.
- Ödünç sayısına bağlı olarak artan istek sayısı N+1 örüntüsüdür; liste getirip her satır
  için ayrı istek yapan her tasarımda ortaya çıkar.
- Gereksinim değiştiğinde uzak yordam biçeminde sunucu, sorgu temelli biçemde istemci
  değişmek zorunda kaldı; kaynak temelli biçemde ise fazla taşınan veri değişikliği gereksiz
  kıldı.
- Sorgu temelli biçemin bedeli sunucu tarafındadır: gelen sorgunun doğuracağı veri erişimi
  önceden bilinmez, oysa sabit biçimli yanıtın maliyeti bellidir.

#### Sonraki Adım

Kaynak temelli biçem ölçümde en çok bayt taşıyan ve en çok istek yapan seçenek oldu, ama en
yaygın kullanılan biçem odur. Bu çelişki, ölçüme girmeyen özelliklerden gelir: her kaydın
kendi adresinin olması aynı kaydın ikinci kez istendiğinde ağa hiç çıkılmamasını mümkün
kılar; her isteğin kendi kendine yeter olması isteğin hangi sunucu kopyasına düştüğünü
önemsiz kılar. Sonraki ders bu özellikleri bir ilkeler kümesi olarak adlandırır ve üçünü de
çalıştırarak sınar: aynı isteğin iki ayrı süreçte aynı yanıtı vermesi, tek biçim arayüzün
somut karşılığı ve önbelleklenebilirliğin taşınan bayt üzerindeki etkisi.

### REST İlkeleri

Source: https://academia.sh/tr/kurslar/api-tasarimi/rest-ilkeleri

Kaynak temelli biçemin arkasındaki kısıt kümesi: durumsuzluğun aynı isteği iki ayrı süreçte sınayarak gösterilmesi, tek biçim arayüzün kaynak adını bilmeyen bir istemciyle ölçülmesi, önbelleklenebilirliğin varlık etiketiyle taşınan bayta etkisi ve katmanlı sistemin bu kısıtlardan doğması.

Önceki ders kaynak temelli biçemi ölçtü ve en pahalı seçenek olarak buldu: beş istek, kırk
yedi taşınan alan, %15 kullanım. Buna karşın ödünç servislerinin çoğu bu biçemle yazılır.
Çelişki, ölçüme girmeyen özelliklerden gelir.

O özellikler bir kısıt kümesinin sonuçlarıdır. **REST** (representational state transfer),
bir protokol ya da bir kitaplık değil, bir arayüzün uyabileceği kısıtların adıdır. Bu ders
kısıtlardan üçünü — durumsuzluk, tek biçim arayüz ve önbelleklenebilirlik — çalıştırarak
sınar.

#### Kısıtlar ve Kaynak Yönelimi

REST'in temel birimi **kaynaktır** (resource): bir adla anılabilen her kavram. Kütüphane
servisinde kitap, üye, ödünç ve raf birer kaynaktır. İstemcinin gördüğü şey kaynağın kendisi
değil, bir **gösterimidir** (representation): o kaynağın belirli bir biçimdeki kopyası.
Aynı kaynağın JSON gösterimi de HTML gösterimi de olabilir; ikisi de aynı şeyi anlatır.

Zorunlu kısıtlar bu birimin üstüne kurulur:

- **İstemci–sunucu ayrımı:** iki taraf ayrı gelişir, aralarındaki tek bağ arayüzdür.
- **Durumsuzluk:** her istek kendi kendine yeter; sunucu istekler arasında istemciye ait
  bir bağlam tutmaz.
- **Önbelleklenebilirlik:** yanıt, saklanabilir olup olmadığını kendisi bildirir.
- **Tek biçim arayüz:** kaynaklar aynı yolla adlandırılır ve aynı yöntemlerle işlenir.
- **Katmanlı sistem:** istemci, karşısındaki tarafın son sunucu mu ara bir katman mı
  olduğunu bilmek zorunda değildir.

Altıncı bir kısıt daha vardır — **istek üzerine kod** (code on demand): sunucunun istemciye
çalıştırılabilir kod göndermesi. Kısıtlar arasında isteğe bağlı olan tek kısıt odur ve bu ders
onu ölçmez.

Aşağıdaki sunucu bu kısıtları hem uygulayan hem de ihlal eden iki tasarımı yan yana taşır;
bağlantı noktasını dışarıdan aldığı için iki kopyası birlikte çalıştırılabilir.

```js
// sunucu.mjs — ayni kodun iki kopyasi calistirilabilsin diye baglanti noktasi disaridan verilir:
// node sunucu.mjs 8461
import { createServer } from "node:http";
import { createHash, randomUUID } from "node:crypto";

const KAPI = Number(process.argv[2] ?? 8461);
const KAYNAKLAR = {
  kitaplar: [{ id: "978-0262033848", ad: "Algoritmalara Giris" }, { id: "978-0201896831", ad: "Programlama Sanati" },
             { id: "978-0131103627", ad: "C Programlama Dili" }, { id: "978-0596007126", ad: "Head First" }],
  uyeler:   [{ id: "u1", ad: "Ayse Kaya" }, { id: "u2", ad: "Baris Demir" }],
  oduncler: [{ id: "o11", uye: "u1", kitap: "978-0262033848" }],
  raflar:   [{ id: "R-12", kat: 2 }, { id: "R-03", kat: 1 }],   // sonradan eklenen kaynak
};
const imlecler = new Map();          // sunucu tarafinda tutulan durum: yalniz bu kopyada var

const json = (yanit, kod, govde, etiket) => {
  const metin = JSON.stringify(govde);
  yanit.setHeader("Content-Type", "application/json; charset=utf-8");
  yanit.setHeader("X-Surec", process.pid);
  if (etiket) yanit.setHeader("ETag", etiket);
  yanit.setHeader("Content-Length", Buffer.byteLength(metin));
  yanit.writeHead(kod).end(metin);
};
const etiketle = (govde) =>
  `"${createHash("sha256").update(JSON.stringify(govde)).digest("hex").slice(0, 16)}"`;

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");
  const [, ilk, ikinci] = adres.pathname.split("/");

  if (ilk === "") return json(yanit, 200, { kaynaklar: Object.keys(KAYNAKLAR) });

  // Durumlu sayfalama: imlec sunucuda tutulur, istek yalniz imlec kimligini tasir.
  if (ilk === "imlec" && istek.method === "POST") {
    const kimlik = randomUUID().slice(0, 8);
    imlecler.set(kimlik, 0);
    return json(yanit, 201, { imlec: kimlik });
  }
  if (ilk === "imlec" && istek.method === "GET") {
    if (!imlecler.has(ikinci)) return json(yanit, 404, { hata: "imlec_bilinmiyor" });
    const ofset = imlecler.get(ikinci);
    imlecler.set(ikinci, ofset + 2);
    return json(yanit, 200, { ofset, sayfa: KAYNAKLAR.kitaplar.slice(ofset, ofset + 2) });
  }

  // Tek bicim arayuz: her kaynak ayni yolla adlandirilir, ayni yontemlerle okunur.
  const kume = KAYNAKLAR[ilk];
  if (!kume) return json(yanit, 404, { hata: "kaynak_yok" });
  if (ikinci) {
    const oge = kume.find((k) => k.id === ikinci);
    if (!oge) return json(yanit, 404, { hata: "bulunamadi" });
    const etiket = etiketle(oge);
    if (istek.headers["if-none-match"] === etiket) {         // kosullu istek: govde gonderilmez
      yanit.setHeader("ETag", etiket);
      yanit.setHeader("X-Surec", process.pid);
      return yanit.writeHead(304).end();
    }
    return json(yanit, 200, oge, etiket);
  }
  // Durumsuz sayfalama: sayfa bilgisi istegin icindedir.
  const ofset = Number(adres.searchParams.get("ofset") ?? 0);
  const adet = Number(adres.searchParams.get("adet") ?? kume.length);
  return json(yanit, 200, { ofset, adet, oge: kume.slice(ofset, ofset + adet) });
}).listen(KAPI, "127.0.0.1", () => console.log(`kopya :${KAPI} pid=${process.pid}`));
```

Ölçüm istemcisi üç bölümden oluşur ve istekleri iki kopya arasında dönüşümlü dağıtır.

```js
// olc.mjs — uc ilkeyi sinar: durumsuzluk, tek bicim arayuz, onbelleklenebilirlik
import { Agent, request } from "node:http";
import { connect } from "node:net";

let alinan = 0;
const ajan = new Agent({ keepAlive: true });
ajan.createConnection = (s) => { const b = connect(s); b.on("data", (p) => (alinan += p.length)); return b; };
const iste = (kapi, yol, secenek = {}) => new Promise((coz, hata) => {
  const r = request({ agent: ajan, host: "127.0.0.1", port: kapi, path: yol,
    method: secenek.yontem ?? "GET", headers: secenek.basliklar ?? {} }, (y) => {
    let m = ""; y.on("data", (p) => (m += p));
    y.on("end", () => coz({ kod: y.statusCode, etiket: y.headers.etag, surec: y.headers["x-surec"],
                            govde: m ? JSON.parse(m) : null }));
  });
  r.on("error", hata); r.end();
});

// 1) DURUMSUZLUK — ayni istek iki ayri surecte ayni yaniti veriyor mu?
console.log("-- 1) durumsuzluk: istekler iki kopya arasinda donusumlu dagitiliyor --");
const { govde: kur } = await iste(8461, "/imlec", { yontem: "POST" });
for (const kapi of [8461, 8462, 8461]) {
  const y = await iste(kapi, `/imlec/${kur.imlec}`);
  console.log(`  durumlu   :${kapi} pid=${y.surec} -> ${y.kod} ${JSON.stringify(y.govde).slice(0, 62)}`);
}
for (const kapi of [8461, 8462, 8461]) {
  const y = await iste(kapi, "/kitaplar?ofset=2&adet=2");
  console.log(`  durumsuz  :${kapi} pid=${y.surec} -> ${y.kod} ${JSON.stringify(y.govde).slice(0, 62)}`);
}

// 2) TEK BICIM ARAYUZ — ayni istemci kodu, kaynak adini bilmeden her kaynagi okuyor mu?
console.log("\n-- 2) tek bicim arayuz: kaynak adlari sunucudan ogreniliyor --");
const { govde: kok } = await iste(8461, "/");
for (const ad of kok.kaynaklar) {
  const liste = await iste(8461, `/${ad}`);
  const ilk = liste.govde.oge[0];
  const tek = await iste(8461, `/${ad}/${ilk.id}`);
  console.log(`  ${ad.padEnd(9)} liste=${liste.kod} adet=${liste.govde.oge.length}` +
              `  tek=${tek.kod} etiket=${tek.etiket}`);
}
const yok = await iste(8461, "/subeler");
console.log(`  subeler   liste=${yok.kod} ${JSON.stringify(yok.govde)}`);

// 3) ONBELLEKLENEBILIRLIK — kosullu istek tasinan bayti ne kadar dusuruyor?
console.log("\n-- 3) onbelleklenebilirlik: ayni kayit on kez isteniyor --");
const YOL = "/kitaplar/978-0262033848";
alinan = 0;
for (let i = 0; i < 10; i++) await iste(8461, YOL);
const kosulsuz = alinan;
alinan = 0;
const ilk = await iste(8461, YOL);
for (let i = 0; i < 9; i++) await iste(8461, YOL, { basliklar: { "If-None-Match": ilk.etiket } });
const kosullu = alinan;
const son = await iste(8461, YOL, { basliklar: { "If-None-Match": ilk.etiket } });
console.log(`  kosulsuz  10 istek -> ${kosulsuz} B`);
console.log(`  kosullu   10 istek -> ${kosullu} B  (son yanit ${son.kod}, kazanc %${
  Math.round((100 * (kosulsuz - kosullu)) / kosulsuz)})`);
ajan.destroy();
```

```bash
node sunucu.mjs 8461 & a=$!
node sunucu.mjs 8462 & b=$!
for k in 8461 8462; do
  curl -s --retry 20 --retry-connrefused --retry-delay 0 -o /dev/null "http://127.0.0.1:$k/"
done
node olc.mjs
kill $a $b
```

```
kopya :8461 pid=8582
kopya :8462 pid=8583
-- 1) durumsuzluk: istekler iki kopya arasinda donusumlu dagitiliyor --
  durumlu   :8461 pid=8582 -> 200 {"ofset":0,"sayfa":[{"id":"978-0262033848","ad":"Algoritmalara
  durumlu   :8462 pid=8583 -> 404 {"hata":"imlec_bilinmiyor"}
  durumlu   :8461 pid=8582 -> 200 {"ofset":2,"sayfa":[{"id":"978-0131103627","ad":"C Programlama
  durumsuz  :8461 pid=8582 -> 200 {"ofset":2,"adet":2,"oge":[{"id":"978-0131103627","ad":"C Prog
  durumsuz  :8462 pid=8583 -> 200 {"ofset":2,"adet":2,"oge":[{"id":"978-0131103627","ad":"C Prog
  durumsuz  :8461 pid=8582 -> 200 {"ofset":2,"adet":2,"oge":[{"id":"978-0131103627","ad":"C Prog

-- 2) tek bicim arayuz: kaynak adlari sunucudan ogreniliyor --
  kitaplar  liste=200 adet=4  tek=200 etiket="9ad1f08038f3c1fa"
  uyeler    liste=200 adet=2  tek=200 etiket="5d5092c2e89cc21d"
  oduncler  liste=200 adet=1  tek=200 etiket="de830fee13237e05"
  raflar    liste=200 adet=2  tek=200 etiket="8703c0e7db6c6311"
  subeler   liste=404 {"hata":"kaynak_yok"}

-- 3) onbelleklenebilirlik: ayni kayit on kez isteniyor --
  kosulsuz  10 istek -> 2240 B
  kosullu   10 istek -> 1277 B  (son yanit 304, kazanc %43)
```

Süreç kimlikleri her çalıştırmada değişir. Kimlik `X-Surec` başlığında taşındığı için
basamak sayısı toplam baytı da birkaç bayt kaydırır; oran ise değişmez. Bayt sayıları
ayrıca çalışma zamanının yazdığı başlık kümesine bağlıdır.

#### Durumsuzluk: İsteğin Kendi Kendine Yetmesi

İlk bölüm iki sayfalama tasarımını aynı koşulda sınar: istekler iki sunucu kopyası arasında
dönüşümlü dağıtılır. Bu, önceki kursta kurulan vekil arkasındaki çoklu kopya kurulumunun
küçük bir örneğidir.

**Durumlu tasarımda** imleç sunucunun belleğinde durur. İlk istek 8461'e düşer ve çalışır.
İkinci istek 8462'ye düştüğünde aynı imleç orada bulunmadığı için `404` alınır. Üçüncü istek
yine 8461'e düştüğünde ikinci sayfa gelir — yani ikinci kopyanın verdiği hata, imlecin
durumunu da bozmuştur: istemci bir sayfayı hiç görmeden atlamıştır.

**Durumsuz tasarımda** sayfa bilgisi isteğin içindedir. Aynı istek üç kez, iki ayrı sürece
gitmiş ve üçünde de aynı gövdeyi vermiştir. Sunucu kimliğinin yanıt üzerinde hiçbir etkisi
yoktur.

Durumsuzluğun bedeli her istekte bağlamın yeniden gönderilmesidir: kimlik bilgisi,
sayfalama konumu, süzme ölçütleri her seferinde taşınır. Karşılığında elde edilen şey,
sunucu kopyalarının birbirinin yerine geçebilmesidir. Bir kopya düştüğünde istek başka bir
kopyaya yönlendirilebilir, kopya sayısı yük altında artırılabilir ve hiçbiri istemcinin
oturumunu taşımak zorunda kalmaz.

#### Tek Biçim Arayüz: Aynı İstemci Kodu, Dört Kaynak

İkinci bölümdeki istemci hiçbir kaynak adını bilmez. Kök adresten kaynak listesini okur,
sonra her kaynak için aynı iki isteği yapar: koleksiyonu al, ilk ögenin kimliğiyle tek
kaydı al. Dördü de aynı kodla okunmuştur; sonradan eklenen `raflar` kaynağı için istemcide
tek satır değişmemiştir.

Tek biçim arayüzün bu ölçümdeki üç somut karşılığı budur: **kaynaklar adreslerle
adlandırılır**, **aynı yöntemler her kaynakta aynı anlama gelir** ve **yanıtlar kendini
betimler** — gövdenin nasıl
okunacağı içerik türü başlığından, kaydın sürümü varlık etiketinden anlaşılır. Var olmayan
bir kaynak da aynı biçimde, `404` ile karşılanır.

Bunun bedeli esneklik kaybıdır. Bir işlem tek bir kaynağa oturmuyorsa — "şu üç kitabı aynı
anda uzat ve gecikme cezasını yeniden hesapla" gibi — tek biçim arayüz o işlemi doğal
karşılamaz. Bu tür işlemler ya kendileri birer kaynağa dönüştürülür ya da arayüz bu noktada
uzak yordam çağrısına yaklaşır.

#### Önbelleklenebilirlik: Aynı Kaydı On Kez İstemek

Üçüncü bölüm aynı kitabı on kez ister. Bu çalıştırmada koşulsuz istekler 2240 bayt taşır. İkinci turda
istemci ilk yanıttaki varlık etiketini saklar ve sonraki dokuz isteğe `If-None-Match`
başlığıyla ekler; sunucu kaydın değişmediğini görüp gövdesiz `304` döndürür. Taşınan bayt
1277'ye iner — %43 azalma.

Varlık etiketi Statik Dosya Sunumu dersinde tanıtılmıştı; buradaki fark, aynı düzeneğin
hesaplanmış bir kayda uygulanmasıdır. Kazancın kaynağı gövdenin gönderilmemesidir; başlıklar
yine taşınır, bu yüzden azalma %100 olamaz. Kayıt büyüdükçe oran yükselir: bu ölçümdeki
kayıt yalnız iki alan taşıyor.

Önbelleklenebilirliğin durumsuzlukla bağı doğrudandır. Bir yanıtın saklanabilmesi, o yanıtın
yalnız isteğin içeriğine bağlı olmasını gerektirir. Sunucuda tutulan bir imlece bağlı yanıt
saklanamaz, çünkü aynı adres iki farklı sonuç verebilir.

#### Katmanlı Sistem

Üç ölçümün hepsi bir dördüncü kısıtı da mümkün kılar. Yanıt yalnız isteğe bağlıysa ve
saklanabilir olduğunu kendisi bildiriyorsa, istemci ile sunucu arasına bir katman
konulabilir: bir ters vekil, bir önbellek ya da bir yönlendirici. İstemci karşısındakinin
son sunucu mu ara katman mı olduğunu bilmez ve bilmesi gerekmez.

Önceki kursun Web Sunucusu Kavramı dersinde kurulan ters vekil tam olarak budur. O ders
vekilin bilgi kaybı da getirdiğini göstermişti; buradaki ek gözlem şudur: vekilin araya
girebilmesi, arayüzün durumsuz ve önbelleklenebilir olmasına dayanır. Sunucuda oturum
tutan bir tasarımda vekil, isteği herhangi bir kopyaya değil, oturumun bulunduğu kopyaya
göndermek zorunda kalır.

#### Özet

- REST bir protokol değil, kaynak yönelimli bir arayüzün uyduğu kısıtlar kümesidir; temel
  birim kaynak, taşınan şey ise kaynağın gösterimidir.
- Durumsuzluk ölçümde doğrudan görüldü: sunucuda tutulan imleç ikinci kopyada bulunamadı ve
  bir sayfa atlandı; istek içinde taşınan sayfa bilgisi iki kopyada da aynı yanıtı verdi.
- Tek biçim arayüz, kaynak adını bilmeyen tek bir istemci kodunun dört kaynağı da okuyabilmesi
  demektir; sonradan eklenen kaynak istemcide değişiklik gerektirmedi.
- Koşullu istek aynı kaydın on kez alınmasında taşınan baytı yaklaşık %43 düşürdü; kazanç
  gövdenin gönderilmemesinden gelir, başlıklar taşınmaya devam ettiği için tam olmaz.
- Katmanlı sistem bu iki kısıta dayanır: yanıt yalnız isteğe bağlı ve saklanabilir olduğunda
  araya vekil ya da önbellek konulabilir.

#### Sonraki Adım

REST'in kısıtları arayüzü ağın kurallarına yaslar: adresler, yöntemler, durum kodları,
başlıklar. Bunun karşılığında sözleşme metin biçiminde ve gevşek kalır — bir alanın tipi ya
da bir yöntemin imzası, ancak belgeye bakılarak bilinir. Bunun tersini yapan bir yaklaşım
vardır: sözleşmeyi önce ayrı bir şema dosyasında yazmak, istemci ve sunucu tarafını o
şemadan üretmek ve teli metin yerine ikili biçimde kullanmak. Sonraki ders bu yaklaşımı
kurar: bir şema dosyası yazar, ondan bir doğrulayıcı üretir, kendi ikili çerçevelemesini
yazar ve aynı kaydın ikili ile metin biçimindeki bayt sayısını karşılaştırır.

### Uzak Yordam Çağrısı ve gRPC

Source: https://academia.sh/tr/kurslar/api-tasarimi/uzak-yordam-cagrisi-ve-grpc

Sözleşmeyi önce yazan yaklaşım: şema dosyasından üretilen doğrulayıcı, alan numarasının tel üzerindeki kimlik olması, kendi ikili çerçevelemesiyle metin biçiminin bayt karşılaştırması ve gRPC'nin bu tasarımı akış türleriyle birlikte getirmesi.

Önceki ders REST'in kısıtlarını sınadı. O kısıtların ortak yanı, sözleşmeyi ağın kurallarına
yaslamalarıdır: adresler, yöntemler, durum kodları. Sözleşmenin ayrıntısı — bir alanın tipi,
bir alanın zorunlu olup olmadığı — telin dışında, belgede kalır.

Bunun tersini yapan bir yaklaşım vardır. **Uzak yordam çağrısı** (remote procedure call),
ağ üzerindeki bir işlemi yerel bir fonksiyon çağrısı gibi gösterir: istemci `KitapGetir`
diye bir işlev çağırır, çağrı ağa iner, karşı tarafta çalışır ve sonuç döner. Bu gösterimin
işleyebilmesi için iki tarafın çağrının imzası üzerinde anlaşması gerekir ve bu anlaşma
belgede değil, makine okunur bir **şemada** durur.

#### Yordam Çağrısı Benzetmesinin Sınırı

Benzetme yararlıdır ama tam değildir ve nerede bozulduğunu bilmek arayüz tasarımını
belirler. Yerel bir çağrı ölçülemeyecek kadar hızlıdır; uzak çağrı en iyi durumda bir ağ
turu sürer. Yerel bir çağrı ya çalışır ya da bir istisna atar; uzak çağrı üçüncü bir sonuç
üretebilir — **kısmi başarısızlık**: isteğin gidip gitmediği, işin yapılıp yapılmadığı
bilinmez. Yerel çağrıda argümanlar bellekteki nesnelerdir; uzak çağrıda bayt dizisine
çevrilmeleri gerekir ve bu çevirinin kuralları sözleşmenin kendisidir.

Bu üç fark, uzak yordam çağrısı biçemini kullanan arayüzlerin tasarımına yansır: çağrılar
kaba taneli tutulur (çok sayıda küçük çağrı yerine az sayıda dolu çağrı), yeniden deneme
davranışı açıkça tanımlanır ve veri biçimi sıkı biçimde belirlenir.

#### Sözleşme Önce Yazılır

Şema dosyası arayüzün tek doğruluk kaynağıdır. Aşağıdaki dosya bir mesaj tipi ile bir servis
tanımlar; her alanın bir **numarası**, bir tipi, bir adı ve bir zorunluluk durumu vardır.

```
mesaj Kitap {
  1 dizgi    isbn      zorunlu
  2 dizgi    ad        zorunlu
  3 tamsayi  sayfa     istege-bagli
  4 mantiksal oduncte  zorunlu
}
servis Katalog {
  KitapGetir(KitapIstek) -> Kitap
  KitapAra(AramaIstek) -> akis Kitap
}
```

Bu dosyadan iki şey üretilir: alanların varlığını ve tipini denetleyen bir **doğrulayıcı**
ve nesneyi bayt dizisine çeviren bir **kodlayıcı**. Kodlayıcının biçimi, alan adlarını tele
hiç çıkarmaz; her alan kendi numarasıyla anılır ve sayılar değişken uzunlukta yazılır.

```js
// uret.mjs — sema dosyasindan dogrulayici ve ikili kodlayici uretir (sozlesme onceliklidir).
import { readFileSync } from "node:fs";

export const semaOku = (dosya) => {
  const mesajlar = {}, servisler = {};
  let acik = null;
  for (const ham of readFileSync(dosya, "utf8").split("\n")) {
    const satir = ham.trim();
    if (!satir) continue;
    let e;
    if ((e = /^mesaj (\w+) \{$/.exec(satir))) { acik = mesajlar[e[1]] = []; continue; }
    if ((e = /^servis (\w+) \{$/.exec(satir))) { acik = servisler[e[1]] = []; continue; }
    if (satir === "}") { acik = null; continue; }
    if ((e = /^(\d+) (\w+)\s+(\w+)\s+(\S+)$/.exec(satir)))
      acik.push({ numara: +e[1], tip: e[2], ad: e[3], zorunlu: e[4] === "zorunlu" });
    else if ((e = /^(\w+)\((\w+)\) -> (akis )?(\w+)$/.exec(satir)))
      acik.push({ yordam: e[1], girdi: e[2], akis: Boolean(e[3]), cikti: e[4] });
  }
  return { mesajlar, servisler };
};

const TIPLER = { dizgi: "string", tamsayi: "number", mantiksal: "boolean" };

// Uretilmis dogrulayici: alanlarin varligini ve tipini semaya gore denetler.
export const dogrulayiciUret = (alanlar) => (nesne) => {
  const kusur = [];
  for (const a of alanlar) {
    const v = nesne[a.ad];
    if (v === undefined) { if (a.zorunlu) kusur.push(`${a.ad}: zorunlu alan eksik`); continue; }
    if (typeof v !== TIPLER[a.tip]) kusur.push(`${a.ad}: ${a.tip} bekleniyordu, ${typeof v} geldi`);
  }
  for (const ad of Object.keys(nesne))
    if (!alanlar.some((a) => a.ad === ad)) kusur.push(`${ad}: semada yok`);
  return kusur;
};

// Degisken uzunluklu tam sayi: kucuk sayilar az bayt tutar.
const varintYaz = (n) => {
  const b = [];
  do { let p = n & 0x7f; n >>>= 7; b.push(n ? p | 0x80 : p); } while (n);
  return Buffer.from(b);
};
const varintOku = (b, i) => {
  let n = 0, k = 0;
  while (b[i] & 0x80) { n |= (b[i++] & 0x7f) << k; k += 7; }
  return [n | (b[i++] << k), i];
};

// Ikili cerceveleme: her alan (numara<<3 | tur) anahtariyla yazilir; alan adi tele hic cikmaz.
export const kodlayiciUret = (alanlar) => ({
  kodla(nesne) {
    const parcalar = [];
    for (const a of alanlar) {
      const v = nesne[a.ad];
      if (v === undefined) continue;
      const tur = a.tip === "dizgi" ? 2 : 0;
      parcalar.push(varintYaz((a.numara << 3) | tur));
      if (tur === 0) parcalar.push(varintYaz(a.tip === "mantiksal" ? (v ? 1 : 0) : v));
      else { const g = Buffer.from(v, "utf8"); parcalar.push(varintYaz(g.length), g); }
    }
    return Buffer.concat(parcalar);
  },
  cozumle(bayt) {
    const nesne = {};
    let i = 0;
    while (i < bayt.length) {
      let anahtar; [anahtar, i] = varintOku(bayt, i);
      const alan = alanlar.find((a) => a.numara === anahtar >> 3);
      if ((anahtar & 7) === 2) {
        let uzunluk; [uzunluk, i] = varintOku(bayt, i);
        nesne[alan?.ad ?? `bilinmeyen_${anahtar >> 3}`] = bayt.toString("utf8", i, i + uzunluk);
        i += uzunluk;
      } else {
        let sayi; [sayi, i] = varintOku(bayt, i);
        nesne[alan?.ad ?? `bilinmeyen_${anahtar >> 3}`] = alan?.tip === "mantiksal" ? Boolean(sayi) : sayi;
      }
    }
    return nesne;
  },
});
```

#### Ölçüm

Ölçüm dört soruyu yanıtlar: şemadan hangi yüzey üretilir, doğrulayıcı hangi kayıtları
reddeder, ikili biçim ne kadar bayt tutar ve alan adı değiştiğinde tel üzerindeki baytlara
ne olur?

```js
// olc.mjs — sema oncelikli sozlesmeyi sinar: dogrulama, ikili/metin bayt karsilastirmasi, alan adi degisimi
import { writeFileSync } from "node:fs";
import { semaOku, dogrulayiciUret, kodlayiciUret } from "./uret.mjs";

const { mesajlar, servisler } = semaOku("kutuphane.sema");
const alanlar = mesajlar.Kitap;
const dogrula = dogrulayiciUret(alanlar);
const { kodla, cozumle } = kodlayiciUret(alanlar);

console.log("-- 1) semadan uretilen servis yuzeyi --");
for (const y of servisler.Katalog)
  console.log(`  ${y.yordam}(${y.girdi}) -> ${y.akis ? "akis " : ""}${y.cikti}`);

console.log("\n-- 2) uretilmis dogrulayici --");
const ORNEKLER = [
  { isbn: "978-0262033848", ad: "Algoritmalara Giris", sayfa: 1312, oduncte: false },
  { isbn: "978-0201896831", ad: "Programlama Sanati", oduncte: true },   // istege bagli alan yok
  { isbn: "978-0131103627", oduncte: true },                             // zorunlu alan eksik
  { isbn: "978-0596007126", ad: "Head First", sayfa: "272", oduncte: true },  // tip yanlis
  { isbn: "978-0000000000", ad: "Deneme", oduncte: true, raf: "R-12" },  // semada olmayan alan
];
for (const o of ORNEKLER) {
  const kusur = dogrula(o);
  console.log(`  ${JSON.stringify(o).slice(0, 52).padEnd(54)} ${kusur.length ? "RED  " + kusur[0] : "GECER"}`);
}

console.log("\n-- 3) ikili cerceveleme ile metin biciminin bayt sayisi --");
const gecerli = ORNEKLER.filter((o) => dogrula(o).length === 0);
for (const o of gecerli) {
  const ikili = kodla(o), metin = Buffer.from(JSON.stringify(o), "utf8");
  console.log(`  ${o.isbn}  ikili=${String(ikili.length).padStart(3)} B  ` +
    `metin=${String(metin.length).padStart(3)} B  kazanc=%${Math.round(100 * (1 - ikili.length / metin.length))}`);
}
const toplamIkili = gecerli.reduce((t, o) => t + kodla(o).length, 0);
const toplamMetin = Buffer.byteLength(JSON.stringify(gecerli));
console.log(`  toplam       ikili=${toplamIkili} B  metin=${toplamMetin} B  ` +
  `kazanc=%${Math.round(100 * (1 - toplamIkili / toplamMetin))}`);

console.log("\n-- 4) tel uzerinde alan adi yok: sema alan adini degistirir --");
const bayt = kodla(gecerli[0]);
console.log(`  eski semayla cozumlenen : ${JSON.stringify(cozumle(bayt))}`);
writeFileSync("yeni.sema", `mesaj Kitap {
  1 dizgi    isbn      zorunlu
  2 dizgi    baslik    zorunlu
  3 tamsayi  sayfa     istege-bagli
  4 mantiksal oduncte  zorunlu
}
`);
const yeni = kodlayiciUret(semaOku("yeni.sema").mesajlar.Kitap);
console.log(`  yeni semayla cozumlenen : ${JSON.stringify(yeni.cozumle(bayt))}`);
console.log(`  ayni baytlar, ${bayt.length} B; degisen tek sey alan numarasinin adi.`);
```

```
-- 1) semadan uretilen servis yuzeyi --
  KitapGetir(KitapIstek) -> Kitap
  KitapAra(AramaIstek) -> akis Kitap

-- 2) uretilmis dogrulayici --
  {"isbn":"978-0262033848","ad":"Algoritmalara Giris",   GECER
  {"isbn":"978-0201896831","ad":"Programlama Sanati","   GECER
  {"isbn":"978-0131103627","oduncte":true}               RED  ad: zorunlu alan eksik
  {"isbn":"978-0596007126","ad":"Head First","sayfa":"   RED  sayfa: tamsayi bekleniyordu, string geldi
  {"isbn":"978-0000000000","ad":"Deneme","oduncte":tru   RED  raf: semada yok

-- 3) ikili cerceveleme ile metin biciminin bayt sayisi --
  978-0262033848  ikili= 42 B  metin= 81 B  kazanc=%48
  978-0201896831  ikili= 38 B  metin= 66 B  kazanc=%42
  toplam       ikili=80 B  metin=150 B  kazanc=%47

-- 4) tel uzerinde alan adi yok: sema alan adini degistirir --
  eski semayla cozumlenen : {"isbn":"978-0262033848","ad":"Algoritmalara Giris","sayfa":1312,"oduncte":false}
  yeni semayla cozumlenen : {"isbn":"978-0262033848","baslik":"Algoritmalara Giris","sayfa":1312,"oduncte":false}
  ayni baytlar, 42 B; degisen tek sey alan numarasinin adi.
```

#### Ölçümün Söyledikleri

**Doğrulama sözleşmeden üretilir, elle yazılmaz.** Beş kaydın üçü reddedildi ve üç ret üç
ayrı türdendir: eksik zorunlu alan, yanlış tip, şemada bulunmayan alan. Bu denetimlerin
hiçbiri uygulama kodunda yazılmamıştır; şema dosyasındaki satırlardan çıkmıştır. Şema
değiştiğinde doğrulama da değişir ve iki tarafın kuralları ayrışamaz.

**İkili biçim aynı kaydı yaklaşık yarı bayta indirir.** Kazancın kaynağı sıkıştırma değil,
gereksiz olanın hiç yazılmamasıdır: alan adları, tırnak işaretleri, iki nokta ve virgüller
tele çıkmaz. Sayılar da metin olarak değil, değişken uzunluklu tam sayı olarak yazılır —
`1312` dört karakter yerine iki bayt tutar.

**Alan numarası kimliktir, alan adı değil.** Dördüncü bölüm, birinci derste kırıcı sayılan
bir değişikliği tekrarlar: `ad` alanı `baslik` olarak yeniden adlandırılır. Metin biçiminde
bu değişiklik tüketiciyi kırmıştı. İkili biçimde aynı 42 baytlık dizi, yeni şemayla
sorunsuz çözümlenir; yalnız çıktıdaki anahtar değişir. Tel üzerindeki kimlik `2` sayısıdır.

Bunun aynadaki karşılığı da doğrudur ve daha tehlikelidir: **numara değiştirmek sessizce
kırar.** İki alanın numaraları takas edilirse doğrulayıcı hiçbir şey söylemez, çünkü baytlar
geçerlidir; yalnız değerler yanlış alanlara yerleşir. Şema öncelikli tasarımın değişmez
kuralı buradan gelir: bir alan numarası bir kez verildikten sonra başka bir alana verilmez,
kaldırılan numara yeniden kullanılmaz.

#### gRPC'nin Getirdikleri

Bu dersteki şema, doğrulayıcı ve çerçeveleme, bu tasarımın küçük bir örneğidir. **gRPC**
aynı yapıyı hazır bir bütün olarak sunar: mesajlar **Protocol Buffers** şemasıyla tanımlanır,
istemci ve sunucu tarafı bu şemadan üretilir, taşıma katmanı olarak HTTP/2 kullanılır.

HTTP/2'nin seçilmesi bir ayrıntı değil, dört çağrı türünü mümkün kılan şeydir. Şema
dosyasındaki `akis` işareti bunu gösterir: bir çağrı tek istek–tek yanıt olabileceği gibi,
sunucu tarafı akış (bir istek, çok yanıt), istemci tarafı akış (çok istek, tek yanıt) ya da
çift yönlü akış olabilir. Kütüphane servisinde "raf sayımını akış olarak gönder" ya da
"katalogda eşleşenleri geldikçe ver" gibi işler bu türlere oturur.

Bedeli de bellidir. Tel üzerindeki içerik gözle okunamaz; bir isteği incelemek için şemayı
bilen bir araç gerekir. Tarayıcı bu protokolü doğrudan konuşamaz, araya bir çeviri katmanı
girer. Ve arayüzü kullanmak için karşı tarafın şemasına erişmek zorunludur — REST'te bir
adrese istek atıp yanıtı okumak yeterken, burada sözleşme olmadan tek bayt anlaşılmaz.

Bu bedeller, gRPC'nin en çok nerede kullanıldığını da açıklar: sistemin kendi bileşenleri
arasında, iki tarafın da aynı ekipçe yönetildiği ve çağrı sayısının yüksek olduğu yerlerde.

#### Özet

- Uzak yordam çağrısı, ağ üzerindeki işlemi yerel bir çağrı gibi gösterir; benzetme gecikme,
  kısmi başarısızlık ve bayta çevirme noktalarında bozulur ve tasarımı bu noktalar belirler.
- Şema öncelikli tasarımda sözleşme makine okunur bir dosyadır; doğrulayıcı ve kodlayıcı
  ondan üretilir, böylece iki tarafın kuralları ayrışamaz.
- Ölçümde ikili çerçeveleme aynı kayıtları metin biçimine göre yaklaşık yarı bayta indirdi;
  kazanç alan adlarının ve ayraçların tele hiç çıkmamasından gelir.
- Tel üzerindeki kimlik alan numarasıdır: alan adı değiştiğinde aynı baytlar sorunsuz
  çözümlenir, ama numara değiştiğinde hata sessizce yanlış değer olarak görünür.
- gRPC bu tasarımı Protocol Buffers şeması ve HTTP/2 taşımasıyla birleştirir; dört çağrı
  türünü mümkün kılar, karşılığında okunabilirlik ve tarayıcıdan doğrudan erişim kaybolur.

#### Sonraki Adım

Şema öncelikli tasarım yanıtın biçimini sözleşmede sabitler ve bu, ikinci derste ölçülen
soruna geri döner: istemcinin gereksinimi değiştiğinde sunucu değişmek zorundadır. Bir de
bunun tam tersini yapan yaklaşım vardır — biçimi istemcinin belirlediği, ama bunu gevşek bir
seçim metniyle değil, sunucunun yayımladığı bir tip sistemi içinde yaptığı bir yaklaşım.
Sonraki ders bu yaklaşımı kurar: tek uç nokta, tip sistemiyle sınırlanmış istemci güdümlü
sorgu, alan başına çözümleme ve kısmen başarılı yanıt kavramı.

### GraphQL

Source: https://academia.sh/tr/kurslar/api-tasarimi/graphql

Tek uç nokta üzerinden istemci güdümlü sorgu: sunucunun yayımladığı tip sistemi, sorgunun yürütülmeden önce şemaya göre doğrulanması, alan başına çözümleme ve bir alanın hata vermesiyle oluşan kısmen başarılı yanıt; değişkenlerle aynı belgenin yeniden kullanımı.

Önceki ders sözleşmeyi şemada sabitleyen yaklaşımı kurdu ve bir eksiğini bıraktı: yanıtın
biçimi sunucunun kararı olduğu için, istemcinin gereksinimi değiştiğinde sunucu değişmek
zorundadır. İkinci derste ölçülen sorun buydu.

**GraphQL** bunun tersini yapar. Yanıtın biçimini istemci belirler, ama bunu serbest bir
metinle değil, sunucunun yayımladığı bir **tip sistemi** (type system) içinde yapar. Ortada
tek bir uç nokta vardır; ne istendiği adreste değil, gövdedeki **sorgu belgesinde** (query
document) yazar. Bu ders o düzeneği küçük bir işleyici yazarak kurar.

#### Tip Sistemi Sunucunun Yayımladığı Yüzeydir

Tip sistemi, hangi alanların bulunduğunu, her alanın hangi tipte olduğunu ve hangi alanların
alt seçim gerektirdiğini söyler. Her alanın verisini nasıl bulacağını bilen işleve **çözücü**
(resolver) denir; bir alanın çözücüsü yoksa değer kaynak nesneden okunur.

```js
// sema.mjs — tip sistemi ve cozuculer. Sunucunun yayimladigi yuzeyin tamami buradadir.
export const TIPLER = {
  Sorgu: { uye: { tip: "Uye", arg: "id" }, kitap: { tip: "Kitap", arg: "isbn" } },
  Uye:   { id: "ID", ad: "Dizgi", eposta: "Dizgi", ceza: "Sayi", oduncler: { tip: "Odunc", liste: true } },
  Odunc: { id: "ID", sonTarih: "Dizgi", gecikmeGunu: "Sayi", kitap: { tip: "Kitap" } },
  Kitap: { isbn: "ID", ad: "Dizgi", yazar: "Dizgi", sayfa: "Sayi" },
};

const UYELER = [{ id: "u1", ad: "Ayse Kaya", eposta: "ayse@ornek.test", ceza: 0 }];
const KITAPLAR = {
  "978-0262033848": { isbn: "978-0262033848", ad: "Algoritmalara Giris", yazar: "Cormen", sayfa: 1312 },
  "978-0201896831": { isbn: "978-0201896831", ad: "Programlama Sanati", yazar: "Knuth", sayfa: 650 },
};
const ODUNCLER = [{ id: "o11", uye: "u1", isbn: "978-0262033848", sonTarih: "2024-05-15" },
                  { id: "o12", uye: "u1", isbn: "978-0201896831", sonTarih: "2024-05-17" }];

// Cozucu: her alan kendi verisini kendi getirir. Bir alanin hata vermesi digerlerini durdurmaz.
export const COZUCULER = {
  "Sorgu.uye": (_, arg) => UYELER.find((u) => u.id === arg),
  "Sorgu.kitap": (_, arg) => KITAPLAR[arg],
  "Uye.oduncler": (uye) => ODUNCLER.filter((o) => o.uye === uye.id),
  "Odunc.kitap": (odunc) => KITAPLAR[odunc.isbn],
  "Odunc.gecikmeGunu": () => { throw new Error("gecikme servisi yanit vermiyor"); },
};
```

`Odunc.gecikmeGunu` çözücüsünün hata atması bilinçlidir: gecikme tutarı gerçek bir kurulumda
ayrı bir servisten gelir ve o servis yanıt vermeyebilir. Bu alan, kısmen başarılı yanıtı
göstermek için oradadır.

#### İşleyicinin Üç Adımı

Sorgu belgesi üç adımdan geçer: **çözümleme** (metin bir seçim ağacına dönüşür),
**doğrulama** (ağaç tip sistemiyle karşılaştırılır) ve **yürütme** (her alan kendi çözücüsüyle
hesaplanır). Doğrulamanın yürütmeden önce gelmesi belirleyicidir: geçersiz bir sorgu için
hiçbir çözücü çalışmaz, dolayısıyla hiçbir veri erişimi yapılmaz.

```js
// yurut.mjs — kucuk sorgu isleyicisi: cozumleme, tip sistemine gore dogrulama, alan basina yurutme
import { TIPLER, COZUCULER } from "./sema.mjs";

export const cozumle = (metin) => {                    // sorgu belgesini secim agacina cevirir
  const s = metin.match(/[A-Za-z_][A-Za-z0-9_]*|[{}():]|"[^"]*"|\d+/g) ?? [];
  let i = 0;
  const kume = () => {
    const alanlar = [];
    i++;
    while (s[i] !== "}") {
      const ad = s[i++];
      let arg = null;
      if (s[i] === "(") { arg = s[i + 3].replace(/^"|"$/g, ""); i += 5; }   // ad ( anahtar : deger )
      alanlar.push({ ad, arg, alt: s[i] === "{" ? kume() : null });
    }
    i++;
    return alanlar;
  };
  s.unshift("{"); s.push("}");
  return kume();
};

export const dogrula = (alanlar, tipAdi = "Sorgu", yol = []) => {   // tip sistemine gore denetim
  const kusur = [];
  for (const alan of alanlar) {
    const tanim = TIPLER[tipAdi]?.[alan.ad];
    const su = [...yol, alan.ad];
    if (!tanim) { kusur.push(`${su.join(".")}: ${tipAdi} tipinde boyle bir alan yok`); continue; }
    const nesneMi = typeof tanim === "object";
    if (nesneMi && !alan.alt) kusur.push(`${su.join(".")}: ${tanim.tip} bir nesne, alt alan secilmeli`);
    else if (!nesneMi && alan.alt) kusur.push(`${su.join(".")}: ${tanim} bir skaler, alt alan secilemez`);
    else if (nesneMi) kusur.push(...dogrula(alan.alt, tanim.tip, su));
  }
  return kusur;
};

export const yurut = (alanlar, kok) => {
  const hatalar = [];
  const adim = (alanlar, kaynak, tipAdi, yol) => {
    const cikti = {};
    for (const alan of alanlar) {
      const tanim = TIPLER[tipAdi][alan.ad];
      const su = [...yol, alan.ad];
      const cozucu = COZUCULER[`${tipAdi}.${alan.ad}`] ?? ((k) => k[alan.ad]);
      let deger;
      // Alan basina cozumleme: bir alanin hatasi digerlerini durdurmaz, o alan null olur.
      try { deger = cozucu(kaynak, alan.arg); }
      catch (e) { hatalar.push({ yol: su.join("."), ileti: e.message }); cikti[alan.ad] = null; continue; }
      if (typeof tanim !== "object") { cikti[alan.ad] = deger ?? null; continue; }
      cikti[alan.ad] = tanim.liste
        ? deger.map((d, k) => adim(alan.alt, d, tanim.tip, [...su, k]))
        : adim(alan.alt, deger, tanim.tip, su);
    }
    return cikti;
  };
  return { veri: adim(alanlar, kok, "Sorgu", []), hatalar };
};

export const degiskenleriYerlestir = (belge, degiskenler) =>
  belge.replace(/\$(\w+)/g, (_, ad) => JSON.stringify(degiskenler[ad]));
```

```js
// sunucu.mjs — tek uc nokta. Yol her sorgu icin aynidir; ne istendigi govdede yazar.
import { createServer } from "node:http";
import { cozumle, dogrula, yurut, degiskenleriYerlestir } from "./yurut.mjs";

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  let ham = "";
  for await (const parca of istek) ham += parca;
  const { belge, degiskenler = {} } = JSON.parse(ham);

  const alanlar = cozumle(degiskenleriYerlestir(belge, degiskenler));
  const kusur = dogrula(alanlar);
  // Dogrulama yurutmeden once yapilir: gecersiz sorgu icin hicbir cozucu calismaz.
  const govde = kusur.length ? { hatalar: kusur } : yurut(alanlar, null);
  const metin = JSON.stringify(govde);
  yanit.setHeader("Content-Type", "application/json; charset=utf-8");
  yanit.setHeader("Content-Length", Buffer.byteLength(metin));
  yanit.writeHead(kusur.length ? 400 : 200).end(metin);
}).listen(8471, "127.0.0.1", () => console.log("tek uc nokta 127.0.0.1:8471/graf"));
```

#### Ölçüm

```js
// olc.mjs — tek uc noktanin dort ozelligini olcer
const sor = async (belge, degiskenler) => {
  const govde = JSON.stringify({ belge, degiskenler });
  const y = await fetch("http://127.0.0.1:8471/graf", { method: "POST", body: govde });
  const metin = await y.text();
  return { kod: y.status, bayt: Buffer.byteLength(metin), gonderilen: Buffer.byteLength(govde),
           govde: JSON.parse(metin) };
};
const yaz = (baslik, y) => console.log(`  ${baslik.padEnd(22)} HTTP ${y.kod}  ` +
  `gonderilen=${String(y.gonderilen).padStart(3)} B alinan=${String(y.bayt).padStart(3)} B\n` +
  `    ${JSON.stringify(y.govde)}`);

console.log("-- 1) ayni uc nokta, iki farkli istemci gereksinimi --");
yaz("raf ekrani", await sor('uye(id: "u1") { ad }'));
yaz("gecikme raporu", await sor('uye(id: "u1") { ad oduncler { sonTarih kitap { ad yazar } } }'));

console.log("\n-- 2) tip sistemi: sorgu yurutulmeden once dogrulanir --");
yaz("olmayan alan", await sor('uye(id: "u1") { ad telefon }'));
yaz("skalerde alt secim", await sor('uye(id: "u1") { ad { uzunluk } }'));
yaz("nesnede secim yok", await sor('uye(id: "u1") { ad oduncler }'));

console.log("\n-- 3) kismi basari: bir alanin cozucusu hata veriyor --");
yaz("gecikme alani", await sor('uye(id: "u1") { ad oduncler { sonTarih gecikmeGunu } }'));

console.log("\n-- 4) degiskenler: ayni belge, iki farkli deger --");
const BELGE = "kitap(isbn: $kimlik) { ad yazar sayfa }";
for (const kimlik of ["978-0262033848", "978-0201896831"])
  yaz(`kimlik=${kimlik.slice(-4)}`, await sor(BELGE, { kimlik }));
```

```bash
node sunucu.mjs & p=$!
curl -s --retry 20 --retry-connrefused --retry-delay 0 -o /dev/null \
  -X POST -d '{"belge":"kitap(isbn: \"978-0262033848\") { ad }"}' http://127.0.0.1:8471/graf
node olc.mjs
kill $p
```

```
tek uc nokta 127.0.0.1:8471/graf
-- 1) ayni uc nokta, iki farkli istemci gereksinimi --
  raf ekrani             HTTP 200  gonderilen= 34 B alinan= 48 B
    {"veri":{"uye":{"ad":"Ayse Kaya"}},"hatalar":[]}
  gecikme raporu         HTTP 200  gonderilen= 75 B alinan=219 B
    {"veri":{"uye":{"ad":"Ayse Kaya","oduncler":[{"sonTarih":"2024-05-15","kitap":{"ad":"Algoritmalara Giris","yazar":"Cormen"}},{"sonTarih":"2024-05-17","kitap":{"ad":"Programlama Sanati","yazar":"Knuth"}}]}},"hatalar":[]}

-- 2) tip sistemi: sorgu yurutulmeden once dogrulanir --
  olmayan alan           HTTP 400  gonderilen= 42 B alinan= 59 B
    {"hatalar":["uye.telefon: Uye tipinde boyle bir alan yok"]}
  skalerde alt secim     HTTP 400  gonderilen= 46 B alinan= 60 B
    {"hatalar":["uye.ad: Dizgi bir skaler, alt alan secilemez"]}
  nesnede secim yok      HTTP 400  gonderilen= 43 B alinan= 65 B
    {"hatalar":["uye.oduncler: Odunc bir nesne, alt alan secilmeli"]}

-- 3) kismi basari: bir alanin cozucusu hata veriyor --
  gecikme alani          HTTP 200  gonderilen= 68 B alinan=306 B
    {"veri":{"uye":{"ad":"Ayse Kaya","oduncler":[{"sonTarih":"2024-05-15","gecikmeGunu":null},{"sonTarih":"2024-05-17","gecikmeGunu":null}]}},"hatalar":[{"yol":"uye.oduncler.0.gecikmeGunu","ileti":"gecikme servisi yanit vermiyor"},{"yol":"uye.oduncler.1.gecikmeGunu","ileti":"gecikme servisi yanit vermiyor"}]}

-- 4) degiskenler: ayni belge, iki farkli deger --
  kimlik=3848            HTTP 200  gonderilen= 93 B alinan= 90 B
    {"veri":{"kitap":{"ad":"Algoritmalara Giris","yazar":"Cormen","sayfa":1312}},"hatalar":[]}
  kimlik=6831            HTTP 200  gonderilen= 93 B alinan= 87 B
    {"veri":{"kitap":{"ad":"Programlama Sanati","yazar":"Knuth","sayfa":650}},"hatalar":[]}
```

#### Ölçümün Söyledikleri

**Tek uç nokta, iki farklı yanıt biçimi.** Raf ekranı 48 bayt, gecikme raporu 219 bayt almıştır.
İkisi de aynı adrese gitmiştir ve sunucuda hiçbir şey değişmemiştir. İkinci derste sorgu
temelli biçemin gösterdiği özellik budur; buradaki fark, seçimin serbest bir metin değil,
yayımlanmış bir tip sistemi içinde yapılmasıdır.

**Tip sistemi üç ayrı hatayı yürütmeden önce yakalar.** Var olmayan bir alan, bir skalerin
altında alt seçim yapılması ve bir nesnenin alt seçimsiz istenmesi — üçü de `400` ile
reddedilir ve hiçbir çözücü çalışmaz. Bu, sorgu temelli bir arayüzün gevşek olmadığını
gösterir: istemci alan bileşimini seçebilir, ama şemanın dışına çıkamaz.

**Kısmen başarılı yanıt bir HTTP hatası değildir.** Gecikme alanının çözücüsü hata atmıştır,
ama yanıt `200` gelmiştir. Veri kısmında istenen alanların çoğu doludur, hata veren alan
`null` olmuştur ve hata listesi her başarısızlığı **yolu**yla birlikte bildirir:
`uye.oduncler.0.gecikmeGunu`. Bu davranış alan başına çözümlemenin doğrudan sonucudur —
her alan kendi verisini kendi getirdiği için bir alanın başarısızlığı komşusunu etkilemez.

Bunun tüketici tarafındaki karşılığı, HTTP durum kodunun tek başına yeterli bir gösterge
olmamasıdır. `200` alan bir istemci gövdeyi de incelemek zorundadır. Uygulama Mimarisi
kursunda kısmi başarı kavramı istemci tarafından anlatılmıştı; burada aynı davranışın
sunucudaki kaynağı görünür.

**Değişkenler sorgu belgesini yeniden kullanılabilir kılar.** Aynı belge iki farklı kitap için
çalışmıştır. Değişkenlerin ayrı gönderilmesinin ikinci bir yararı da vardır: belge sabit
kaldığı için sunucu tarafında tanınabilir, kaydedilebilir ve maliyeti önceden hesaplanabilir.

#### Bedeller

Bu esnekliğin dört bedeli vardır ve üçü ölçümde görünür.

**Önbellekleme zorlaşır.** Bütün sorgular tek adrese, `POST` yöntemiyle gider. Önceki dersteki
varlık etiketi düzeneği burada doğrudan çalışmaz, çünkü adres kaydı adlandırmaz. Önbellekleme
ya istemci tarafında, sorgu belgesi anahtar alınarak yapılır ya da alan düzeyinde kurulur.

**Yanıtın maliyeti önceden bilinmez.** Sunucu, gelen sorgunun kaç veri erişimi doğuracağını
sorgu gelmeden hesaplayamaz. Derinliği artan bir sorgu ya da uzun bir liste içinde iç içe
seçim, tek bir istekle çok iş yaptırabilir. Bu, sorgu maliyeti ve derinlik sınırı gibi
korumaları gerektirir.

**Alan başına çözümleme veri erişimini çoğaltır.** Ölçümdeki iki ödünç için `Odunc.kitap`
çözücüsü iki kez çalıştı; ödünç sayısı arttıkça bu sayı da artar. İkinci derste kaynak
temelli biçemde görülen N+1 örüntüsü burada sunucunun içine taşınmıştır. Toplu yükleme ile
çözümü bu kursun GraphQL Ayrıntısı konusunda ele alınır.

**Hata sözleşmesi ikiye bölünür.** Bir istekte hem taşıma düzeyinde durum kodu hem de gövde
içinde hata listesi bulunur. İkisinin ne zaman hangisini kullanacağı açıkça kararlaştırılmazsa,
tüketiciler hataları iki ayrı yerde aramak zorunda kalır.

#### Özet

- GraphQL tek uç nokta üzerinden çalışır; ne istendiği adreste değil, gövdedeki sorgu
  belgesinde yazar ve yanıtın biçimini istemcinin seçim kümesi belirler.
- Sunucunun yayımladığı tip sistemi seçimi sınırlar: ölçümde var olmayan alan, skalerde alt
  seçim ve alt seçimsiz nesne yürütülmeden önce `400` ile reddedildi.
- Alan başına çözümleme, bir alanın çözücüsü hata verdiğinde diğerlerinin çalışmaya devam
  etmesini sağlar; sonuç `200` durumlu, veri ile hata listesini birlikte taşıyan kısmen
  başarılı bir yanıttır.
- Değişkenler belgeyi sabit tutar; aynı belge farklı değerlerle yeniden kullanılır ve sunucu
  tarafında tanınabilir hâle gelir.
- Bedeller ölçüme yansır: tek adres ve `POST` önbelleklemeyi zorlaştırır, yanıtın doğuracağı
  iş önceden bilinmez, alan başına çözümleme veri erişimini çoğaltır ve hata sözleşmesi iki
  yere bölünür.

#### Sonraki Adım

Tek uç nokta üzerinden, gövdede taşınan bir işlem adıyla çalışmak yeni bir fikir değildir.
Aynı düzeneği kuran ve arkasında geniş bir miras bırakan bir protokol ailesi vardır: her
iletinin bir zarfa sarıldığı, işlem adının ve hatanın da o zarfın içinde taşındığı, sözleşmenin
ayrı bir tanım dosyasında yayımlandığı bir aile. Sonraki ders bu zarf tabanlı yaklaşımı kendi
küçük ayrıştırıcısıyla kurar, zarfın bayt maliyetini ölçer ve bugünkü arayüz kararlarının
hangilerinin oradan devralındığını gösterir.

### Eski Nesil Protokoller

Source: https://academia.sh/tr/kurslar/api-tasarimi/eski-nesil-protokoller

Zarf tabanlı protokollerin tasarımı ve mirası: başlık ile gövdenin ayrılması, işlem adının zarfın içinde taşınması, hatanın zarfın içinde bildirilmesi, zarf maliyetinin kayıt sayısıyla değişimi ve bugünkü arayüz kararlarının hangilerinin buradan devralındığı.

Önceki ders tek uç nokta üzerinden, gövdede taşınan bir seçim belgesiyle çalışan bir arayüz
kurdu. Bu düzenek yeni değildir. Aynı fikri — tek adres, tek yöntem, işlemi gövdede
adlandırma — çok daha önce kuran bir protokol ailesi vardır ve bugünkü arayüz kararlarının
bir kısmı doğrudan oradan devralınmıştır.

Bu ailenin ortak yapı taşı **zarftır** (envelope): iletinin tamamını saran, içinde bir
**başlık** ve bir **gövde** bulunduran bir kap. **SOAP** bu yapının en yaygın bilinen örneğidir
ve XML üzerine kurulmuştur; `Envelope`, `Header`, `Body` ve `Fault` ögeleri bu dersin
kuracağı `Zarf`, `Baslik`, `Govde` ve `Hata` ögelerinin karşılığıdır.

#### Zarfın Fikri

Zarfın çözdüğü sorun şudur: bir iletiyi taşıyan protokolden bağımsız olarak, iletinin kendi
üstverisini yanında taşımak. HTTP başlıkları bu işi zaten yapar, ama zarf tasarımı HTTP'ye
bağlı değildir — aynı zarf bir ileti kuyruğu ya da bir posta iletisi üzerinden de gidebilir.
Bunun bedeli ve kazancı bir arada durur.

Zarfın üç yapısal kararı vardır:

- **Başlık kesişen ilgileri taşır:** izlem kimliği, kimlik bilgisi, işlem adı. Ara katmanlar
  bu bölümü okuyup gövdeyi hiç çözümlemeden karar verebilir.
- **Gövde yükü taşır:** işleme özgü veri. Yükün biçimi işlemden işleme değişir.
- **Hata da gövdeye girer:** başarısızlık gövdedeki bir hata ögesiyle bildirilir. Taşıma bir
  durum kodu taşısa bile — SOAP'ın HTTP bağlaması hatayı `500` ile gönderir — hatanın
  sözleşmesi zarfın içindedir; kuyruk ya da posta gibi taşımalarda tek yer orasıdır.

#### Küçük Bir Gerçekleştirim

Aşağıdaki dosya zarfı kuran ve okuyan işlevleri içerir. XML ayrıştırıcısı iç içe ögeleri bir
nesneye çevirir; öznitelik ve ad alanı desteklemez, çünkü ders için gereken şey yapının
kendisidir.

```js
// zarf.mjs — kucuk XML ayristirici ve zarf kuruculari.
// Zarf/Baslik/Govde/Hata ogeleri SOAP'taki Envelope/Header/Body/Fault karsiliklaridir.
const kacisla = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const cozKacis = (s) => s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");

export const xmlYaz = (ad, icerik) =>
  typeof icerik === "object" && icerik !== null
    ? `<${ad}>${Object.entries(icerik).map(([a, d]) => xmlYaz(a, d)).join("")}</${ad}>`
    : `<${ad}>${kacisla(icerik)}</${ad}>`;

// Ayristirici: ic ice ogeleri bir nesneye cevirir. Ayni adli kardesler diziye toplanir.
export const xmlOku = (metin) => {
  const oge = /<(\w+)>([\s\S]*?)<\/\1>/g;
  const cikti = {};
  let e, bulundu = false;
  while ((e = oge.exec(metin))) {
    bulundu = true;
    const deger = /<\w+>/.test(e[2]) ? xmlOku(e[2]) : cozKacis(e[2]);
    if (e[1] in cikti) cikti[e[1]] = [].concat(cikti[e[1]], deger);
    else cikti[e[1]] = deger;
  }
  return bulundu ? cikti : cozKacis(metin);
};

export const zarfla = (islem, izlem, govde) =>
  `<Zarf>${xmlYaz("Baslik", { Islem: islem, Izlem: izlem })}` +
  `<Govde>${Object.entries(govde).map(([a, d]) => xmlYaz(a, d)).join("")}</Govde></Zarf>`;

export const hataZarfi = (kod, neden, ayrinti) =>
  `<Zarf><Baslik><Islem>Hata</Islem></Baslik><Govde>` +
  xmlYaz("Hata", { Kod: kod, Neden: neden, Ayrinti: ayrinti }) + `</Govde></Zarf>`;

// Ara katmanin okudugu tek yer: govdeyi hic cozumlemeden baslik alinabilir.
export const basligiAyikla = (zarf) => xmlOku(/<Baslik>[\s\S]*?<\/Baslik>/.exec(zarf)[0]).Baslik;
```

```js
// sunucu.mjs — tek uc nokta, tek yontem: islem adi zarfin basligindadir.
import { createServer } from "node:http";
import { xmlOku, zarfla, hataZarfi, basligiAyikla } from "./zarf.mjs";

const KITAPLAR = {
  "978-0262033848": { isbn: "978-0262033848", ad: "Algoritmalara Giris", yazar: "Cormen", sayfa: 1312 },
  "978-0201896831": { isbn: "978-0201896831", ad: "Programlama Sanati", yazar: "Knuth", sayfa: 650 },
};

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  let ham = "";
  for await (const p of istek) ham += p;
  const baslik = basligiAyikla(ham);                  // yonlendirme: yalniz baslik okunur
  const govde = xmlOku(ham).Zarf.Govde;
  let cikti;
  if (baslik.Islem === "KitapGetir") {
    const kitap = KITAPLAR[govde.KitapIstek.isbn];
    cikti = kitap ? zarfla("KitapGetirYanit", baslik.Izlem, { Kitap: kitap })
                  : hataZarfi("Istemci", "kitap bulunamadi", `isbn: ${govde.KitapIstek.isbn}`);
  } else cikti = hataZarfi("Istemci", "bilinmeyen islem", baslik.Islem);
  yanit.setHeader("Content-Type", "text/xml; charset=utf-8");
  yanit.setHeader("Content-Length", Buffer.byteLength(cikti));
  // Hata da basarili istek de ayni durum koduyla doner: hata tasima katmaninda degil zarftadir.
  yanit.writeHead(200).end(cikti);
}).listen(8481, "127.0.0.1", () => console.log("zarf ucu 127.0.0.1:8481/servis"));
```

#### Ölçüm

```js
// olc.mjs — zarf tabanli protokolun uc ozelligini olcer
import { zarfla, xmlOku, basligiAyikla } from "./zarf.mjs";

const cagir = async (islem, izlem, govde) => {
  const istek = zarfla(islem, izlem, govde);
  const y = await fetch("http://127.0.0.1:8481/servis",
    { method: "POST", headers: { "Content-Type": "text/xml" }, body: istek });
  const metin = await y.text();
  return { kod: y.status, gonderilen: Buffer.byteLength(istek), alinan: Buffer.byteLength(metin),
           metin, cozulen: xmlOku(metin).Zarf.Govde };
};

console.log("-- 1) tek uc nokta, islem adi zarfta --");
const iyi = await cagir("KitapGetir", "izl-1", { KitapIstek: { isbn: "978-0262033848" } });
console.log(`  istek : ${zarfla("KitapGetir", "izl-1", { KitapIstek: { isbn: "978-0262033848" } })}`);
console.log(`  yanit : ${iyi.metin}`);
console.log(`  HTTP ${iyi.kod}  gonderilen=${iyi.gonderilen} B  alinan=${iyi.alinan} B`);

console.log("\n-- 2) zarf maliyeti: ayni veri JSON ve zarf biciminde --");
const KAYIT = { isbn: "978-0262033848", ad: "Algoritmalara Giris", yazar: "Cormen", sayfa: 1312 };
for (const adet of [1, 5, 20]) {
  const liste = Array.from({ length: adet }, () => KAYIT);
  const json = Buffer.byteLength(JSON.stringify({ Kitap: liste }));
  const zarf = Buffer.byteLength(zarfla("KitapGetirYanit", "izl-1", { Kitap: liste }));
  console.log(`  ${String(adet).padStart(2)} kayit  json=${String(json).padStart(4)} B  ` +
    `zarf=${String(zarf).padStart(4)} B  fark=%${Math.round(100 * (zarf / json - 1))}`);
}

console.log("\n-- 3) hata zarfta, tasima katmaninda degil --");
const kotu = await cagir("KitapGetir", "izl-2", { KitapIstek: { isbn: "978-0000000000" } });
console.log(`  yanit : ${kotu.metin}`);
console.log(`  durum koduna bakan istemci  -> HTTP ${kotu.kod} : "istek basarili"`);
console.log(`  govdeyi cozen istemci       -> ${JSON.stringify(kotu.cozulen.Hata)}`);

console.log("\n-- 4) ara katman govdeyi cozmeden basligi okur --");
for (const z of [iyi.metin, kotu.metin]) {
  const govdeBoyu = /<Govde>[\s\S]*<\/Govde>/.exec(z)[0].length;
  console.log(`  baslik=${JSON.stringify(basligiAyikla(z))}  (govde ${govdeBoyu} B, cozumlenmedi)`);
}
```

```bash
node sunucu.mjs & p=$!
curl -s --retry 20 --retry-connrefused --retry-delay 0 -o /dev/null -X POST \
  -d '<Zarf><Baslik><Islem>x</Islem></Baslik><Govde></Govde></Zarf>' http://127.0.0.1:8481/servis
node olc.mjs
kill $p
```

```
zarf ucu 127.0.0.1:8481/servis
-- 1) tek uc nokta, islem adi zarfta --
  istek : <Zarf><Baslik><Islem>KitapGetir</Islem><Izlem>izl-1</Izlem></Baslik><Govde><KitapIstek><isbn>978-0262033848</isbn></KitapIstek></Govde></Zarf>
  yanit : <Zarf><Baslik><Islem>KitapGetirYanit</Islem><Izlem>izl-1</Izlem></Baslik><Govde><Kitap><isbn>978-0262033848</isbn><ad>Algoritmalara Giris</ad><yazar>Cormen</yazar><sayfa>1312</sayfa></Kitap></Govde></Zarf>
  HTTP 200  gonderilen=142 B  alinan=205 B

-- 2) zarf maliyeti: ayni veri JSON ve zarf biciminde --
   1 kayit  json=  94 B  zarf= 212 B  fark=%126
   5 kayit  json= 426 B  zarf= 620 B  fark=%46
  20 kayit  json=1671 B  zarf=2170 B  fark=%30

-- 3) hata zarfta, tasima katmaninda degil --
  yanit : <Zarf><Baslik><Islem>Hata</Islem></Baslik><Govde><Hata><Kod>Istemci</Kod><Neden>kitap bulunamadi</Neden><Ayrinti>isbn: 978-0000000000</Ayrinti></Hata></Govde></Zarf>
  durum koduna bakan istemci  -> HTTP 200 : "istek basarili"
  govdeyi cozen istemci       -> {"Kod":"Istemci","Neden":"kitap bulunamadi","Ayrinti":"isbn: 978-0000000000"}

-- 4) ara katman govdeyi cozmeden basligi okur --
  baslik={"Islem":"KitapGetirYanit","Izlem":"izl-1"}  (govde 125 B, cozumlenmedi)
  baslik={"Islem":"Hata"}  (govde 116 B, cozumlenmedi)
```

#### Ölçümün Söyledikleri

**Zarfın maliyeti sabittir, yükün maliyeti değişkendir.** Tek kayıtta zarf, aynı verinin JSON
biçiminden %126 daha büyüktür; yirmi kayıtta bu fark %30'a iner. Zarfın kendisi — başlık
bölümü, sarmalayan ögeler — kayıt sayısından bağımsız sabit bir maliyettir; kayıt başına
düşen fazlalık ise kapanış etiketlerinden gelir ve her kayıtta tekrarlanır. Küçük ve sık
çağrılarda zarf pahalıdır; büyük yüklerde oran düşer.

**Hatanın zarfta bildirilmesinin bedeli tüketiciye çıkar.** Var olmayan bir kitap istendiğinde
sunucu `200` döndürmüştür. Durum koduna bakan bir istemci isteği başarılı sayar; hatayı
görmek için gövdeyi çözümlemek gerekir. Bu, dördüncü derste anılan **hata sözleşmesinin ikiye
bölünmesi** sorununun kaynağıdır ve GraphQL'in kısmen başarılı yanıtında aynı biçimde geri
gelir.

**Başlık gövdeden bağımsız okunabilir.** Ölçümün son bölümü, yüz baytın üzerindeki gövde
hiç çözümlenmeden başlığın alınabildiğini gösterir. Bu, bir ara katmanın — yönlendirici,
yetkilendirme katmanı, izleme katmanı — yükü anlamadan iş yapabilmesi demektir. Başlığın
taşıdığı **izlem kimliği** aynı isteğin farklı bileşenlerdeki kayıtlarını birbirine bağlar.

#### Bırakılan Miras

Zarf tabanlı tasarımın bugüne aktarılan yanları, ölçümde görülen özelliklerin doğrudan
devamıdır.

**Makine okunur sözleşme.** Bu ailenin arayüzleri, hangi işlemlerin bulunduğunu ve her
işlemin girdi–çıktı tiplerini ayrı bir tanım dosyasında yayımlar. Buradan iki alışkanlık
kaldı: sözleşmenin belgeden ayrı, çalıştırılabilir bir dosya olması ve istemci tarafının bu
dosyadan üretilebilmesi. Aynı fikir dördüncü derste şema dosyası olarak, bu kursun ilerleyen
bölümünde ise OpenAPI ve JSON Schema tabanlı tanımlar olarak görülür.

**Yapılandırılmış hata.** Hata ögesindeki üçlü — kod, neden, ayrıntı — bugünkü standart hata
gövdelerinin atasıdır: makinenin okuyacağı bir kod, insanın okuyacağı bir açıklama ve
bağlama özgü bir ek. Bu yapı, hata sözleşmesi dersinde ayrıntılandırılır.

**Kesişen ilgilerin ayrılması.** Kimlik, izlem ve yönlendirme bilgisinin yükten ayrı
tutulması korunmuştur; HTTP tabanlı arayüzlerde bu ayrım zarfın başlığı yerine HTTP
başlıklarında yaşar.

**İşlem adının gövdede taşınması.** Tek uç nokta ve gövdede işlem adı örüntüsü, hem uzak
yordam çağrısı biçemli HTTP arayüzlerinde hem de GraphQL'de sürer.

Devralınmayanlar da bellidir. Taşımadan bağımsız olma isteği, HTTP'nin kendi anlamlarını —
yöntemler, durum kodları, koşullu istekler, önbellek yönergeleri — kullanılmaz kılar. Üçüncü
derste ölçülen önbelleklenebilirlik kazancı, her isteğin `POST` olduğu bir tasarımda elde
edilemez. Metin biçiminin ayrıntı yüküyle birlikte bu, aynı işi daha az bayt ve daha az
katmanla yapan tasarımların yolunu açan asıl nedendir.

#### Özet

- Zarf tabanlı protokoller iletiyi başlık ve gövde olarak saran bir kapla çalışır; işlem adı
  adreste değil, zarfın başlığında taşınır ve taşıma katmanı değiştirilebilir sayılır.
- Ölçümde zarfın bayt maliyeti tek kayıtta %126, yirmi kayıtta %30 çıktı: zarfın kendisi
  sabit maliyet, kapanış etiketleri ise kayıt başına tekrarlanan maliyettir.
- Ölçümdeki sunucu hatayı yalnız zarfın içinde bildirdiği için başarısız işlemde de `200`
  döndürür; durum koduna bakan istemci hatayı göremez, gövdeyi çözümlemek zorundadır.
- Başlık gövdeden bağımsız okunabildiği için ara katmanlar yükü hiç çözümlemeden yönlendirme,
  yetkilendirme ve izleme yapabilir.
- Devralınan miras dört kalemdir: makine okunur sözleşme dosyası, yapılandırılmış hata
  yapısı, kesişen ilgilerin yükten ayrılması ve tek uç nokta üzerinde adlandırılmış işlem.

#### Sonraki Adım

Buraya kadarki beş biçemin ortak varsayımı aynıdır: iletişimi istemci başlatır, sunucu yanıt
verir, alışveriş biter. Kütüphane servisinin bazı işleri bu kalıba girmez. Bir kitabın rafa
dönmesi sunucuda olan bir olaydır ve bunu bekleyen istemciye kimse sormaz. Bir sayım işi
dakikalarca sürer ve isteği açık tutmak bir seçenek değildir. Sonraki ders istek–yanıt
dışındaki iletişim biçimlerini kurar ve ikisini ölçerek karşılaştırır: bir olay akışı ile uzun
yoklama aynı bildirimi kaç bağlantıyla ve ne kadar gecikmeyle ulaştırır?

### Eşzamanlı ve Eşzamansız API'ler

Source: https://academia.sh/tr/kurslar/api-tasarimi/eszamanli-ve-eszamansiz-apiler

İstek–yanıt kalıbının dışına çıkan iletişim biçimleri: olay akışı, uzun yoklama ve kısa yoklama aynı olay dizisinde bağlantı sayısı, taşınan bayt ve teslim gecikmesiyle karşılaştırılır; uzun süren iş kabul edilen istek ve ayrı bir durum kaynağıyla modellenir.

Buraya kadarki beş biçemin ortak bir varsayımı vardı: iletişimi istemci başlatır, sunucu
yanıt verir, alışveriş biter. Kütüphane servisinin bazı işleri bu kalıba girmez.

Üç durum bu kalıbı kırar. Birincisi, **olay sunucuda oluşur**: bir kitabın rafa dönmesini
bekleyen istemciye kimse haber vermez, istemci de ne zaman soracağını bilmez. İkincisi,
**iş uzun sürer**: şube sayımı dakikalarca çalışır ve isteği o süre boyunca açık tutmak
bağlantıyı da istemciyi de bekletir. Üçüncüsü, **aynı bildirim çok sayıda tüketiciye gider**:
her tüketicinin ayrı ayrı sorması aynı işi tekrarlar.

Bu ders ilk durumu üç biçimde yazıp ölçer, sonra ikinci durumun kalıbını kurar.

#### Aynı Olayı Üç Biçimde Yayımlamak

Sunucu altı olay yayımlar — kitapların rafa dönüşü — ve aynı olay dizisini üç ayrı uçtan
sunar. **Olay akışı** bağlantıyı açık tutar ve olay oluştukça yazar. **Uzun yoklama** isteği
yeni olay çıkana kadar yanıtsız bekletir. **Kısa yoklama** her isteği hemen yanıtlar; boş
dönebilir, istemci belirli aralıklarla yeniden sorar.

Üçünde de istemci en son gördüğü olayın kimliğini gönderir. Bu, yoklama biçimlerinde bir
zorunluluktur: iki istek arasındaki boşlukta oluşan olaylar aksi hâlde kaybolur.

```js
// sunucu.mjs — ayni olay akisini uc bicimde yayimlar: olay akisi, uzun yoklama, kisa yoklama
import { createServer } from "node:http";

const OLAYLAR = [];              // yayimlanmis olaylar; kimlik dizideki sira numarasidir
const bekleyenler = [];          // uzun yoklamada yanit bekleyen istekler
const akislar = new Set();       // acik olay akisi baglantilari
const isler = new Map();         // uzun suren islerin durumu
let isSayaci = 0;

const yayimla = (olay) => {
  OLAYLAR.push(olay);
  for (const y of akislar) y.write(`id: ${OLAYLAR.length}\ndata: ${JSON.stringify(olay)}\n\n`);
  while (bekleyenler.length) bekleyenler.pop()();
};

const json = (yanit, govde) => {
  const metin = JSON.stringify(govde);
  yanit.setHeader("Content-Type", "application/json; charset=utf-8");
  yanit.setHeader("Content-Length", Buffer.byteLength(metin));
  yanit.writeHead(200).end(metin);
};
const sonrasi = (n) => OLAYLAR.slice(n).map((o, i) => ({ id: n + i + 1, ...o }));

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");
  const son = Number(adres.searchParams.get("son") ?? 0);

  // 1) Olay akisi: baglanti acik kalir, olay olustukca sunucu yazar.
  if (adres.pathname === "/akis") {
    yanit.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-store",
                           Connection: "keep-alive" });
    for (const o of sonrasi(son)) yanit.write(`id: ${o.id}\ndata: ${JSON.stringify(o)}\n\n`);
    akislar.add(yanit);
    istek.on("close", () => akislar.delete(yanit));
    return;
  }
  // 2) Uzun yoklama: yeni olay yoksa yanit bekletilir, olay gelince yazilir.
  if (adres.pathname === "/uzun") {
    if (OLAYLAR.length > son) return json(yanit, { olaylar: sonrasi(son) });
    const zamanAsimi = setTimeout(() => {
      const k = bekleyenler.indexOf(surdur); if (k >= 0) bekleyenler.splice(k, 1);
      json(yanit, { olaylar: [] });
    }, 2000);
    const surdur = () => { clearTimeout(zamanAsimi); json(yanit, { olaylar: sonrasi(son) }); };
    bekleyenler.push(surdur);
    return;
  }
  // 3) Kisa yoklama: her istek hemen yanitlanir, istemci belli araliklarla sorar.
  if (adres.pathname === "/kisa") return json(yanit, { olaylar: sonrasi(son) });
  // 4) Uzun suren is: istek kabul edilir, sonuc ayri bir kaynaktan izlenir.
  if (adres.pathname === "/sayimlar" && istek.method === "POST") {
    const kimlik = `is-${++isSayaci}`;
    isler.set(kimlik, { durum: "calisiyor", sonuc: null });
    setTimeout(() => isler.set(kimlik, { durum: "bitti", sonuc: { eksik: 3 } }), 600);
    yanit.setHeader("Location", `/isler/${kimlik}`);
    yanit.setHeader("Content-Type", "application/json; charset=utf-8");
    return yanit.writeHead(202).end(JSON.stringify({ is: kimlik }));
  }
  if (adres.pathname.startsWith("/isler/")) return json(yanit, isler.get(adres.pathname.slice(7)));
  // Olculerin ayni kosulda calismasi icin yayin disaridan baslatilir.
  if (adres.pathname === "/basla") { basla(); return json(yanit, { baslatildi: true }); }
  return json(yanit, { hata: "bulunamadi" });
}).listen(8491, "127.0.0.1", () => console.log("olay ucu 127.0.0.1:8491"));

// Alti olay, 250 ms araliklarla: bir kitabin rafa donmesi sunucuda olusan bir olaydir.
const basla = () => {
  let n = 0;
  const t = setInterval(() => {
    yayimla({ tur: "kitap_rafa_dondu", isbn: `978-000000000${n}`, an: Date.now() });
    if (++n === 6) clearInterval(t);
  }, 250);
};
```

Ölçümün geçerli olması için üç istemcinin de aynı olay dizisini, aynı anda izlemesi gerekir.
Bu yüzden yayın kendiliğinden başlamaz: üç istemci bağlandıktan sonra dışarıdan tetiklenir.

```js
// olc.mjs — uc bicimi ayni olay akisinda, ayni anda olcer: baglanti sayisi, bayt, teslim gecikmesi
import { Agent, request } from "node:http";
import { connect } from "node:net";

const olustur = () => {
  const durum = { baglanti: 0, istek: 0, bayt: 0, gecikmeler: [] };
  const ajan = new Agent({ keepAlive: false });     // her istek yeni baglanti acar
  ajan.createConnection = (s) => {
    durum.baglanti++;
    const b = connect(s);
    b.on("data", (p) => (durum.bayt += p.length));
    return b;
  };
  return { durum, ajan };
};
const kaydet = (durum, olaylar) => {
  for (const o of olaylar) durum.gecikmeler.push(Date.now() - o.an);
  return olaylar.length ? olaylar[olaylar.length - 1].id : null;
};
const al = (birim, yol) => new Promise((coz) => {
  birim.durum.istek++;
  request({ agent: birim.ajan, host: "127.0.0.1", port: 8491, path: yol }, (y) => {
    let m = ""; y.on("data", (p) => (m += p)); y.on("end", () => coz(JSON.parse(m)));
  }).end();
});

// 1) OLAY AKISI — tek baglanti acik kalir, sunucu yazdikca istemci okur
const akis = olustur();
const akisIsi = new Promise((bitti) => {
  akis.durum.istek++;
  request({ agent: akis.ajan, host: "127.0.0.1", port: 8491, path: "/akis?son=0" }, (y) => {
    y.on("data", (p) => {
      for (const blok of String(p).split("\n\n")) {
        const e = /data: (.+)/.exec(blok);
        if (e) kaydet(akis.durum, [JSON.parse(e[1])]);
      }
      if (akis.durum.gecikmeler.length === 6) { y.destroy(); bitti(); }
    });
  }).end();
});

// 2) UZUN YOKLAMA — sunucu yanit vermeden bekletir, olay gelince yazar; istemci yeniden baglanir
const uzun = olustur();
const uzunIs = (async () => {
  let son = 0;
  while (uzun.durum.gecikmeler.length < 6)
    son = kaydet(uzun.durum, (await al(uzun, `/uzun?son=${son}`)).olaylar) ?? son;
})();

// 3) KISA YOKLAMA — sabit aralikla sorma
const kisa = olustur();
const kisaIs = (async () => {
  let son = 0;
  while (kisa.durum.gecikmeler.length < 6) {
    son = kaydet(kisa.durum, (await al(kisa, `/kisa?son=${son}`)).olaylar) ?? son;
    if (kisa.durum.gecikmeler.length < 6) await new Promise((c) => setTimeout(c, 500));
  }
})();

await new Promise((c) => setTimeout(c, 200));       // uc istemci de baglandiktan sonra yayin baslar
await fetch("http://127.0.0.1:8491/basla");
await Promise.all([akisIsi, uzunIs, kisaIs]);

const ozet = (ad, durum) => {
  const g = durum.gecikmeler;
  console.log(`  ${ad.padEnd(13)} baglanti=${String(durum.baglanti).padStart(2)}  ` +
    `bayt=${String(durum.bayt).padStart(4)}  olay=${g.length}  ` +
    `ortalama gecikme=${String(Math.round(g.reduce((t, v) => t + v, 0) / g.length)).padStart(3)} ms  ` +
    `en buyuk=${String(Math.max(...g)).padStart(3)} ms`);
};
console.log("-- alti olay, 250 ms araliklarla yayimlandi --");
ozet("olay akisi", akis.durum);
ozet("uzun yoklama", uzun.durum);
ozet("kisa yoklama", kisa.durum);

// 4) UZUN SUREN IS — istek kabul edilir, sonuc ayri bir kaynaktan izlenir
console.log("\n-- uzun suren is: kabul edilen istek --");
const t0 = Date.now();
const kabul = await fetch("http://127.0.0.1:8491/sayimlar", { method: "POST" });
const yer = kabul.headers.get("location");
console.log(`  POST /sayimlar -> HTTP ${kabul.status}  Location: ${yer}`);
for (let i = 0; i < 5; i++) {
  const d = await (await fetch(`http://127.0.0.1:8491${yer}`)).json();
  console.log(`  GET ${yer} (+${Date.now() - t0} ms) -> ${JSON.stringify(d)}`);
  if (d.durum === "bitti") break;
  await new Promise((c) => setTimeout(c, 200));
}
```

```bash
node sunucu.mjs > /dev/null & p=$!
curl -s --retry 20 --retry-connrefused --retry-delay 0 -o /dev/null "http://127.0.0.1:8491/kisa?son=0"
node olc.mjs
kill $p
```

```
-- alti olay, 250 ms araliklarla yayimlandi --
  olay akisi    baglanti= 1  bayt= 663  olay=6  ortalama gecikme=  3 ms  en buyuk=  5 ms
  uzun yoklama  baglanti= 6  bayt=1170  olay=6  ortalama gecikme=  2 ms  en buyuk=  4 ms
  kisa yoklama  baglanti= 5  bayt=1055  olay=6  ortalama gecikme=165 ms  en buyuk=292 ms

-- uzun suren is: kabul edilen istek --
  POST /sayimlar -> HTTP 202  Location: /isler/is-1
  GET /isler/is-1 (+14 ms) -> {"durum":"calisiyor","sonuc":null}
  GET /isler/is-1 (+219 ms) -> {"durum":"calisiyor","sonuc":null}
  GET /isler/is-1 (+425 ms) -> {"durum":"calisiyor","sonuc":null}
  GET /isler/is-1 (+629 ms) -> {"durum":"bitti","sonuc":{"eksik":3}}
```

Gecikme değerleri ve bayt sayıları makineye ve çalışma zamanının yazdığı başlıklara bağlıdır;
altı olayın sayısı, bağlantı sayıları ve büyüklük sıralaması bağlı değildir.

#### Ölçümün Söyledikleri

**Olay akışı tek bağlantıyla altı olayı taşır.** İstemci bir kez bağlanır, sunucu yazdıkça
okur; gecikme milisaniye düzeyindedir. Taşınan bayt da en düşüktür, çünkü altı olay için tek
bir başlık kümesi gönderilmiştir.

**Uzun yoklama aynı gecikmeyi altı bağlantıyla elde eder.** Her olaydan sonra yanıt kapanır
ve istemci yeniden bağlanır; her yeniden bağlanma yeni bir istek satırı ve yeni bir başlık
kümesi demektir. Bayt sayısı olay akışının yaklaşık iki katıdır. Gecikme düşüktür, çünkü
sunucu isteği olay gelene kadar bekletmiştir — beklemenin bedeli baytta değil, açık tutulan
istek sayısındadır: her bekleyen istemci sunucuda bir yanıt nesnesi ve bir bağlantı tutar.

**Kısa yoklama gecikmeyi yoklama aralığına bağlar.** Ortalama gecikme 165 milisaniye, en
büyüğü 292 milisaniyedir; yoklama aralığı yarım saniyedir ve beklenen ortalama gecikme
aralığın yarısı kadardır. Bağlantı sayısı burada olay sayısına değil, geçen süreye bağlıdır:
hiç olay olmasa da istemci sormaya devam eder ve her boş yanıt taşınan bayta eklenir.

Üçü arasındaki seçim tek bir ölçüyle yapılmaz. Kısa yoklama en basit olanıdır ve gecikmenin
önemsiz olduğu yerlerde yeterlidir; ara katmanlar için sıradan bir istektir. Uzun yoklama
gecikmeyi düşürür ama sunucuda açık tutulan istek sayısını yükseltir. Olay akışı her ikisinde
de kazanır, karşılığında uzun ömürlü bağlantı ister: araya giren katmanların bağlantıyı
kapatmaması, sunucunun her istemci için bir bağlantı ayırabilmesi ve akışın kopması hâlinde
yeniden kurulabilmesi gerekir.

Son nokta ölçümdeki `son` parametresinin karşılığıdır. **Server-Sent Events**, kopan bir
bağlantıyı yeniden kurarken en son alınan olay kimliğini `Last-Event-ID` başlığıyla geri
gönderir; sunucu da kaldığı yerden devam eder. Bu düzenek olmadan hiçbir eşzamansız yayım
güvenilir değildir: kopma anında oluşan olaylar sessizce kaybolur.

#### Tek Yönlü ve Çift Yönlü

Olay akışı tek yönlüdür: sunucudan istemciye. İstemcinin de sürekli veri göndermesi
gerekiyorsa — raf sayımında el uçbiriminin okuduğu her etiketi anında bildirmesi gibi — iki
yönlü bir bağlantı gerekir ve bunun karşılığı **WebSocket** protokolüdür. Bedeli, HTTP'nin
anlamlarından çıkmaktır: yöntem, durum kodu, önbellek yönergesi ve koşullu istek kavramları
bağlantı kurulduktan sonra geçerli değildir. Üçüncü derste ölçülen kazançların hiçbiri açık
soket üzerinde geçerli olmaz.

Seçim ölçütü basittir: veri yalnız sunucudan geliyorsa olay akışı yeterlidir; her iki taraf
da sürekli veri gönderiyorsa çift yönlü bağlantı gerekir.

#### Kabul Edilen İstek

Ölçümün son bölümü ikinci durumu kurar. Şube sayımı istendiğinde sunucu işi bitirmeye
çalışmaz; isteği **kabul eder**, `202` döndürür ve `Location` başlığıyla işin izleneceği
kaynağı bildirir. İstemci o kaynağı sorarak durumu öğrenir; iş bitince aynı kaynak sonucu
taşır.

Bu kalıbın üç yararı vardır. İstek süresi işin süresinden ayrılır, dolayısıyla zaman aşımı
sorunu ortadan kalkar. İşin durumu bir kaynak olduğu için tek biçim arayüzün içinde kalır;
listelenebilir, iptal edilebilir, saklanabilir. Ve aynı iş iki kez istendiğinde ne olacağı
ayrı bir karara dönüşür — bu kursun Etkisizlik Anahtarları dersinin konusudur.

İşin bittiğini öğrenmek için istemcinin sorması da zorunlu değildir: durum kaynağı bir olay
akışıyla birleştirildiğinde, iş bittiğinde bildirim istemciye kendiliğinden ulaşır. İki
kalıp birbirinin yerine değil, birlikte kullanılır.

#### Özet

- İstek–yanıt kalıbı üç durumda yetmez: olayın sunucuda oluşması, işin uzun sürmesi ve aynı
  bildirimin çok sayıda tüketiciye gitmesi.
- Ölçümde olay akışı altı olayı tek bağlantı ve 663 baytla, uzun yoklama altı bağlantı ve
  1170 baytla taşıdı; ikisinin de gecikmesi milisaniye düzeyinde kaldı.
- Kısa yoklamada gecikme yoklama aralığına bağlıdır: yarım saniyelik aralıkta ortalama 165,
  en büyük 292 milisaniye ölçüldü ve bağlantı sayısı olay sayısına değil geçen süreye bağlandı.
- Her yoklama biçimi son görülen olay kimliğini taşımak zorundadır; kopma ve istekler arası
  boşlukta oluşan olaylar aksi hâlde kaybolur.
- Uzun süren iş `202` ile kabul edilir ve ayrı bir durum kaynağıyla izlenir; böylece istek
  süresi işin süresinden ayrılır ve iş, tek biçim arayüzün içinde bir kaynağa dönüşür.

#### Sonraki Adım

Bu konu biçemleri karşılaştırdı ve her birinin neyi kolaylaştırıp neyi zorlaştırdığını ölçtü.
Bundan sonrası bir seçimin ayrıntısıdır: kaynak temelli biçemin gerçekten tasarlanması.
Ölçümlerde kaynaklar hazır varsayıldı — kitap, üye, ödünç, iş — oysa bunların kaynak olması
bir karardı. Alan kavramlarının hangisi bir kaynağa dönüşür, hangisi bir başkasının alanı
olarak kalır? "Kitabı uzat" gibi bir eylem hangi kaynağa oturur? Sonraki konunun ilk dersi
bu soruyla başlar ve kaynak modelini kütüphane alanının kavramlarından çıkarır.

## Kaynak ve Sözleşme Tasarımı

Alan kavramlarının kaynağa dönüşmesi, URI düzeni, HTTP yöntemlerinin ve durum kodlarının doğru eşlenmesi, gövde biçimi, sayfalama, filtreleme, alan seçimi, tekillik anahtarları ve bağlantı odaklı yanıtlar.

### Kaynak Modelleme

Source: https://academia.sh/tr/kurslar/api-tasarimi/kaynak-modelleme

Alan kavramlarının adreslenebilir kaynağa dönüşmesi: eylem adlarıyla büyüyen uç nokta listesi, işlemin durum değişimi olarak modellenmesi, hiçbir tabloya karşılık gelmeyen hesaplanmış kaynaklar ve kimlik seçiminin bedeli.

API Biçemleri konusu seçim işini bitirdi: kaynak temelli, uzak yordam çağrısı ve sorgu
temelli yaklaşımların hangi kısıt altında birbirine üstün geldiği, eşzamanlı ve eşzamansız
iletişimin nerede ayrıştığı kuruldu. Kütüphane ödünç servisi için karar kaynak temelli
biçemden yana verildiğinde iş bitmez; asıl tasarım o noktada başlar. Biçem, "kaynakları
adresle ve tek biçim arayüzle konuş" der, ama hangi şeyin kaynak olduğunu söylemez.

Bu ders o boşluğu doldurur. Elde bir alan vardır — kitap, üye, ödünç işlemi, şube, gecikme
ücreti, ödünç süresi — ve bu kavramların hepsi aynı türden değildir. Bazıları adreslenir,
bazıları yalnızca bir kaynağın içinde bir alan olarak yaşar, bazıları da hiçbir tabloda
satırı olmadan kaynak olur. Ayrımı yapan ölçüt kimliktir.

#### Kaynak Nedir, Ne Değildir

**Kaynak** (resource), istemcinin ayrı ayrı başvurmak isteyeceği ve zaman içinde aynı
kalan bir kimliği olan şeydir. Bu tanımın iki yarısı da işlevseldir. Kimliği olmayan bir
şey adreslenemez; başvurulmayan bir şeyin adreslenmesi de yalnız yüzey büyütür.

Ödünç servisinde ölçütü uygulayalım. **Kitap** kaynaktır: katalogda tek başına aranır,
ayrıntısı gösterilir, güncellenir. **Üye** kaynaktır. **Ödünç işlemi** kaynaktır — bu, ilk
bakışta bir eylem gibi görünse de kendi kimliği, başlangıcı, bitişi ve tarihçesi olan bir
şeydir. **Şube** kaynaktır. Buna karşılık **ödünç süresi** kaynak değildir: on dört günlük
süre bir kuraldır, ödünç kaydının içinde ya da servisin yapılandırmasında yaşar.
**Gecikme ücreti** de tek başına kaynak değildir; bir üyenin borcunun bileşenidir.

Kaynağın kendisi soyuttur. İstemciye giden JSON gövde, kaynağın o andaki **gösterimidir**
(representation). Aynı kaynağın birden çok gösterimi olabilir: özet liste satırı ile
ayrıntı sayfası aynı ödünç kaydını farklı ayrıntı düzeyinde anlatır. Kaynak ile gösterimi
ayırmak, sonraki derslerde alan seçimi ve kısmi yanıt konularını mümkün kılan ayrımdır.

#### Eylemleri Adres Adı Yapmanın Bedeli

Kaynak düşüncesi olmadan bir servis yazıldığında adresler eylem adlarına dönüşür:
`/oduncVer`, `/oduncIade`, `/uyeninOduncleri`. Her yeni istemci ihtiyacı yeni bir yol açar.
Aşağıdaki iki sunucu aynı verinin üstünde aynı soruları yanıtlar; farkları adres kümesinin
nasıl büyüdüğüdür.

```js
// eylem-sunucusu.mjs — her istemci ihtiyacina bir yol acan tasarim
import { createServer } from "node:http";

const KITAPLAR = [
  { isbn: "978-0201896831", ad: "Programlama Sanati", yazar: "Knuth", sube: "S-01" },
  { isbn: "978-0262033848", ad: "Algoritmalara Giris", yazar: "Cormen", sube: "S-02" },
];
const ODUNCLER = [
  { id: 1, uye: "U-1001", isbn: "978-0262033848", iade: null },
  { id: 2, uye: "U-1002", isbn: "978-0201896831", iade: "2026-03-04" },
];

const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

// Her satir bir istemci ihtiyaci; ihtiyac artinca satir sayisi artar.
const YOLLAR = {
  "/kitapListesi":      () => KITAPLAR,
  "/kitapDetay":        (s) => KITAPLAR.find((k) => k.isbn === s.get("isbn")) ?? null,
  "/kitapEkle":         () => ({ sonuc: "eklendi" }),
  "/kitapSil":          () => ({ sonuc: "silindi" }),
  "/oduncListesi":      () => ODUNCLER,
  "/oduncDetay":        (s) => ODUNCLER.find((o) => o.id === Number(s.get("id"))) ?? null,
  "/oduncVer":          () => ({ sonuc: "verildi" }),
  "/oduncIade":         () => ({ sonuc: "iade alindi" }),
  "/uyeninOduncleri":   (s) => ODUNCLER.filter((o) => o.uye === s.get("uye")),
  "/subedekiKitaplar":  (s) => KITAPLAR.filter((k) => k.sube === s.get("sube")),
  "/acikOduncler":      () => ODUNCLER.filter((o) => o.iade === null),
};

const sunucu = createServer((istek, yanit) => {
  const adres = new URL(istek.url, "http://127.0.0.1");
  if (adres.pathname === "/yollar") return yanitla(yanit, 200, { sayi: Object.keys(YOLLAR).length });
  const isleyici = YOLLAR[adres.pathname];
  if (!isleyici) return yanitla(yanit, 404, { hata: "yol_yok" });
  yanitla(yanit, 200, isleyici(adres.searchParams));
});

sunucu.listen(8471, "127.0.0.1", () => console.log("eylem sunucusu 127.0.0.1:8471"));
```

```js
// kaynak-sunucusu.mjs — ayni ihtiyaclari kaynak adresleriyle karsilayan tasarim
import { createServer } from "node:http";

const KITAPLAR = [
  { isbn: "978-0201896831", ad: "Programlama Sanati", yazar: "Knuth", sube: "S-01" },
  { isbn: "978-0262033848", ad: "Algoritmalara Giris", yazar: "Cormen", sube: "S-02" },
];
const ODUNCLER = [
  { id: 1, uye: "U-1001", isbn: "978-0262033848", iade: null },
  { id: 2, uye: "U-1002", isbn: "978-0201896831", iade: "2026-03-04" },
];

const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

// Uc adres kalibi; suzme olcutleri sorgu bolumunde tasinir.
const KALIPLAR = [
  [/^\/kitaplar$/, (e, s) => {
    const sube = s.get("sube");
    return sube ? KITAPLAR.filter((k) => k.sube === sube) : KITAPLAR;
  }],
  [/^\/kitaplar\/([\w-]+)$/, (e) => KITAPLAR.find((k) => k.isbn === e[1]) ?? null],
  [/^\/oduncler$/, (e, s) => {
    let sonuc = ODUNCLER;
    if (s.get("uye")) sonuc = sonuc.filter((o) => o.uye === s.get("uye"));
    if (s.get("durum") === "acik") sonuc = sonuc.filter((o) => o.iade === null);
    return sonuc;
  }],
  [/^\/oduncler\/(\d+)$/, (e) => ODUNCLER.find((o) => o.id === Number(e[1])) ?? null],
];

const sunucu = createServer((istek, yanit) => {
  const adres = new URL(istek.url, "http://127.0.0.1");
  if (adres.pathname === "/kaliplar") return yanitla(yanit, 200, { sayi: KALIPLAR.length });
  for (const [kalip, isleyici] of KALIPLAR) {
    const eslesme = kalip.exec(adres.pathname);
    if (eslesme) return yanitla(yanit, 200, isleyici(eslesme, adres.searchParams));
  }
  yanitla(yanit, 404, { hata: "yol_yok" });
});

sunucu.listen(8472, "127.0.0.1", () => console.log("kaynak sunucusu 127.0.0.1:8472"));
```

Her iki sunucu da uzun ömürlüdür; aşağıdaki betik ikisini de başlatır, aynı soruları
sorar ve sonunda durdurur.

```bash
# Iki sunucuyu sirayla baslatir, ayni dort soruyu her ikisine sorar, durdurur.
node eylem-sunucusu.mjs & eylem=$!
sleep 0.4
node kaynak-sunucusu.mjs & kaynak=$!
sleep 0.4

sor() { printf '%-22s %s\n' "$1" "$(curl -sS "$2")"; }

echo "--- eylem tabanli ---"
sor "sube S-01 kitaplari" "http://127.0.0.1:8471/subedekiKitaplar?sube=S-01"
sor "U-1001 oduncleri"    "http://127.0.0.1:8471/uyeninOduncleri?uye=U-1001"
sor "acik oduncler"       "http://127.0.0.1:8471/acikOduncler"
sor "yol sayisi"          "http://127.0.0.1:8471/yollar"

echo "--- kaynak tabanli ---"
sor "sube S-01 kitaplari" "http://127.0.0.1:8472/kitaplar?sube=S-01"
sor "U-1001 oduncleri"    "http://127.0.0.1:8472/oduncler?uye=U-1001"
sor "acik oduncler"       "http://127.0.0.1:8472/oduncler?durum=acik"
sor "adres kalibi sayisi" "http://127.0.0.1:8472/kaliplar"

kill $eylem $kaynak
```

```
eylem sunucusu 127.0.0.1:8471
kaynak sunucusu 127.0.0.1:8472
--- eylem tabanli ---
sube S-01 kitaplari    [{"isbn":"978-0201896831","ad":"Programlama Sanati","yazar":"Knuth","sube":"S-01"}]
U-1001 oduncleri       [{"id":1,"uye":"U-1001","isbn":"978-0262033848","iade":null}]
acik oduncler          [{"id":1,"uye":"U-1001","isbn":"978-0262033848","iade":null}]
yol sayisi             {"sayi":11}
--- kaynak tabanli ---
sube S-01 kitaplari    [{"isbn":"978-0201896831","ad":"Programlama Sanati","yazar":"Knuth","sube":"S-01"}]
U-1001 oduncleri       [{"id":1,"uye":"U-1001","isbn":"978-0262033848","iade":null}]
acik oduncler          [{"id":1,"uye":"U-1001","isbn":"978-0262033848","iade":null}]
adres kalibi sayisi    {"sayi":4}
```

Yanıt gövdeleri satır satır aynı; sayılar aynı değil. On bir yola karşılık dört adres
kalıbı. Fark, kalıpların **istemci ihtiyacını değil kaynağı** adlandırmasından doğar.
"Şubedeki kitaplar" ve "açık ödünçler" ayrı kavramlar değildir; ikisi de var olan bir
koleksiyonun süzülmüş görünümüdür ve süzme ölçütü adresin sorgu bölümünde taşınır.

Sayının kendisi asıl sorun değildir. Asıl sorun, eylem tabanlı listenin **kapalı
olmamasıdır**. Bir ekran "geciken ödünçler" isterse liste on ikiye çıkar; başka bir ekran
"şubedeki geciken ödünçler" isterse on üçe. Kaynak tabanlı tarafta yeni bir sorgu ölçütü
eklenir, adres kümesi aynı kalır. Uç nokta sayısıyla ihtiyaç sayısı arasındaki bu
bağın kopması, kaynak modellemenin ölçülebilir çıktısıdır.

#### İşlem de Bir Kaynağın Durum Değişimidir

Eylem tabanlı listede en dikkat çeken satır `/oduncIade` idi. İade bir fiildir, dolayısıyla
kaynak gibi görünmez. Ama iade edilen şey, sistemde zaten kimliği olan bir ödünç kaydıdır;
iade o kaydın `iade` alanının boş olmaktan çıkmasıdır. Yeni adres gerekmez, var olan
kaynağın durumu değişir.

Aşağıdaki servis bunu kütüphane şeması üstünde gösterir. Şema tüm ders boyunca aynıdır:
şube, kitap, üye ve ödünç tabloları.

```sql
-- sema.sql — kutuphane odunc servisinin cekirdek semasi
CREATE TABLE sube (
  kod TEXT PRIMARY KEY,
  ad  TEXT NOT NULL
);
CREATE TABLE kitap (
  isbn  TEXT PRIMARY KEY,
  ad    TEXT NOT NULL,
  yazar TEXT NOT NULL,
  yil   INTEGER NOT NULL,
  sube  TEXT NOT NULL REFERENCES sube(kod)
);
CREATE TABLE uye (
  kod  TEXT PRIMARY KEY,
  ad   TEXT NOT NULL,
  sube TEXT NOT NULL REFERENCES sube(kod)
);
CREATE TABLE odunc (
  id      INTEGER PRIMARY KEY,
  uye     TEXT NOT NULL REFERENCES uye(kod),
  isbn    TEXT NOT NULL REFERENCES kitap(isbn),
  verilis TEXT NOT NULL,
  iade    TEXT
);

INSERT INTO sube VALUES ('S-01','Merkez'), ('S-02','Yenisehir');
INSERT INTO kitap VALUES
  ('978-0201896831','Programlama Sanati','Knuth',1968,'S-01'),
  ('978-0262033848','Algoritmalara Giris','Cormen',1990,'S-02'),
  ('978-0131103627','C Programlama Dili','Ritchie',1978,'S-01');
INSERT INTO uye VALUES ('U-1001','Aylin Deren','S-01'), ('U-1002','Berk Tuna','S-02');
INSERT INTO odunc (uye, isbn, verilis, iade) VALUES
  ('U-1001','978-0262033848','2026-03-01',NULL),
  ('U-1001','978-0131103627','2026-02-10','2026-02-24'),
  ('U-1002','978-0201896831','2026-03-04',NULL);
```

```js
// kaynak-servisi.mjs — odunc bir kaynaktir; iade onun durum degisimidir
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const govdeOku = (istek) => new Promise((coz) => {
  let veri = ""; istek.on("data", (p) => (veri += p));
  istek.on("end", () => coz(veri ? JSON.parse(veri) : {}));
});
const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

const sunucu = createServer(async (istek, yanit) => {
  const yol = new URL(istek.url, "http://127.0.0.1").pathname;
  const odunc = /^\/oduncler\/(\d+)$/.exec(yol);
  const durum = /^\/uyeler\/([\w-]+)\/durum$/.exec(yol);

  // Tekil odunc kaydi: bir tablo satirina karsilik gelir.
  if (odunc && istek.method === "GET") {
    const satir = db.prepare("SELECT * FROM odunc WHERE id = ?").get(Number(odunc[1]));
    return satir ? yanitla(yanit, 200, satir) : yanitla(yanit, 404, { hata: "odunc_yok" });
  }
  // Iade: yeni bir adres degil, var olan kaynagin durum degisimi.
  if (odunc && istek.method === "PATCH") {
    const govde = await govdeOku(istek);
    db.prepare("UPDATE odunc SET iade = ? WHERE id = ? AND iade IS NULL")
      .run(govde.iade, Number(odunc[1]));
    return yanitla(yanit, 200, db.prepare("SELECT * FROM odunc WHERE id = ?").get(Number(odunc[1])));
  }
  // Uye durumu: hicbir tabloda satiri yok, iki tablodan hesaplanir.
  if (durum && istek.method === "GET") {
    const sayilar = db.prepare(`
      SELECT COUNT(*) AS toplam, SUM(iade IS NULL) AS acik
      FROM odunc WHERE uye = ?`).get(durum[1]);
    const uye = db.prepare("SELECT ad, sube FROM uye WHERE kod = ?").get(durum[1]);
    if (!uye) return yanitla(yanit, 404, { hata: "uye_yok" });
    return yanitla(yanit, 200, {
      uye: durum[1], ad: uye.ad, sube: uye.sube,
      acikOdunc: sayilar.acik, toplamOdunc: sayilar.toplam,
      yeniOduncAlabilir: sayilar.acik < 2,
    });
  }
  yanitla(yanit, 404, { hata: "yol_yok" });
});

sunucu.listen(8473, "127.0.0.1", () => console.log("kaynak servisi 127.0.0.1:8473"));
```

```bash
# Semayi kurar, sunucuyu baslatir, odunc kaydini okur, iade eder, durumu sorar.
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql
node kaynak-servisi.mjs & sunucu=$!
sleep 0.4

curl -sS http://127.0.0.1:8473/oduncler/1; echo
curl -sS -X PATCH -H 'content-type: application/json' \
     -d '{"iade":"2026-03-12"}' http://127.0.0.1:8473/oduncler/1; echo
curl -sS http://127.0.0.1:8473/uyeler/U-1001/durum; echo
sqlite3 kutuphane.db "SELECT COUNT(*) || ' satir, acik: ' || SUM(iade IS NULL) FROM odunc;"

kill $sunucu
```

```
kaynak servisi 127.0.0.1:8473
{"id":1,"uye":"U-1001","isbn":"978-0262033848","verilis":"2026-03-01","iade":null}
{"id":1,"uye":"U-1001","isbn":"978-0262033848","verilis":"2026-03-01","iade":"2026-03-12"}
{"uye":"U-1001","ad":"Aylin Deren","sube":"S-01","acikOdunc":0,"toplamOdunc":2,"yeniOduncAlabilir":true}
3 satir, acik: 1
```

Son satır önemlidir: iade yeni bir satır üretmedi, var olan satırın bir alanı doldu.
Tablodaki kayıt sayısı üçtür ve öyle kalır. İade için ayrı bir adres açmak, sistemde ayrı
bir şey yaratıldığı yanılsamasını verir; oysa yaratılan bir şey yoktur.

Bunun tersi de doğrudur ve abartıya kaçmamak gerekir. Bir geçişin kendi kuralları,
kendi yetkisi ve kendi kaydı varsa — ödüncün başka bir üyeye devri gibi — o geçişi kaynağa
bağlı ayrı bir adres olarak açmak savunulabilir. Ölçüt yine kimliktir: geçişin kendisi
sorgulanabilir bir kayıt bırakıyorsa kaynak olmayı hak eder, bırakmıyorsa alan değişimidir.

#### Tablo ile Kaynak Aynı Şey Değil

Çıktının üçüncü satırı `/uyeler/U-1001/durum` adresine aittir ve bu kaynağın hiçbir tabloda
satırı yoktur. Açık ödünç sayısı, toplam ödünç sayısı ve yeni ödünç alma hakkı iki
tablodan hesaplanır. Yine de kaynaktır: kimliği sabittir, istemci ona doğrudan başvurur ve
sonucu tek bir gösterimdir. Kütüphane görevlisinin ekranı bu tek adresi çağırır; üç ayrı
sorgu yapıp kuralı istemcide uygulamaz.

Karşılık kurmak tek yönlü de değildir. Tek bir tablo birden çok kaynağı besleyebilir:
`odunc` tablosu hem ödünç kayıtları koleksiyonunu hem de bir üyenin ödünç geçmişini
besler. Kaynak modeli, veritabanı şemasının dışa yansıtılması değildir; şema depolama için
en iyi biçimi arar, kaynak modeli istemcinin başvurmak isteyeceği kavramları arar. İkisinin
çoğu zaman örtüşmesi, aynı şey oldukları anlamına gelmez — ve bu ayrım korunmazsa
veritabanında yapılan her yeniden düzenleme dışarıdaki sözleşmeyi kırar.

#### Kimlik Seçiminin Bedeli

Kaynak kimliği olarak ne kullanılacağı bir tasarım kararıdır. İki seçenek vardır: Veri
Modelleme ve İlişkisel Kuram kursunda tanımlanan **doğal anahtar** — kitap için ISBN — ve
**vekil anahtar** — ödünç kaydı için üretilen sayı. Doğal anahtar okunabilir ve
anlamlıdır; vekil anahtar anlamsızdır ama değişmez.

Fark, veri düzeltildiğinde ortaya çıkar.

```bash
# Kaynak kimligi degisince ona yapilan basvurular ne olur?
rm -f kimlik.db
sqlite3 kimlik.db <<'SQL'
CREATE TABLE kitap (isbn TEXT PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE odunc (id INTEGER PRIMARY KEY, isbn TEXT NOT NULL REFERENCES kitap(isbn));
INSERT INTO kitap VALUES ('978-0262033848', 'Algoritmalara Giris');
INSERT INTO odunc (isbn) VALUES ('978-0262033848');
-- Katalog kaydindaki ISBN yanlis girilmis; duzeltiliyor.
UPDATE kitap SET isbn = '978-0262046305' WHERE isbn = '978-0262033848';
SELECT 'kitap tablosu : ' || isbn FROM kitap;
SELECT 'odunc tablosu : ' || isbn FROM odunc;
SELECT 'yetim kayit   : ' || COUNT(*)
  FROM odunc o LEFT JOIN kitap k ON k.isbn = o.isbn WHERE k.isbn IS NULL;
SQL
```

```
kitap tablosu : 978-0262046305
odunc tablosu : 978-0262033848
yetim kayit   : 1
```

Tek bir düzeltme bir yetim kayıt üretti. Bu motorda yabancı anahtar denetimi varsayılan
olarak kapalıdır ve `PRAGMA foreign_keys = ON` ile açılır; açık olsaydı güncelleme
reddedilir, düzeltme yapılamazdı. İki durumda da bedel vardır: ya başvuru kopar ya
düzeltme engellenir.

API tarafındaki karşılığı daha ağırdır, çünkü başvurular sistemin dışındadır.
`/kitaplar/978-0262033848` adresi bir kütüğe yazılmış, bir e-postaya konmuş, başka bir
servisin yapılandırmasına gömülmüş olabilir. Kimlik değişince bu adres artık bulunamaz.
Bu yüzden kaynak kimliği için ölçüt "anlamlı mı" değil, **"değişmeyeceğinden emin miyiz"**
sorusudur. Değişebilecek doğal anahtarlar kaynağın bir alanı olarak durur ve arama ölçütü
diye sunulur; kimlik için vekil anahtar kullanılır.

#### Özet

- Kaynak, kimliği zaman içinde sabit kalan ve istemcinin doğrudan başvurduğu şeydir; her
  alan kavramı kaynak değildir, bazıları kaynağın alanı ya da servisin kuralıdır.
- Kaynağın kendisi soyuttur; istemciye giden gövde onun gösterimidir ve aynı kaynağın
  birden çok gösterimi olabilir.
- Eylem adlarıyla açılan uç noktaların sayısı istemci ihtiyaçlarıyla birlikte büyür; aynı
  ihtiyaçlar dört adres kalıbıyla karşılandığında bu bağ kopar.
- İade gibi geçişler yeni kaynak yaratmaz, var olan kaynağın durumunu değiştirir; kendi
  kaydını bırakan geçişler ise ayrı kaynak olmayı hak eder.
- Kaynak modeli veritabanı şemasının yansıması değildir: bir kaynak hiçbir tabloya
  karşılık gelmeyebilir, bir tablo birden çok kaynağı besleyebilir.
- Kimlik için ölçüt anlamlılık değil değişmezliktir; değişebilen doğal anahtarlar kimlik
  yerine alan olarak taşınır.

#### Sonraki Adım

Kaynaklar belirlendi, ama adreslerinin nasıl yazılacağı henüz kararlaştırılmadı. Bu derste
`/kitaplar`, `/oduncler/1` ve `/uyeler/U-1001/durum` biçimleri sorgulanmadan kullanıldı;
oysa her biri bir seçimdi. Koleksiyon adı tekil mi çoğul mu olur, bir ödünç kaydına
`/oduncler/1` üzerinden mi yoksa `/uyeler/U-1001/oduncler/1` üzerinden mi başvurulur, iki
yol da açılırsa hangisi doğru sayılır? Sonraki ders yol düzenini bir eşleyici tablosu
kurarak ele alır ve iç içe adreslerin hangi noktada belirsizlik ürettiğini ölçer.

### URI Tasarımı

Source: https://academia.sh/tr/kurslar/api-tasarimi/uri-tasarimi

Kaynak adreslerinin yol düzeni: yol ile sorgu bölümünün işbölümü, koleksiyon ve tekil kaynak adlandırması, sabit dilimle parametre diliminin çakışması, iç içe yolların üst dilimi ve kanonik adres kararı.

Bir önceki ders kaynakları belirledi: kitap, üye, ödünç kaydı, şube ve tabloya karşılık
gelmeyen üye durumu. O dersin örneklerinde `/kitaplar`, `/oduncler/1` ve
`/uyeler/U-1001/durum` gibi adresler sorgulanmadan kullanıldı. Oysa her biri bir karardı ve
her kararın alternatifi vardı.

Bu ders o kararları açar. Adresin hangi parçası kaynağın kimliğini taşır, hangi parçası
görünümünü belirler; koleksiyon adı nasıl yazılır; bir ödünç kaydına iki farklı yoldan
başvurulabildiğinde hangisi geçerli sayılır; sondaki eğik çizgi neyi değiştirir. Bunların
hepsi, bir eşleyici tablosu çalıştırıldığında ölçülebilir sonuçlar üretir.

#### Yolun ve Sorgunun İşbölümü

İnternet Nasıl Çalışır kursunda adresin bölümleri tanıtılmıştı: şema, konak, yol, sorgu ve
parça. API tasarımında bu bölümler arasında keskin bir işbölümü vardır.

**Yol kaynağın kimliğini taşır.** `/kitaplar/978-0131103627` belirli bir kitabı gösterir;
o kitap başka bir yolda bulunmaz. **Sorgu bölümü kaynağın görünümünü belirler.**
`/oduncler?durum=acik` yeni bir kaynak değildir, ödünç koleksiyonunun süzülmüş bir
görünümüdür. Aynı ayrım sıralama, sayfalama ve alan seçimi için de geçerlidir; bunların
hepsi sonraki derslerin konusudur ve hepsi sorgu bölümünde yaşar.

Bu işbölümünün pratik sonucu şudur: **yolda fiil bulunmaz.** `/oduncIade`, `/kitapAra`,
`/uyeSil` gibi adresler kaynağı değil işlemi adlandırır. Ne yapılacağını söyleyen şey
yöntemdir; yol yalnızca neye yapılacağını söyler. Bir sonraki ders yöntemlerin bu görevi
nasıl üstlendiğini ayrıntısıyla ele alır.

Koleksiyon adları çoğul yazılır: `/kitaplar`, `/uyeler`, `/oduncler`. Gerekçe estetik
değildir. `/kitap/978-0131103627` ile `/kitaplar` bir arada kullanıldığında istemci
tarafındaki her adres kurma noktası, tekil mi çoğul mu olduğunu ayrıca hatırlamak zorunda
kalır. Tek bir kural — koleksiyon çoğul, üyesi onun altında — bu hatırlama yükünü sıfırlar.
Hangi kuralın seçildiği ikincildir; kurs boyunca değişmemesi birincildir.

#### Sabit Dilim ile Parametre Diliminin Çakışması

Adres kalıpları bir tablodur ve tablo sırayla taranır. Sıra yanlış olduğunda hata
sessizdir. Aşağıdaki eşleyici, hangi kalıbın eşleştiğini yanıtın içinde bildirir; böylece
karar görünür olur.

```js
// yol-esleyici.mjs — adres kaliplarini sirayla deneyen esleyici; eslesen kalibi bildirir
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

// Sira onemlidir: liste yukaridan asagi taranir, ilk eslesen kazanir.
const KALIPLAR = [
  ["/kitaplar/{isbn}", /^\/kitaplar\/([^/]+)$/, (e) =>
    db.prepare("SELECT * FROM kitap WHERE isbn = ?").get(e[1]) ?? null],
  ["/kitaplar/ara",    /^\/kitaplar\/ara$/, () =>
    db.prepare("SELECT isbn, ad FROM kitap WHERE yazar LIKE 'K%'").all()],
  ["/uyeler/{kod}/oduncler/{id}", /^\/uyeler\/([^/]+)\/oduncler\/(\d+)$/, (e) =>
    db.prepare("SELECT * FROM odunc WHERE id = ?").get(Number(e[2])) ?? null],
];

const sunucu = createServer((istek, yanit) => {
  const yol = new URL(istek.url, "http://127.0.0.1").pathname;
  for (const [ad, kalip, isleyici] of KALIPLAR) {
    const eslesme = kalip.exec(yol);
    if (eslesme) return yanitla(yanit, 200, { eslesen: ad, sonuc: isleyici(eslesme) });
  }
  yanitla(yanit, 404, { hata: "yol_yok" });
});

sunucu.listen(8474, "127.0.0.1", () => console.log("yol esleyici 127.0.0.1:8474"));
```

Şema, bir önceki derste kurulan `sema.sql` dosyasıdır; betik veritabanını her çalıştırmada
yeniden üretir.

```bash
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql
node yol-esleyici.mjs & sunucu=$!
sleep 0.4

iste() { printf '%-34s %s\n' "$1" "$(curl -sS "http://127.0.0.1:8474$1")"; }
iste "/kitaplar/978-0131103627"
iste "/kitaplar/ara"
iste "/uyeler/U-1002/oduncler/1"
iste "/kitaplar/978-0131103627/"
iste "/Kitaplar/978-0131103627"

kill $sunucu
```

```
yol esleyici 127.0.0.1:8474
/kitaplar/978-0131103627           {"eslesen":"/kitaplar/{isbn}","sonuc":{"isbn":"978-0131103627","ad":"C Programlama Dili","yazar":"Ritchie","yil":1978,"sube":"S-01"}}
/kitaplar/ara                      {"eslesen":"/kitaplar/{isbn}","sonuc":null}
/uyeler/U-1002/oduncler/1          {"eslesen":"/uyeler/{kod}/oduncler/{id}","sonuc":{"id":1,"uye":"U-1001","isbn":"978-0262033848","verilis":"2026-03-01","iade":null}}
/kitaplar/978-0131103627/          {"hata":"yol_yok"}
/Kitaplar/978-0131103627           {"hata":"yol_yok"}
```

Beş satırda dört ayrı sorun görünür.

İkinci satır çakışmadır. `/kitaplar/ara` adresi arama kalıbına hiç ulaşmadı; parametre
dilimi olan `{isbn}` onu önce yakaladı ve `ara` dizgisini ISBN sanıp veritabanında aradı.
Sonuç `null` döndü — yani istemci, arama uç noktasının bozuk olduğunu değil, aradığı
kitabın bulunmadığını düşünür. Uygulama Mimarisi kursunda tanımlanan **özgüllük** ölçütü
burada da geçerlidir: sabit dilimli kalıplar parametre dilimli kalıplardan önce
denenmelidir. Sıraya güvenmek istemiyorsanız ikinci yol vardır — arama ölçütünü koleksiyon
adresinin sorgu bölümüne taşımak: `/kitaplar?yazar=Knuth`. Bu yazımda çakışacak bir dilim
kalmaz.

Üçüncü satır daha ağırdır. `/uyeler/U-1002/oduncler/1` adresine U-1001 üyesinin ödünç
kaydı döndü. Kalıp üst dilimi yakaladı ama işleyici onu hiç kullanmadı; yalnızca
`{id}` ile sorgu yaptı. İç içe yol burada süsleme durumuna düşmüştür: adres bir sahiplik
iddiası taşır, sunucu o iddiayı denetlemez.

Dördüncü ve beşinci satırlar kanonik biçim sorunudur. Sondaki eğik çizgi ve büyük harf,
yolun eşleşmemesine yol açtı. İkisi de "bulunamadı" olarak döndü, oysa kaynak duruyor.

#### Üst Dilimin Doğrulanması ve Kanonik Biçim

Aşağıdaki eşleyici üç düzeltmeyi birlikte uygular: sabit dilim öne alınır, üst dilim sorguya
katılır, sondaki eğik çizgi kanonik adrese yönlendirilir.

```js
// yol-esleyici-2.mjs — ozgullugu once sinayan, ust dilimi dogrulayan esleyici
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

// Sabit dilimli kaliplar once; parametre dilimi en sonda kalir.
const KALIPLAR = [
  ["/kitaplar/ara",    /^\/kitaplar\/ara$/, () =>
    [200, db.prepare("SELECT isbn, ad FROM kitap WHERE yazar LIKE 'K%'").all()]],
  ["/kitaplar/{isbn}", /^\/kitaplar\/([^/]+)$/, (e) => {
    const k = db.prepare("SELECT * FROM kitap WHERE isbn = ?").get(e[1]);
    return k ? [200, k] : [404, { hata: "kitap_yok" }];
  }],
  ["/uyeler/{kod}/oduncler/{id}", /^\/uyeler\/([^/]+)\/oduncler\/(\d+)$/, (e) => {
    // Ust dilim suslemedir demek yanlistir: kayit o uyeye ait degilse yol yoktur.
    const o = db.prepare("SELECT * FROM odunc WHERE id = ? AND uye = ?").get(Number(e[2]), e[1]);
    return o ? [200, o] : [404, { hata: "odunc_yok" }];
  }],
];

const sunucu = createServer((istek, yanit) => {
  let yol = new URL(istek.url, "http://127.0.0.1").pathname;
  // Kanonik bicim: sondaki egik cizgi tek adrese indirgenir.
  if (yol.length > 1 && yol.endsWith("/")) {
    yanit.writeHead(308, { location: yol.slice(0, -1) });
    return yanit.end();
  }
  for (const [ad, kalip, isleyici] of KALIPLAR) {
    const eslesme = kalip.exec(yol);
    if (eslesme) {
      const [kod, sonuc] = isleyici(eslesme);
      return yanitla(yanit, kod, { eslesen: ad, sonuc });
    }
  }
  yanitla(yanit, 404, { hata: "yol_yok" });
});

sunucu.listen(8475, "127.0.0.1", () => console.log("duzeltilmis esleyici 127.0.0.1:8475"));
```

```bash
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql
node yol-esleyici-2.mjs & sunucu=$!
sleep 0.4

iste() { printf '%-30s %3s  %s\n' "$1" \
  "$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:8475$1")" \
  "$(curl -sS "http://127.0.0.1:8475$1")"; }
iste "/kitaplar/ara"
iste "/uyeler/U-1002/oduncler/1"
iste "/uyeler/U-1001/oduncler/1"
iste "/kitaplar/978-0131103627/"

# Ayni adres, yonlendirme izlenerek:
printf '%-30s %3s  %s\n' "/kitaplar/978-0131103627/ (-L)" \
  "$(curl -sSL -o /dev/null -w '%{http_code}' http://127.0.0.1:8475/kitaplar/978-0131103627/)" \
  "$(curl -sSL http://127.0.0.1:8475/kitaplar/978-0131103627/)"

kill $sunucu
```

```
duzeltilmis esleyici 127.0.0.1:8475
/kitaplar/ara                  200  {"eslesen":"/kitaplar/ara","sonuc":[{"isbn":"978-0201896831","ad":"Programlama Sanati"}]}
/uyeler/U-1002/oduncler/1      404  {"eslesen":"/uyeler/{kod}/oduncler/{id}","sonuc":{"hata":"odunc_yok"}}
/uyeler/U-1001/oduncler/1      200  {"eslesen":"/uyeler/{kod}/oduncler/{id}","sonuc":{"id":1,"uye":"U-1001","isbn":"978-0262033848","verilis":"2026-03-01","iade":null}}
/kitaplar/978-0131103627/      308
/kitaplar/978-0131103627/ (-L) 200  {"eslesen":"/kitaplar/{isbn}","sonuc":{"isbn":"978-0131103627","ad":"C Programlama Dili","yazar":"Ritchie","yil":1978,"sube":"S-01"}}
```

İkinci ve üçüncü satır birlikte okunmalıdır: aynı ödünç kaydı, iki farklı üst dilimle iki
farklı sonuç verir. Üst dilim artık iddia değil koşuldur. Bu, güvenlik değil doğruluk
kararıdır; yetkilendirme ayrı bir konudur ve Kimlik Doğrulama ve Yetkilendirme kursunda ele
alınır. Buradaki kazanç şudur: adres neyi söylüyorsa sunucu onu doğrular.

Dördüncü satır yönlendirmeyi gösteriyor: sondaki eğik çizgili adres 308 ile kanonik adrese
çevrilir, gövde boştur. Beşinci satırda aynı istek yönlendirme izlenerek yapılır ve 200
ile kaynağa ulaşır. Yönlendirmenin bu türü yöntemi ve gövdeyi korur; bu ayrım, yazma
isteklerinin yönlendirilmesi gerektiğinde belirleyici olur.

Büyük harf sorunu bilinçli olarak düzeltilmedi. Yol büyük–küçük harfe duyarlıdır ve
duyarlı kalmalıdır; `/Kitaplar` yolunu da kabul etmek, aynı kaynak için sonsuz sayıda
adres üretmek demektir. Doğru karar tek yazımı belgelemek ve diğerlerini reddetmektir.

#### Kimliğin İçindeki Ayraçlar

Yol dilimlerini ayıran karakter eğik çizgidir; kimlik değeri o karakteri içeriyorsa adres
bozulur.

```bash
# Kimlikte yol ayraci gecerse ne olur?
node -e '
const kod = "S-01/Merkez";
console.log("ham        :", "/subeler/" + kod);
console.log("kodlanmis  :", "/subeler/" + encodeURIComponent(kod));
const adres = new URL("http://127.0.0.1/subeler/" + encodeURIComponent(kod));
console.log("cozulen    :", decodeURIComponent(adres.pathname.split("/")[2]));
'
```

```
ham        : /subeler/S-01/Merkez
kodlanmis  : /subeler/S-01%2FMerkez
cozulen    : S-01/Merkez
```

Ham yazımda tek bir kimlik iki dilime bölünür ve eşleyici onu alt kaynak sanır. Kodlanmış
yazımda dilim bütünlüğünü korur. Bu, istemcinin sorumluluğudur ve sözleşmede yazılı olmalıdır.
Daha sağlam olan yol ise kimliğin biçimini baştan kısıtlamaktır: ayraç, boşluk ve büyük harf
içermeyen kimlikler bu sorunu hiç doğurmaz.

#### Kanonik Adres Kararı

Bir kaynağa birden çok yoldan ulaşılabilir: `/oduncler/1` ve `/uyeler/U-1001/oduncler/1`
aynı kaydı verir. İkisi de geçerli olabilir, ama biri **kanonik adres** olarak seçilmelidir.
Seçim ölçütü şudur: kaynağın bağımsız kimliği varsa kanonik adres kısa olandır. Ödünç
kaydının kendi kimliği vardır, dolayısıyla `/oduncler/1` kanoniktir; iç içe yazım, üyenin
kayıtlarını gezerken kullanılan bir kısayoldur.

Kanonik adresin belirlenmesi üç yerde iş görür. Yanıt içinde bir kaynağa bağlantı
verilirken hangi biçimin yazılacağı bellidir. Yeni bir kayıt oluşturulduğunda `Location`
başlığına konacak adres bellidir. Önbellek anahtarı olarak adres kullanıldığında aynı
kaynak iki farklı anahtarla saklanmaz.

Derinlik de bu ölçütle sınırlanır. `/subeler/S-01/uyeler/U-1001/oduncler/1` gibi dört
düzeyli bir yol, dört ayrı doğrulama gerektirir ve dördü de her istekte yapılmalıdır. Kural
olarak bir düzey yeterlidir: koleksiyonun sahibi olan kaynak, sonra koleksiyon, sonra üyesi.
Daha derini gerekiyorsa bu, ilişkinin sorgu ölçütü olarak yazılmasının daha uygun olduğunun
işaretidir.

#### Özet

- Yol kaynağın kimliğini taşır, sorgu bölümü görünümünü belirler; yolda fiil bulunmaz,
  ne yapılacağını yöntem söyler.
- Koleksiyonlar çoğul adlandırılır ve bu karar kurs boyunca değişmez; tutarlılık,
  hangi kuralın seçildiğinden daha önemlidir.
- Sabit dilimli kalıplar parametre dilimlilerden önce denenmelidir; aksi hâlde
  `/kitaplar/ara` adresi ISBN sanılır ve hata "bulunamadı" gibi görünür.
- İç içe yolun üst dilimi süsleme değil koşuldur: kayıt o üyeye ait değilse yanıt 404
  olmalıdır.
- Her kaynağın tek bir kanonik adresi belirlenir; sondaki eğik çizgi gibi türevler 308 ile
  ona yönlendirilir, büyük harf türevleri hiç kabul edilmez.
- Kimlik değerleri yol ayracı içeriyorsa adres kodlanmalıdır; daha sağlam çözüm kimlik
  biçimini baştan kısıtlamaktır.

#### Sonraki Adım

Adresler yerleştiğine göre geriye eylemin nereden geldiği sorusu kalır. Bu derste yol
düzeninin fiil taşımadığı söylendi ve "ne yapılacağını yöntem söyler" denip geçildi. Ancak
yöntem seçmek bir ad seçmek değildir: her yöntemin, istemcinin ve aradaki bileşenlerin
güvendiği özellikleri vardır. Bir okumanın veriyi değiştirmediği, aynı isteğin iki kez
gönderilmesinin tek etki ürettiği varsayımları buradan gelir. Sonraki ders bu özellikleri
tanımlar ve her birini veritabanındaki satır sayısını sayarak sınar.

### HTTP Yöntemlerinin Doğru Kullanımı

Source: https://academia.sh/tr/kurslar/api-tasarimi/http-yontemlerinin-dogru-kullanimi

Yöntemin taşıdığı sözler: güvenli yöntemin veriyi değiştirmemesi ve etkisiz yöntemin tekrarlandığında tek etki üretmesi; her özelliğin veritabanındaki satır sayısı sayılarak sınanması, PATCH'in artırım ve atama yüzleri, PUT ile POST arasındaki kimlik seçimi.

Bir önceki ders yolu kaynağın kimliğine ayırdı ve "ne yapılacağını yöntem söyler" diyerek
geçti. O cümle bir görev dağılımından fazlasını içerir. HTTP yöntemi yalnızca sunucuya
gönderilen bir emir değildir; istemcinin, önbelleklerin ve aradaki her bileşenin isteğe
bakarak güvendiği bir sözdür.

Bu ders o sözleri tanımlar ve her birini ölçer. Ölçüm biçimi tektir: aynı istek arka arkaya
iki kez gönderilir ve veritabanındaki satır sayısı ile alan değerleri her adımda sayılır.
Bir yöntemin sözünü tuttuğu ya da tutmadığı, tartışmayla değil sayılarla belirlenir.

#### Yöntem Bir Ad Değil, Bir Söz

İnternet Nasıl Çalışır kursu iki özelliği tanıtmıştı. **Güvenli** (safe) bir yöntem sunucu
tarafında gözlenebilir bir değişiklik bırakmaz; okuma yapar, yazma yapmaz. **Etkisiz**
(idempotent) bir yöntem, aynı isteğin bir kez ya da arka arkaya beş kez gönderilmesi
arasında sunucunun son durumu bakımından fark bırakmaz. İkisi bağımsızdır: her güvenli
yöntem etkisizdir, ama her etkisiz yöntem güvenli değildir.

| Yöntem | Güvenli | Etkisiz | Gövde taşır |
|---|---|---|---|
| GET | evet | evet | hayır |
| HEAD | evet | evet | hayır |
| OPTIONS | evet | evet | hayır |
| POST | hayır | hayır | evet |
| PUT | hayır | evet | evet |
| PATCH | hayır | tasarıma bağlı | evet |
| DELETE | hayır | evet | genellikle hayır |

Bu özellikler sunucunun iç işi değildir; dışarıya verilmiş taahhütlerdir. Bir önbellek,
GET yanıtını saklayıp bir sonraki isteğe sunucuya hiç gitmeden yanıt verebilir — çünkü
GET'in bir yan etkisi olmadığına güvenir. Bir istemci, ağ hatası aldığında PUT isteğini
yeniden gönderebilir — çünkü tekrarın zararsız olduğuna güvenir. Uygulama Mimarisi
kursundaki istek katmanı bu güveni **yeniden denenebilirlik** olarak kodlamıştı.

Taahhüt tutulmazsa kırılan şey sunucu değil, sunucuya güvenen taraflardır.

#### Güvenlilik: Okuma Veriyi Değiştirmez

Aşağıdaki servis, ölçüm için bilerek bir kusur taşır: ödünç iadesi hem doğru yoldan hem de
okuma görünümlü bir adresten yapılabilir.

```js
// yontem-sunucusu.mjs — yontem ozelliklerini olcmek icin kurulmus servis
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const govdeOku = (istek) => new Promise((coz) => {
  let veri = ""; istek.on("data", (p) => (veri += p));
  istek.on("end", () => coz(veri ? JSON.parse(veri) : {}));
});
const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(nesne === null ? "" : JSON.stringify(nesne));
};

const sunucu = createServer(async (istek, yanit) => {
  const yol = new URL(istek.url, "http://127.0.0.1").pathname;
  const y = istek.method;
  const kitap = /^\/kitaplar\/([^/]+)$/.exec(yol);
  const odunc = /^\/oduncler\/(\d+)$/.exec(yol);
  const iadeYolu = /^\/oduncler\/(\d+)\/iade$/.exec(yol);

  if (y === "GET" && yol === "/oduncler")
    return yanitla(yanit, 200, db.prepare("SELECT * FROM odunc").all());

  // YANLIS: okuma gorunumlu bir adres veriyi degistiriyor.
  if (y === "GET" && iadeYolu) {
    db.prepare("UPDATE odunc SET iade = '2026-03-12' WHERE id = ? AND iade IS NULL")
      .run(Number(iadeYolu[1]));
    return yanitla(yanit, 200, { sonuc: "iade alindi" });
  }

  if (y === "POST" && yol === "/oduncler") {
    const g = await govdeOku(istek);
    const s = db.prepare("INSERT INTO odunc (uye, isbn, verilis, iade) VALUES (?,?,?,NULL)")
                .run(g.uye, g.isbn, g.verilis);
    return yanitla(yanit, 201, { id: Number(s.lastInsertRowid) });
  }

  if (y === "PUT" && kitap) {
    const g = await govdeOku(istek);
    // Tam degisim: gonderilen gosterim kaydin tamamini belirler.
    const varMi = db.prepare("SELECT 1 FROM kitap WHERE isbn = ?").get(kitap[1]);
    db.prepare(`INSERT INTO kitap (isbn, ad, yazar, yil, sube, kopya) VALUES (?,?,?,?,?,?)
                ON CONFLICT(isbn) DO UPDATE SET
                  ad=excluded.ad, yazar=excluded.yazar, yil=excluded.yil,
                  sube=excluded.sube, kopya=excluded.kopya`)
      .run(kitap[1], g.ad, g.yazar, g.yil, g.sube, g.kopya);
    return yanitla(yanit, varMi ? 200 : 201, db.prepare("SELECT * FROM kitap WHERE isbn = ?").get(kitap[1]));
  }

  if (y === "PATCH" && kitap) {
    const g = await govdeOku(istek);
    if ("kopyaEkle" in g)  // artirim: onceki degere baglidir
      db.prepare("UPDATE kitap SET kopya = kopya + ? WHERE isbn = ?").run(g.kopyaEkle, kitap[1]);
    if ("kopya" in g)      // atama: onceki degerden bagimsizdir
      db.prepare("UPDATE kitap SET kopya = ? WHERE isbn = ?").run(g.kopya, kitap[1]);
    return yanitla(yanit, 200, db.prepare("SELECT isbn, kopya FROM kitap WHERE isbn = ?").get(kitap[1]));
  }

  if (y === "DELETE" && odunc) {
    db.prepare("DELETE FROM odunc WHERE id = ?").run(Number(odunc[1]));
    return yanitla(yanit, 204, null);
  }

  yanitla(yanit, 404, { hata: "yol_yok" });
});

sunucu.listen(8476, "127.0.0.1", () => console.log("yontem sunucusu 127.0.0.1:8476"));
```

Şimdi bu servise, sayfadaki bağlantıları sırayla izleyen bir istemci gelsin. Böyle bir
istemci yalnızca GET yollar; bağlantının arkasında ne olduğunu bilmez.

```bash
# Okuma gorunumlu adresi bir baglanti tarayicisi izlerse ne olur?
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql
sqlite3 kutuphane.db "ALTER TABLE kitap ADD COLUMN kopya INTEGER NOT NULL DEFAULT 1;"
node yontem-sunucusu.mjs & sunucu=$!
sleep 0.4

acik() { sqlite3 kutuphane.db "SELECT COUNT(*) FROM odunc WHERE iade IS NULL;"; }
printf 'baslangicta acik odunc      : %s\n' "$(acik)"

# Sayfadaki butun baglantilari izleyen bir istemci: yalnizca GET yollar.
for yol in /oduncler /oduncler/1/iade /oduncler/3/iade; do
  printf 'GET %-22s -> %s\n' "$yol" "$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:8476$yol")"
done
printf 'GET sonrasi acik odunc      : %s\n' "$(acik)"

kill $sunucu
```

```
yontem sunucusu 127.0.0.1:8476
baslangicta acik odunc      : 2
GET /oduncler              -> 200
GET /oduncler/1/iade       -> 200
GET /oduncler/3/iade       -> 200
GET sonrasi acik odunc      : 0
```

İki açık ödünç kaydı, hiçbir kullanıcı iade işlemi yapmadan kapandı. İstemci kural
çiğnemedi: bağlantı izleyen bir program için GET, sorulması serbest bir sorudur. Kuralı
çiğneyen sunucudur.

Bu kusurun ölçeği tasarım hatasının ötesine geçer. Sayfa önizlemesi üreten bir araç, bağlantı
denetleyen bir betik, adres çubuğuna yazılanı önceden getiren bir tarayıcı — hepsi aynı
varsayımla çalışır. Şema iki tabloyla kurulmuş bir örnekte iki satır kaybolur; gerçek bir
katalogda kayıp, izlenen bağlantı sayısı kadardır.

Doğru tasarımda `/oduncler/{id}/iade` gibi bir adres hiç açılmaz; iade, bir önceki derste
gösterildiği gibi `PATCH /oduncler/{id}` isteğiyle yapılır. Adres okuma görünümlüyse yöntem
de okuma olmalıdır ve tersi.

#### Etkisizlik: Aynı İsteği İki Kez Göndermek

Etkisizlik ölçümü doğrudandır. Her yöntem arka arkaya iki kez çağrılır; iki çağrının
sonucu bir çağrınınkinden farklıysa yöntem etkisiz değildir.

```bash
# Her yontem iki kez cagrilir; her cagridan sonra veritabanindaki etki sayilir.
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql
sqlite3 kutuphane.db "ALTER TABLE kitap ADD COLUMN kopya INTEGER NOT NULL DEFAULT 1;"
node yontem-sunucusu.mjs & sunucu=$!
sleep 0.4

T=http://127.0.0.1:8476
say() { printf '%-26s odunc=%s  kopya=%s\n' "$1" \
  "$(sqlite3 kutuphane.db 'SELECT COUNT(*) FROM odunc;')" \
  "$(sqlite3 kutuphane.db "SELECT kopya FROM kitap WHERE isbn='978-0131103627';")"; }

say "baslangic"
curl -sS -o /dev/null $T/oduncler; curl -sS -o /dev/null $T/oduncler
say "GET x2"
for i in 1 2; do curl -sS -o /dev/null -X POST -H 'content-type: application/json' \
  -d '{"uye":"U-1002","isbn":"978-0131103627","verilis":"2026-03-10"}' $T/oduncler; done
say "POST x2"
for i in 1 2; do curl -sS -o /dev/null -X PUT -H 'content-type: application/json' \
  -d '{"ad":"C Programlama Dili","yazar":"Ritchie","yil":1988,"sube":"S-01","kopya":4}' \
  $T/kitaplar/978-0131103627; done
say "PUT x2"
for i in 1 2; do curl -sS -o /dev/null -X PATCH -H 'content-type: application/json' \
  -d '{"kopyaEkle":2}' $T/kitaplar/978-0131103627; done
say "PATCH artirim x2"
for i in 1 2; do curl -sS -o /dev/null -X PATCH -H 'content-type: application/json' \
  -d '{"kopya":6}' $T/kitaplar/978-0131103627; done
say "PATCH atama x2"
printf '%-26s %s %s\n' "DELETE x2 (durum kodu)" \
  "$(curl -sS -o /dev/null -w '%{http_code}' -X DELETE $T/oduncler/2)" \
  "$(curl -sS -o /dev/null -w '%{http_code}' -X DELETE $T/oduncler/2)"
say "DELETE x2"

kill $sunucu
```

```
yontem sunucusu 127.0.0.1:8476
baslangic                  odunc=3  kopya=1
GET x2                     odunc=3  kopya=1
POST x2                    odunc=5  kopya=1
PUT x2                     odunc=5  kopya=4
PATCH artirim x2           odunc=5  kopya=8
PATCH atama x2             odunc=5  kopya=6
DELETE x2 (durum kodu)     204 204
DELETE x2                  odunc=4  kopya=6
```

Satırlar tek tek okunmalıdır.

**GET x2** hiçbir sayıyı değiştirmedi: yöntem güvenlidir, dolayısıyla etkisizdir.

**POST x2** ödünç sayısını üçten beşe çıkardı. İki istek, iki kayıt üretti. POST'un bu tasarımdaki
karşılığı budur: koleksiyona yeni bir üye eklenir ve kimliği sunucu belirler. HTTP, POST'u
tek bir işleme bağlamaz — gövdenin kaynağın kendi anlamına göre işlenmesini ister;
koleksiyona ekleme bu işlemenin en yaygın biçimidir. Ağ koptuğu için
yeniden gönderilen bir POST, ikinci bir ödünç kaydı doğurur. Bu, tasarımın bozukluğu
değil, POST'un doğasıdır; çözümü dokuzuncu dersin konusudur.

**PUT x2** kopya sayısını 4 yaptı ve orada bıraktı. Gönderilen gövde kaydın tamamını
belirlediği için ikinci istek birinciyle aynı son durumu üretir. Kaç kez gönderildiği
sonucu değiştirmez.

**PATCH artırım x2** kopya sayısını 4'ten 8'e çıkardı. Gövde `kopyaEkle` alanını taşıyor,
yani yeni değer eskisine bağlı. Bu bir PATCH kusuru değildir; PATCH'in etkisizliğinin
gövdeye bağlı olduğunun kanıtıdır. **PATCH atama x2** aynı yöntemle 6 değerini üretti ve
ikinci istek onu değiştirmedi.

Buradan çıkan tasarım kuralı nettir: kısmi güncelleme gövdesi artırım değil atama üzerine
kurulursa yöntem etkisiz olur. Artırım gerçekten gerekiyorsa — sayaç, bakiye, stok —
etkisizlik gövdeyle sağlanamaz; dokuzuncu dersteki anahtar düzeneği gerekir.

**DELETE x2** iki isteğe de 204 döndürdü ve ödünç sayısı beşten dörde indi, ikinci silmede
düşmedi. Etkisizliğin tanımı budur: son durum aynıdır. İkinci isteğe 404 döndürmek de
savunulabilir bir seçimdir ve etkisizliği bozmaz, çünkü etkisizlik yanıt kodunun değil
sunucu durumunun özelliğidir. Hangisinin seçileceği bir sonraki dersin konusudur.

#### PUT ile POST Arasındaki Seçim

İkisi de yazma yapar; ayrımı yapan şey kimliği kimin belirlediğidir.

**POST**, kimliği sunucunun belirlediği durumda kullanılır. İstemci `/oduncler`
koleksiyonuna bir gövde yollar, sunucu yeni kaydın kimliğini üretir ve yanıtta bildirir.
İstemci isteği göndermeden önce hedef adresi bilemez.

**PUT**, kimliği istemcinin bildiği durumda kullanılır. `/kitaplar/978-0131103627` adresine
yapılan PUT, o adreste bir kaynak yoksa oluşturur, varsa gönderilen gösterimle değiştirir.
Yukarıdaki sunucu bu ayrımı durum koduna da yansıtır: kayıt yoksa 201, varsa 200.

PUT'un ikinci özelliği çoğu zaman gözden kaçar: **gönderilmeyen alanlar boşalır.** PUT bir
tam değişimdir, kısmi güncelleme değildir. Gövdede `yazar` alanı yoksa kaydın yazarı
silinmiş sayılır. Bu davranış istenmiyorsa doğru yöntem PATCH'tir. Bir servisin PUT'u kısmi
güncelleme gibi işletmesi, istemcinin etkisizlik varsayımını bozmaz ama sözleşmeyi
belirsizleştirir: aynı gövde iki serviste iki farklı sonuç verir.

#### Yöntem Uyuşmazlığı

Bir adres var olduğu hâlde istenen yöntemi desteklemiyorsa — örneğin `/uyeler/U-1001/durum`
kaynağına DELETE gelirse — yanıt "bulunamadı" olmamalıdır. Kaynak duruyor, yöntem
uygulanamıyor. HTTP bunun için ayrı bir durum kodu tanımlar ve yanıtın hangi yöntemlerin
kabul edildiğini bildirmesini bekler. Yukarıdaki sunucu bu ayrımı yapmıyor; eşleşmeyen her
şeye 404 dönüyor. Bir sonraki ders bu eksiği ölçüp kapatır.

#### Özet

- Yöntem, sunucunun dışarıya verdiği bir taahhüttür: güvenli yöntem gözlenebilir değişiklik
  bırakmaz, etkisiz yöntem tekrarlandığında son durumu değiştirmez.
- Okuma görünümlü bir adresin veri değiştirmesi, bağlantı izleyen her programı silici hâline
  getirir; ölçümde üç GET isteği iki açık ödünç kaydını kapattı.
- POST etkisiz değildir ve olmaya da çalışmaz; kimliği sunucu ürettiği için her istek yeni
  kayıt doğurur.
- PUT tam değişimdir ve etkisizdir; gönderilmeyen alanlar boşalır, bu yüzden kısmi
  güncelleme için uygun değildir.
- PATCH'in etkisizliği gövdeye bağlıdır: atama biçimindeki gövde etkisiz, artırım
  biçimindeki gövde değildir — ölçümde kopya sayısı 4'ten 8'e çıktı.
- DELETE etkisizdir; ikinci silme isteğinin farklı durum kodu döndürmesi bu özelliği
  bozmaz, çünkü etkisizlik sunucu durumunun özelliğidir.

#### Sonraki Adım

Bu derste yanıtların durum kodları büyük ölçüde varsayıldı: oluşturmada 201, silmede 204,
eşleşmeyen her şeyde 404. Oysa bunların her biri bir seçimdir ve seçim yanlış yapıldığında
istemci hatayı fark etmez. Bir kaynağın var olmaması ile ona erişim izni olmaması aynı kod
mu döner? İş kuralının reddettiği bir istek ile gövdesi bozuk bir istek nasıl ayrılır?
Sonraki ders aynı senaryo kümesini iki farklı sunucuya sorar, ürettikleri kodları yan yana
koyar ve yanlış eşlemenin istemcide ne ürettiğini gösterir.

### Durum Kodu Seçimi

Source: https://academia.sh/tr/kurslar/api-tasarimi/durum-kodu-secimi

Aynı senaryo kümesinin iki farklı kod eşlemesiyle yanıtlanması ve farkın istemcide ölçülmesi: yakın kodların ayrımı, 2xx ailesinin anlamları, yanlış eşlemenin ürettiği sessiz başarı ve boşa giden yeniden denemeler.

Bir önceki ders yöntemin taşıdığı sözleri ölçtü ve bir eksikle bitti: eşleşmeyen her şeye
404 dönen bir sunucu, kaynağın olmamasıyla yöntemin uygulanamamasını aynı sayıya
indirgiyordu. Aynı ders boyunca 201, 204 ve 409 kodları da gerekçe verilmeden kullanıldı.

Bu ders o gerekçeyi kurar. Durum kodu, yanıtın istemci tarafından **yorumlanmadan
okunabilen** tek bölümüdür. Gövdedeki `hata` alanı insan tarafından okunmak üzere yazılır
ve her serviste farklıdır; durum kodu ise standarttır ve istek katmanı, önbellek, yeniden
deneme düzeneği gibi bileşenler kararlarını ona bakarak verir. Kodun yanlış seçilmesi, bu
bileşenlerin yanlış karar vermesi demektir — ve bu, ölçülebilir bir farktır.

#### Aynı Senaryolar, İki Eşleme

Aşağıdaki sunucu tek bir dosyadır ve iki kipte çalışır. `dogru` kipinde her sonuç kendi
koduyla bildirilir. `gevsek` kipinde ise yaygın bir kolaycılık uygulanır: bulunamayan şeyler
için 200 ve gövdede bir `hata` alanı, geri kalan her başarısızlık için 500.

```js
// kod-sunucusu.mjs — ayni senaryolari iki farkli kod esleme kipinde yanitlar
// Kullanim: node kod-sunucusu.mjs <baglanti-noktasi> <dogru|gevsek>
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const [NOKTA, KIP] = [Number(process.argv[2]), process.argv[3]];
const UYE_SINIRI = 2;
const db = new DatabaseSync("kutuphane.db");

const govdeOku = (istek) => new Promise((coz) => {
  let veri = ""; istek.on("data", (p) => (veri += p));
  istek.on("end", () => coz(veri));
});
const gonder = (yanit, kod, nesne, ekBaslik = {}) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8", ...ekBaslik });
  yanit.end(nesne === null ? "" : JSON.stringify(nesne));
};
// Gevsek kip: her sonucu 200 ya da 500 ile bildirir, ayrimi govdeye birakir.
const bildir = (yanit, kod, nesne, ekBaslik = {}) => {
  if (KIP === "dogru") return gonder(yanit, kod, nesne, ekBaslik);
  if (kod >= 400 && kod < 500) return gonder(yanit, kod === 404 ? 200 : 500, nesne ?? {});
  return gonder(yanit, 200, nesne ?? { sonuc: "tamam" });
};

const sunucu = createServer(async (istek, yanit) => {
  const yol = new URL(istek.url, "http://127.0.0.1").pathname;
  const kitap = /^\/kitaplar\/([^/]+)$/.exec(yol);
  const odunc = /^\/oduncler\/(\d+)$/.exec(yol);

  if (istek.method === "GET" && kitap) {
    const k = db.prepare("SELECT * FROM kitap WHERE isbn = ?").get(kitap[1]);
    return k ? bildir(yanit, 200, k) : bildir(yanit, 404, { hata: "kitap_yok" });
  }

  if (istek.method === "POST" && yol === "/oduncler") {
    const tur = istek.headers["content-type"] ?? "";
    if (!tur.startsWith("application/json"))
      return bildir(yanit, 415, { hata: "desteklenmeyen_tur", beklenen: "application/json" });
    const ham = await govdeOku(istek);
    let g;
    try { g = JSON.parse(ham); }
    catch { return bildir(yanit, 400, { hata: "govde_cozumlenemedi" }); }

    if (!db.prepare("SELECT 1 FROM kitap WHERE isbn = ?").get(g.isbn))
      return bildir(yanit, 422, { hata: "dogrulama", alan: "isbn", neden: "katalogda_yok" });

    const acik = db.prepare("SELECT COUNT(*) AS n FROM odunc WHERE uye = ? AND iade IS NULL").get(g.uye).n;
    if (acik >= UYE_SINIRI)
      return bildir(yanit, 409, { hata: "uye_siniri", sinir: UYE_SINIRI, acik });

    const s = db.prepare("INSERT INTO odunc (uye, isbn, verilis, iade) VALUES (?,?,?,NULL)")
                .run(g.uye, g.isbn, g.verilis);
    const id = Number(s.lastInsertRowid);
    return bildir(yanit, 201, { id }, { location: `/oduncler/${id}` });
  }

  if (istek.method === "DELETE" && odunc) {
    db.prepare("DELETE FROM odunc WHERE id = ?").run(Number(odunc[1]));
    return bildir(yanit, 204, null);
  }

  // Kaynak var ama yontem uygulanamiyor: bulunamadi degil.
  if (/^\/uyeler\/[^/]+\/durum$/.test(yol))
    return bildir(yanit, 405, { hata: "yontem_uygulanamaz" }, { allow: "GET" });

  bildir(yanit, 404, { hata: "yol_yok" });
});

sunucu.listen(NOKTA, "127.0.0.1", () => console.log(`${KIP} kip 127.0.0.1:${NOKTA}`));
```

Karşı tarafta, Uygulama Mimarisi kursundaki istek katmanının sadeleştirilmiş bir sürümü
durur. Tek kuralı vardır: 5xx sınıfı yeniden denenebilir sayılır, 4xx sınıfı sayılmaz.

```js
// istemci.mjs — durum koduna gore karar veren istemci; yeniden denemeleri sayar
// Kullanim: node istemci.mjs <taban-adres>
const TABAN = process.argv[2];
const JSON_BASLIK = { "content-type": "application/json" };

const SENARYOLAR = [
  ["oku",     "GET",    "/kitaplar/978-0131103627", null, null],
  ["yok",     "GET",    "/kitaplar/000-0000000000", null, null],
  ["olustur", "POST",   "/oduncler", JSON_BASLIK, '{"uye":"U-1002","isbn":"978-0131103627","verilis":"2026-03-10"}'],
  ["sinir",   "POST",   "/oduncler", JSON_BASLIK, '{"uye":"U-1002","isbn":"978-0131103627","verilis":"2026-03-11"}'],
  ["isbn",    "POST",   "/oduncler", JSON_BASLIK, '{"uye":"U-1001","isbn":"000-0000000000","verilis":"2026-03-10"}'],
  ["bozuk",   "POST",   "/oduncler", JSON_BASLIK, "{bozuk"],
  ["tur",     "POST",   "/oduncler", { "content-type": "text/plain" }, "merhaba"],
  ["sil",     "DELETE", "/oduncler/2", null, null],
  ["yontem",  "DELETE", "/uyeler/U-1001/durum", null, null],
];

let toplamIstek = 0, sessizBasari = 0;
for (const [ad, yontem, yol, baslik, govde] of SENARYOLAR) {
  let deneme = 0, yanit;
  do {
    deneme++; toplamIstek++;
    yanit = await fetch(TABAN + yol, { method: yontem, headers: baslik ?? {}, body: govde });
  } while (yanit.status >= 500 && deneme < 4);

  const metin = await yanit.text();
  const basarili = yanit.status < 400;
  const govdedeHata = metin.includes('"hata"');
  if (basarili && govdedeHata) sessizBasari++;
  const konum = yanit.headers.get("location");
  console.log(
    `${ad.padEnd(9)} ${String(yanit.status).padEnd(4)} ` +
    `${(basarili ? "basari" : "hata").padEnd(7)} deneme: ${deneme}` +
    (konum ? `  location: ${konum}` : ""),
  );
}
console.log(`toplam istek: ${toplamIstek}, basari sayilan hata: ${sessizBasari}`);
```

```bash
# Ayni istemci iki kipe karsi calistirilir; istek sayilari karsilastirilir.
kur() { rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql; }

kur; node kod-sunucusu.mjs 8477 dogru & a=$!; sleep 0.4
echo "--- dogru kip ---"; node istemci.mjs http://127.0.0.1:8477
kill $a

kur; node kod-sunucusu.mjs 8478 gevsek & b=$!; sleep 0.4
echo "--- gevsek kip ---"; node istemci.mjs http://127.0.0.1:8478
kill $b
```

```
dogru kip 127.0.0.1:8477
--- dogru kip ---
oku       200  basari  deneme: 1
yok       404  hata    deneme: 1
olustur   201  basari  deneme: 1  location: /oduncler/4
sinir     409  hata    deneme: 1
isbn      422  hata    deneme: 1
bozuk     400  hata    deneme: 1
tur       415  hata    deneme: 1
sil       204  basari  deneme: 1
yontem    405  hata    deneme: 1
toplam istek: 9, basari sayilan hata: 0
gevsek kip 127.0.0.1:8478
--- gevsek kip ---
oku       200  basari  deneme: 1
yok       200  basari  deneme: 1
olustur   200  basari  deneme: 1
sinir     500  hata    deneme: 4
isbn      500  hata    deneme: 4
bozuk     500  hata    deneme: 4
tur       500  hata    deneme: 4
sil       200  basari  deneme: 1
yontem    500  hata    deneme: 4
toplam istek: 24, basari sayilan hata: 1
```

#### Ölçümün Okunması

Dokuz senaryo, doğru eşlemede dokuz istekle bitti; gevşek eşlemede yirmi dört istek gerekti.
Fark, dört senaryonun yeniden denenmiş olmasından gelir. Bu dört senaryonun ortak yanı
şudur: **hiçbiri tekrarlanınca düzelmez.** Üye sınırı doludur, ISBN katalogda yoktur, gövde
bozuktur, içerik türü yanlıştır. Sunucu bunları 500 ile bildirdiğinde istemciye "bende
geçici bir sorun var, tekrar dene" demiş olur. İstemci sözü tutar ve üç kez daha dener.
İstek üçe katlanır, sonuç değişmez.

İkinci sayı daha sinsidir. Gevşek eşlemede bir başarısızlık **başarı sayıldı**: olmayan bir
kitabı isteyen istemci 200 aldı ve gövdedeki `hata` alanını okumadığı için elinde bir kitap
olduğunu sandı. Ekranda boş bir kitap kartı, kütüğe yazılmayan bir hata ve bulunamayan bir
kusur bu satırdan doğar.

Üçüncü fark satırlarda değil, eksik olan şeydedir. Doğru eşlemede `olustur` senaryosu 201
ile birlikte `Location` başlığını taşıyor; gevşek eşlemede o başlık yok. Yeni kaynağın
adresini bildiren tek yer odur. Onsuz istemci, oluşturduğu ödünç kaydına ulaşmak için ya
gövdedeki kimliği okuyup adresi kendisi kurmak ya da koleksiyonu yeniden çekmek zorunda
kalır.

#### Yakın Kodların Ayrımı

Kod seçimini yapan soru şudur: **isteğin hangi bölümü kabul edilmedi?**

- **400** — Gövde okunamadı. İstek, biçim düzeyinde bozuktur; JSON çözümlenemedi, zorunlu
  bir başlık eksik. Sunucu isteği anlamlandıramadığı için içeriğe dair bir şey söyleyemez.
- **415** — Gövde okunabilir olabilir ama içerik türü desteklenmiyor. Sorun içerikte değil,
  içeriğin hangi biçimde gönderildiğindedir.
- **422** — Gövde çözümlendi, alanlar okundu, ama değerler kabul edilmedi. Katalogda
  olmayan bir ISBN böyledir: yazım doğru, gönderilen değer geçersiz. Bu ayrımın pratik
  değeri, istemcinin hatayı forma geri bağlayabilmesidir — hangi alanın reddedildiği
  söylenebilir. Alan düzeyinde hata bildirimi ayrı bir dersin konusudur.
- **409** — Gövde de değerler de geçerli, ama sistemin o anki durumu isteğin yerine
  getirilmesine izin vermiyor. Üye sınırının dolu olması budur. 422 ile 409 arasındaki
  fark zamandır: 422 aynı gövde için hep geçersizdir, 409 durum değişince geçerli olabilir.
- **404** — Adreslenen kaynak yok. Sorun gövdede değil yoldadır.
- **405** — Kaynak var, yöntem uygulanamıyor. Yanıt, hangi yöntemlerin kabul edildiğini
  `Allow` başlığıyla bildirir; yukarıdaki sunucu `Allow: GET` gönderir. 404 ile karıştırmak
  istemciyi yanlış yere bakmaya iter: adresi düzeltmeye çalışır, oysa yöntemi düzeltmesi
  gerekir.

Kimlik ve yetki kodları — 401 ile 403 — bu listeye bilinçli olarak alınmadı. İkisi
arasındaki ayrım kimliğin bilinip bilinmediğine dayanır ve Kimlik Doğrulama ve Yetkilendirme
kursunda ele alınır. Buradaki tek kural, ikisinin de 404'e indirgenmemesidir.

#### 2xx Ailesi

Başarı da tek bir kod değildir.

**200** genel başarıdır ve gövde taşır. **201** yeni bir kaynak oluşturulduğunu söyler;
yeni kaynağın adresi istek adresinden okunamıyorsa — koleksiyona yapılan POST'ta olduğu gibi
— yanında `Location` başlığı bulunmalıdır, çünkü istemci o adresi kendisi kuramaz. **204**
başarıyı gövdesiz bildirir; silme işlemlerinde ve döndürülecek yeni bir gösterim olmadığında
kullanılır. **202** isteğin kabul edildiğini ama henüz tamamlanmadığını söyler; işin
sonucunu izlemek için ayrı bir kaynak gerektirir ve eşzamansız işleme konusuna girer.

Seçimin ölçütü yine istemcinin kararıdır. 204 alan bir istemci gövdeyi okumaya çalışmaz;
200 alan istemci okur. Silme yanıtına boş bir gövdeyle 200 dönmek, istemciyi olmayan bir
gövdeyi çözümlemeye zorlar ve gövde hatası üretir.

#### Bilinmeyeni 500'e Toplamak

Gevşek kipin asıl kusuru 500 kodunu kullanması değil, **her şey için** kullanmasıdır. 500,
sunucunun kendi kusurunu bildirdiği koddur: beklenmeyen bir özel durum, erişilemeyen bir
veritabanı, çözülmemiş bir varsayım. Bu kodu istemcinin hatası için kullanmak iki şeyi aynı
anda bozar. İstemci, düzeltebileceği bir sorunu düzeltmez — çünkü kendisine sorunun onda
olmadığı söylenmiştir. Sunucu tarafında ise gerçek kusurların sayısı, istemci hatalarıyla
karışır ve ölçülemez hâle gelir.

Doğru davranış, bilinen her başarısızlığı kendi koduna eşlemek ve 500'ü yalnızca
sınıflandırılamayan durum için saklamaktır. Sınıflandırılamayan durumun gövdesinde de
ayrıntı verilmez; iç hata iletisini dışarıya yazmak, sunucunun iç yapısını sözleşmeye
sızdırır.

#### Özet

- Durum kodu, yanıtın istemci tarafından yorumlanmadan okunan bölümüdür; önbellek, istek
  katmanı ve yeniden deneme düzeneği kararlarını ona bakarak verir.
- Aynı dokuz senaryo doğru eşlemede dokuz istekle bitti, gevşek eşlemede yirmi dört istek
  üretti; fark, düzelmeyecek hataların 5xx ile bildirilip yeniden denenmesinden doğar.
- Gevşek eşlemede bir başarısızlık başarı sayıldı: 200 ile dönen "bulunamadı", istemcinin
  hiç fark etmediği bir hataya dönüştü.
- Hata kodu seçimi isteğin hangi bölümünün kabul edilmediğine bakar: 400 biçim, 415 içerik
  türü, 422 alan değerleri, 409 sistemin o anki durumu, 404 adres, 405 yöntem.
- 201 yanıtı `Location` başlığı olmadan eksiktir; 204 gövdesiz başarıyı bildirir ve
  istemciyi gövde çözümlemeye zorlamaz.
- 500 yalnızca sunucunun sınıflandıramadığı kusur için ayrılır; istemci hataları için
  kullanıldığında hem istemci yanlış yönlendirilir hem de gerçek kusurlar ölçülemez olur.

#### Sonraki Adım

Kodlar yerine oturdu, ama yanıtların içine ne konacağı hâlâ açık. Bu derste gövdeler
gelişigüzel yazıldı: bir yerde yalnız bir kimlik alanı, başka bir yerde hata adı ve sınır
değeri. Alan adları hangi yazım biçiminde olacak, tarihler nasıl yazılacak, boş
değerle eksik alan aynı şey mi sayılacak, koleksiyon yanıtı doğrudan bir dizi mi olacak?
Sonraki ders gövde biçimini ele alır ve yanlış seçimlerin nerede sessizce veri
bozduğunu — büyük sayıların çözümlenmesinde ve tarih yazımında — çalıştırarak gösterir.

### İstek ve Yanıt Gövdeleri

Source: https://academia.sh/tr/kurslar/api-tasarimi/istek-ve-yanit-govdeleri

Gövde biçiminin sözleşme değeri: alan adlandırmada tek yazım, büyük tamsayıların ve ondalık tutarların sessiz kaybı, tarihlerin bölge bilgisiyle yazılması, eksik alan ile boş değerin ayrılması ve koleksiyon yanıtının zarf kararı.

Bir önceki ders yanıtın makine tarafından okunan bölümünü — durum kodunu — yerine oturttu.
Geriye gövde kaldı. Ölçüm sunucularında gövdeler gelişigüzel yazıldı: bir yerde yalnız bir
kimlik alanı, başka yerde hata adı ve sınır değeri, bir başka yerde tablonun sütun adları
olduğu gibi.

Gövde biçimi, durum kodu kadar sözleşmedir. Alan adı değişirse istemci kırılır; bir sayının
nasıl yazıldığı yanlış seçilirse istemci kırılmaz — daha kötüsü olur, sessizce yanlış değer
okur. Bu ders gövde kararlarını sıralar ve ikisi ölçülerek gösterilir: sayıların ve
tarihlerin çözümlenmesi, bir de eksik alanla boş değerin ayrılması.

#### Gövde Bir Sözleşmedir

İlk karar alan adlandırmasıdır ve içeriğinden çok tutarlılığı önemlidir. Bir yanıtta
`verilis`, başka bir yanıtta `verilisTarihi`, üçüncüsünde `verilis_tarihi` geçiyorsa
istemci her uç nokta için ayrı bir çözümleme yazar. Kurs boyunca tek bir yazım seçilir ve
değişmez.

İkinci karar, gövdenin veritabanı şemasının kopyası olmamasıdır. Bir önceki derste ödünç
kaydı doğrudan `SELECT *` sonucuyla döndürülmüştü; bu, sütun adlarını sözleşmeye
sızdırır. Sütun adı değiştiğinde ya da bir sütun eklendiğinde istemci etkilenir. Doğru
davranış, gösterimi açıkça kurmaktır: hangi alanların dışarı çıkacağı yazılır, geri kalanı
içeride kalır.

Üçüncü karar numaralandırma değerlerine ilişkindir. Bir ödünç kaydının durumu `acik`,
`iade_edildi`, `gecikti` gibi sabit kodlarla bildirilir; "Açık", "İade Edildi" gibi
görüntülenecek metinlerle değil. Metin dile ve yazıma bağlıdır, kod ise sözleşmenin
parçasıdır ve karşılaştırılabilir. Ekranda ne yazacağı istemcinin kararıdır.

#### Sayılar: Sessiz Kayıp

Kütüphane katalogunda her kopyanın bir demirbaş numarası vardır ve bu numara başka bir
sistemden gelir. Gecikme ücreti de ondalıklı bir tutardır. İkisi de JSON'da sayı olarak
yazıldığında ne olduğuna bakalım.

```bash
# JSON govdesindeki iki sessiz kayip: buyuk tamsayi ve bicimsiz tarih.
node -e '
const govde = `{"demirbas": 9007199254740993, "ucret": 0.1, "ek": 0.2}`;
const c = JSON.parse(govde);
console.log("gonderilen demirbas :", govde.match(/\d{16}/)[0]);
console.log("cozulen  demirbas   :", String(c.demirbas));
console.log("esit mi              :", String(c.demirbas) === govde.match(/\d{16}/)[0]);
console.log("ucret + ek           :", c.ucret + c.ek);
'
echo "---"
TZ=Europe/Istanbul node -e '
for (const d of ["12/03/2026", "2026-03-12", "2026-03-12T00:00:00", "2026-03-12T00:00:00+03:00"])
  console.log(d.padEnd(28), new Date(d).toISOString());
'
```

```
gonderilen demirbas : 9007199254740993
cozulen  demirbas   : 9007199254740992
esit mi              : false
ucret + ek           : 0.30000000000000004
---
12/03/2026                   2026-12-02T21:00:00.000Z
2026-03-12                   2026-03-12T00:00:00.000Z
2026-03-12T00:00:00          2026-03-11T21:00:00.000Z
2026-03-12T00:00:00+03:00    2026-03-11T21:00:00.000Z
```

İlk üç satır bir kimliğin çözümleme sırasında bozulduğunu gösteriyor. JSON'un sayı tipinin
kesinliği çözümleyicinin kayan noktalı sayı temsiline bağlıdır; Bilgisayarlar Nasıl Çalışır
kursunda tanımlanan çift duyarlıklı temsilin tamsayı sınırı aşıldığında en yakın
temsil edilebilir değere yuvarlanır. Sunucuya `...993` gönderilir, istemcide `...992`
okunur. Hiçbir hata üretilmez; kayıp, o kimlikle yapılan bir sorgu boş dönene kadar
görünmez.

Dördüncü satır aynı kökten gelen ikinci sorundur: `0.1 + 0.2` işleminin sonucu tam
`0.3` değildir. Para tutarları ondalıklı sayı olarak taşındığında toplama işlemleri
biriken hatalar üretir.

Her iki sorunun da çözümü aynıdır: **hesaplanmayacak sayılar dizgi olarak taşınır.**
Kimlikler dizgidir — zaten üzerlerinde aritmetik yapılmaz. Para tutarları ya en küçük
birim cinsinden tamsayı olarak (kuruş) ya da dizgi olarak taşınır ve karşılaştırma o
biçimde yapılır. Bir kimliğin sayı görünümlü olması onu sayı yapmaz.

#### Tarihler: Biçimsiz Dizgi Yoktur

Çıktının ikinci bölümü tarih alanlarını gösteriyor ve dört satırın dördü de farklı bir
tuzağa denk gelir. Bu blok `TZ=Europe/Istanbul` ile çalıştırılmıştır; yerel saat dilimi
değiştiğinde üçüncü satırın sonucu da değişir — asıl sorun da budur.

`12/03/2026` yazımı 12 Mart olarak değil, 2 Aralık olarak çözümlendi. Gün ve ayın hangi
sırada yazıldığı yazımın kendisinden anlaşılmadığı için çözümleyici kendi varsayımını
uyguladı. Sözleşmede böyle bir alan bulunuyorsa istemcilerin yarısı sekiz ay ileriyi okur.

`2026-03-12` yazımı bölgesiz bir gündür ve gece yarısı UTC olarak alındı.
`2026-03-12T00:00:00` yazımı ise bölge bilgisi taşımadığı için **yerel** saat sayıldı ve
UTC'ye çevrildiğinde bir gün geriye kaydı. Aynı dizgi, iki farklı makinede iki farklı ana
karşılık gelir. Yalnızca son satır — bölge farkını açıkça yazan biçim — her yerde aynı anı
gösterir.

Kural şudur: **bir zaman anı bildiriliyorsa bölge farkı yazılır.** Ödünç verme anı gibi
alanlar tam biçimde taşınır. Yalnızca takvim günü bildiriliyorsa — son iade günü gibi —
alanın adı bunu söylemeli ve gösterimde saat hiç bulunmamalıdır. İki türü aynı alanda
karıştırmak, yaz saati geçişlerinde bir günlük kaymalar üretir.

#### Yok ile Boş Aynı Şey Değil

Kısmi güncellemede istemci üç şey söyleyebilir: bu alanı şu değere getir, bu alanı boşalt,
bu alana dokunma. JSON gövdesinde ilk ikisi değerle, üçüncüsü **alanın hiç bulunmamasıyla**
bildirilir. Bu ayrımı gözetmeyen bir yazım, "boşalt" isteğini hiçbir zaman
gerçekleştiremez.

```js
// alan-sunucusu.mjs — kismi guncellemede "yok" ile "bos" ayrimi
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const govdeOku = (istek) => new Promise((coz) => {
  let veri = ""; istek.on("data", (p) => (veri += p));
  istek.on("end", () => coz(veri ? JSON.parse(veri) : {}));
});
const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

const sunucu = createServer(async (istek, yanit) => {
  const yol = new URL(istek.url, "http://127.0.0.1").pathname;
  const gevsek = /^\/gevsek\/oduncler\/(\d+)$/.exec(yol);
  const kesin = /^\/kesin\/oduncler\/(\d+)$/.exec(yol);
  if (istek.method !== "PATCH" || !(gevsek || kesin))
    return yanitla(yanit, 404, { hata: "yol_yok" });

  const id = Number((gevsek ?? kesin)[1]);
  const g = await govdeOku(istek);
  const mevcut = db.prepare("SELECT * FROM odunc WHERE id = ?").get(id);
  if (!mevcut) return yanitla(yanit, 404, { hata: "odunc_yok" });

  if (gevsek) {
    // Bos deger ile eksik alani ayirmayan yazim: null hicbir zaman yazilamaz.
    const yeni = g.iade ?? mevcut.iade;
    db.prepare("UPDATE odunc SET iade = ? WHERE id = ?").run(yeni, id);
  } else {
    // Alanin govdede bulunup bulunmadigina bakilir; degeri ayrica degerlendirilir.
    if ("iade" in g) db.prepare("UPDATE odunc SET iade = ? WHERE id = ?").run(g.iade, id);
  }
  yanitla(yanit, 200, db.prepare("SELECT id, iade FROM odunc WHERE id = ?").get(id));
});

sunucu.listen(8479, "127.0.0.1", () => console.log("alan sunucusu 127.0.0.1:8479"));
```

```bash
# Ayni uc govde iki yazima gonderilir; iade alaninin son degeri karsilastirilir.
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql
node alan-sunucusu.mjs & sunucu=$!
sleep 0.4

yolla() { curl -sS -X PATCH -H 'content-type: application/json' -d "$2" "http://127.0.0.1:8479$1"; echo; }
for kip in gevsek kesin; do
  echo "--- $kip ---"
  yolla "/$kip/oduncler/2" '{"iade":"2026-03-20"}'
  yolla "/$kip/oduncler/2" '{"uye":"U-1001"}'
  yolla "/$kip/oduncler/2" '{"iade":null}'
done

kill $sunucu
```

```
alan sunucusu 127.0.0.1:8479
--- gevsek ---
{"id":2,"iade":"2026-03-20"}
{"id":2,"iade":"2026-03-20"}
{"id":2,"iade":"2026-03-20"}
--- kesin ---
{"id":2,"iade":"2026-03-20"}
{"id":2,"iade":"2026-03-20"}
{"id":2,"iade":null}
```

Her iki kipin ilk iki satırı aynı: değer atanır, alan gövdede yoksa korunur. Üçüncü satır
ayrışır. Gevşek yazımda `iade` alanı boşaltılamadı; çünkü `??` işleci `null` değeri
"verilmemiş" sayar ve eski değeri geri koyar. Yanlışlıkla kapatılmış bir ödünç kaydını
düzeltmenin yolu yoktur; istemci isteği gönderir, 200 alır, hiçbir şey değişmez.

Kesin yazımda ayrım gövdenin yapısından okunur: alan gövdede varsa değeri — `null` dahil —
uygulanır, yoksa dokunulmaz. Bu davranış sözleşmede yazılı olmalıdır, çünkü istemcinin
"alanı göndermemek" ile "alanı boş göndermek" arasındaki farkı bilerek kullanması gerekir.

Aynı ayrım yanıt tarafında da geçerlidir. Bir alanın değeri yoksa `null` olarak
gönderilmesi ile hiç gönderilmemesi farklı şeyler söyler: birincisi "bu alan var ama boş",
ikincisi "bu alan bu gösterimde yok". Kurs boyunca tek bir kural seçilir; yanıtta bazen
`null`, bazen alanın hiç bulunmaması istemciyi her iki olasılığı da yazmaya zorlar.

#### Koleksiyon Yanıtı: Çıplak Dizi mi, Zarf mı

Koleksiyon yanıtının en kısa biçimi çıplak bir dizidir. Kısa olması tek üstünlüğüdür.
Diziye ek bilgi konamaz: kaç kayıt olduğu, bir sonraki sayfanın nerede başladığı, sorgunun
hangi ölçütle çalıştığı. Bu bilgiler başlıklara taşınabilir, ama başlıklar gövdeyle
birlikte önbelleklenmeyebilir ve okunmaları istemci tarafında ayrı bir iş gerektirir.

**Zarf** yazımı gövdeyi bir nesneye sarar: kayıtlar bir alanda, sayfalama bilgisi başka bir
alanda durur. Maliyeti bir düzey fazladan iç içelik, kazancı yanıtın kendini
anlatabilmesidir. Sonraki iki ders — sayfalama ve bağlantı odaklı yanıtlar — bu alanı
kullanacağı için kurs boyunca zarf yazımı seçilir.

Tekil kaynak yanıtları ise sarılmaz. `/oduncler/2` adresinden dönen gövde doğrudan ödünç
kaydının gösterimidir; onu bir alana sarmak, hiçbir bilgi eklemeden her istemciye bir
düzey daha inme yükü getirir.

#### Özet

- Alan adlandırmada tek yazım biçimi seçilir ve kurs boyunca değişmez; gövde veritabanı
  şemasının kopyası değil, açıkça kurulmuş bir gösterimdir.
- Üzerinde aritmetik yapılmayacak sayılar — kimlikler — dizgi olarak taşınır; ölçümde
  `9007199254740993` değeri çözümlendiğinde `9007199254740992` oldu ve hata üretilmedi.
- Para tutarları ondalıklı sayı olarak taşınmaz; en küçük birim cinsinden tamsayı ya da
  dizgi kullanılır.
- Zaman anı bildiren alanlar bölge farkını açıkça yazar; bölgesiz yazımlar makineye göre
  farklı ana karşılık gelir ve gün/ay sırası belirsiz yazımlar aylarca kayabilir.
- Kısmi güncellemede alanın gövdede bulunup bulunmadığı ile değerinin boş olması ayrı
  bilgilerdir; ayrımı gözetmeyen yazımda bir alanı boşaltmak olanaksızdır.
- Koleksiyon yanıtları zarfla sarılır, çünkü sayfalama ve gezinme bilgisi gövdenin içinde
  taşınacaktır; tekil kaynak yanıtları sarılmaz.

#### Sonraki Adım

Zarfın boş duran alanı bir sonraki dersin konusudur. Ödünç koleksiyonu üç satırla
sınırlıyken tüm kayıtları tek yanıtta göndermek sorun değildi; on binlerce kayıtta bu
yanıt hem sunucuyu hem istemciyi taşır. Kayıtları parça parça vermek gerekir ve bunun
birden çok yolu vardır. Sonraki ders atlama tabanlı sayfalamayı kurar, araya kayıt
eklendiğinde okuyucunun bir kaydı iki kez görmesini ya da hiç görmemesini gerçekten
üretir, sonra aynı senaryoda kaymayan bir yöntem gösterir.

### Sayfalama

Source: https://academia.sh/tr/kurslar/api-tasarimi/sayfalama

Koleksiyonun parça parça sunulması: atlama tabanlı sayfalamada araya kayıt girdiğinde oluşan tekrar ve atlama, imleç tabanlı sayfalamanın aynı senaryoda kaymaması, derin sayfalamanın sorgu planı ve süre üzerindeki maliyeti.

Bir önceki ders koleksiyon yanıtlarının zarfla sarılmasını kararlaştırdı ve zarfın
`sayfalama` alanını boş bıraktı. Bu ders o alanı doldurur.

Sorun sayılarla başlar. Ödünç koleksiyonu yirmi kayıtken tümünü tek yanıtta göndermek
sorun değildir; iki yüz bin kayıtta aynı yanıt sunucunun belleğini, ağı ve istemcinin
çözümleyicisini birlikte zorlar. Koleksiyon parça parça verilmelidir. Parçanın nerede
başlayacağını söylemenin iki yolu vardır ve aralarındaki fark, koleksiyon istekler
arasında değiştiğinde ortaya çıkar.

#### İki Yöntem

**Atlama tabanlı** sayfalamada istemci kaçıncı sayfayı istediğini söyler; sunucu sıralı
sonucun başından o kadar satır atlar ve sonrakini döndürür. SQL Temelleri kursundaki
`LIMIT` ve `OFFSET` yan tümceleri doğrudan bunu karşılar. İstemci `?sayfa=3` yazar, sunucu
`OFFSET 10` uygular.

**İmleç tabanlı** sayfalamada istemci en son gördüğü kaydın yerini bildirir; sunucu ondan
sonrasını döndürür. Sayfa numarası yoktur; her yanıt bir sonraki isteğin başlangıç noktasını
taşır. Ölçüt sıralama anahtarıdır, dolayısıyla yöntem yalnızca **belirlenimci sıralama**
varken çalışır: sıralama anahtarı çift değer üretebiliyorsa yanına benzersiz bir alan
eklenir.

Aşağıdaki sunucu ikisini de sunar; aynı tablo, aynı sıralama, iki adres.

```js
// sayfa-sunucusu.mjs — ayni koleksiyonu iki sayfalama yontemiyle sunar
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

const sunucu = createServer((istek, yanit) => {
  const adres = new URL(istek.url, "http://127.0.0.1");
  const s = adres.searchParams;
  const boyut = Math.min(Number(s.get("boyut") ?? 5), 50);

  if (adres.pathname === "/atlama/oduncler") {
    const sayfa = Number(s.get("sayfa") ?? 1);
    const satirlar = db.prepare(
      "SELECT id, verilis FROM odunc ORDER BY id DESC LIMIT ? OFFSET ?"
    ).all(boyut, (sayfa - 1) * boyut);
    return yanitla(yanit, 200, {
      veri: satirlar.map((r) => r.id),
      sayfalama: { sayfa, boyut },
    });
  }

  if (adres.pathname === "/imlec/oduncler") {
    // Imlec yoksa en bastan baslanir; varsa o kimlikten kucukler istenir.
    const imlec = s.get("imlec");
    const satirlar = imlec === null
      ? db.prepare("SELECT id FROM odunc ORDER BY id DESC LIMIT ?").all(boyut)
      : db.prepare("SELECT id FROM odunc WHERE id < ? ORDER BY id DESC LIMIT ?")
          .all(Number(imlec), boyut);
    const son = satirlar.at(-1);
    return yanitla(yanit, 200, {
      veri: satirlar.map((r) => r.id),
      sayfalama: { sonrakiImlec: son ? String(son.id) : null },
    });
  }

  yanitla(yanit, 404, { hata: "yol_yok" });
});

sunucu.listen(8480, "127.0.0.1", () => console.log("sayfa sunucusu 127.0.0.1:8480"));
```

Sayfa boyutu istemciden alınırken üst sınırla kesiliyor. Bu, sözleşmenin ayrılmaz
parçasıdır: sınırsız bir `boyut` parametresi, tek bir istekle tüm koleksiyonu çekmeye açık
kapı bırakır.

#### Kayma Ölçümü

Ölçüm şu senaryoyu kurar: istemci ilk sayfayı alır, bu arada koleksiyon değişir, sonra
ikinci sayfayı ister. Değişimin iki türü ayrı ayrı denenir — araya kayıt eklenmesi ve
kayıt silinmesi. Her turda veri baştan tohumlanır, böylece dört senaryo aynı başlangıçtan
çalışır.

```bash
# Yirmi kayitlik koleksiyon; ilk sayfa alinir, koleksiyon degisir, ikinci sayfa alinir.
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql
tohum() {
  sqlite3 kutuphane.db <<'SQL'
DELETE FROM odunc;
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 20)
INSERT INTO odunc (uye, isbn, verilis, iade)
SELECT 'U-1001', '978-0262033848', date('2026-01-01', '+' || n || ' day'), NULL FROM s;
SQL
}
tohum
node sayfa-sunucusu.mjs & sunucu=$!
sleep 0.4

ekle() { sqlite3 kutuphane.db "INSERT INTO odunc (uye, isbn, verilis, iade)
  VALUES ('U-1002','978-0201896831','2026-02-01',NULL),
         ('U-1002','978-0201896831','2026-02-02',NULL);"; }
sil() { sqlite3 kutuphane.db "DELETE FROM odunc WHERE id IN (20, 19);"; }
al() { curl -sS "http://127.0.0.1:8480$1"; echo; }

for etki in ekle sil; do
  tohum; printf 'atlama / %s  sayfa 1 : ' "$etki"; al "/atlama/oduncler?sayfa=1&boyut=5"
  $etki;  printf 'atlama / %s  sayfa 2 : ' "$etki"; al "/atlama/oduncler?sayfa=2&boyut=5"
  tohum; printf 'imlec  / %s  sayfa 1 : ' "$etki"; al "/imlec/oduncler?boyut=5"
  $etki;  printf 'imlec  / %s  sayfa 2 : ' "$etki"; al "/imlec/oduncler?imlec=16&boyut=5"
done

kill $sunucu
```

```
sayfa sunucusu 127.0.0.1:8480
atlama / ekle  sayfa 1 : {"veri":[20,19,18,17,16],"sayfalama":{"sayfa":1,"boyut":5}}
atlama / ekle  sayfa 2 : {"veri":[17,16,15,14,13],"sayfalama":{"sayfa":2,"boyut":5}}
imlec  / ekle  sayfa 1 : {"veri":[20,19,18,17,16],"sayfalama":{"sonrakiImlec":"16"}}
imlec  / ekle  sayfa 2 : {"veri":[15,14,13,12,11],"sayfalama":{"sonrakiImlec":"11"}}
atlama / sil  sayfa 1 : {"veri":[20,19,18,17,16],"sayfalama":{"sayfa":1,"boyut":5}}
atlama / sil  sayfa 2 : {"veri":[13,12,11,10,9],"sayfalama":{"sayfa":2,"boyut":5}}
imlec  / sil  sayfa 1 : {"veri":[20,19,18,17,16],"sayfalama":{"sonrakiImlec":"16"}}
imlec  / sil  sayfa 2 : {"veri":[15,14,13,12,11],"sayfalama":{"sonrakiImlec":"11"}}
```

Dört senaryonun ikisi kusurlu.

**Atlama, ekleme durumunda tekrar üretti.** İlk sayfa 20'den 16'ya kadar geldi. Araya iki
yeni kayıt girince sıralamanın başı iki basamak kaydı; ikinci sayfa artık 17'den başlıyor.
İstemci 17 ve 16 numaralı kayıtları **iki kez** gördü. Sayfaları biriktirip liste kuran bir
ekranda aynı ödünç kaydı iki satır olarak belirir.

**Atlama, silme durumunda atlama üretti.** İki kayıt silinince sıralama iki basamak
yukarı çekildi ve ikinci sayfa 13'ten başladı. 15 ile 14 numaralı kayıtlar hiçbir sayfada
görünmedi. Bu, tekrardan daha tehlikelidir: eksik olan şey fark edilmez.

**İmleç her iki durumda da aynı sonucu verdi:** 15'ten 11'e. Neden açıktır. İmleç
sayfanın kaçıncı sırada olduğunu değil, **nerede kalındığını** taşır. Koleksiyonun başına
kayıt eklenmesi ya da başından kayıt silinmesi, "16'dan küçük kimlikler" ölçütünü
etkilemez. Yeni eklenen kayıtlar bir sonraki turda başa geldiklerinde görülür; bu, kaymanın
değil tazeliğin sonucudur.

#### Derin Sayfalamanın Maliyeti

Kaymadan bağımsız ikinci bir fark daha vardır ve o da sayfa numarası büyüdükçe belirir.
Atlanan satırlar bedavaya atlanmaz: motor onları bulmak zorundadır.

```bash
# Iki milyon kayitta derin sayfalama: plan ve sure karsilastirmasi.
rm -f derin.db
sqlite3 derin.db <<'SQL'
CREATE TABLE odunc (id INTEGER PRIMARY KEY, uye TEXT NOT NULL, verilis TEXT NOT NULL);
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 2000000)
INSERT INTO odunc (uye, verilis)
SELECT 'U-' || (1000 + n % 500), date('2020-01-01', '+' || (n % 2000) || ' day') FROM s;
CREATE INDEX odunc_verilis ON odunc (verilis DESC, id DESC);
SQL

sqlite3 derin.db <<'SQL'
.echo on
EXPLAIN QUERY PLAN
SELECT id FROM odunc ORDER BY verilis DESC, id DESC LIMIT 20 OFFSET 1999980;
EXPLAIN QUERY PLAN
SELECT id FROM odunc WHERE (verilis, id) < ('2020-01-02', 100) ORDER BY verilis DESC, id DESC LIMIT 20;
SQL

echo "--- sureler ---"
sqlite3 derin.db <<'SQL'
.timer on
SELECT count(*) FROM (SELECT id FROM odunc ORDER BY verilis DESC, id DESC LIMIT 20 OFFSET 1999980);
SELECT count(*) FROM (SELECT id FROM odunc WHERE (verilis, id) < ('2020-01-02', 100) ORDER BY verilis DESC, id DESC LIMIT 20);
SQL
```

```
EXPLAIN QUERY PLAN
SELECT id FROM odunc ORDER BY verilis DESC, id DESC LIMIT 20 OFFSET 1999980;
QUERY PLAN
`--SCAN odunc USING COVERING INDEX odunc_verilis
EXPLAIN QUERY PLAN
SELECT id FROM odunc WHERE (verilis, id) < ('2020-01-02', 100) ORDER BY verilis DESC, id DESC LIMIT 20;
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_verilis (verilis<?)
--- sureler ---
20
Run Time: real 0.014 user 0.008816 sys 0.004918
20
Run Time: real 0.000 user 0.000066 sys 0.000017
```

Süre değerleri makineye ve çalıştırmaya göre değişir; sabit olan iki şey plan
satırlarıdır. İleri SQL kursunda okunan plan sözlüğüne göre birincisi **SCAN**, ikincisi
**SEARCH** diyor. Atlama tabanlı sorgu, dizini baştan taramak ve iki milyona yakın girdiyi
atmak zorunda; imleç tabanlı sorgu doğrudan aranan noktaya iniyor. İkisi de aynı yirmi
satırı döndürür, ikisi de kapsayan dizin kullanır, ama biri sayfa derinliğiyle birlikte
pahalanır, diğeri pahalanmaz.

Ölçümde iki büyüklük derecesi fark çıktı. Ölçek arttıkça fark açılır, çünkü atlama
maliyeti sayfa numarasıyla doğru orantılıdır: son sayfa, tablonun tamamını okumak
demektir.

#### Hangi Yöntem Nerede

İki yöntemin de yeri vardır ve seçim ölçütü kullanım biçimidir.

**Atlama tabanlı** sayfalama, kullanıcının sayfa numarasına atlayabildiği yönetim
ekranlarına uyar: "toplam 412 kayıt, 21 sayfa" bilgisi verilebilir, yedinci sayfaya
doğrudan gidilebilir. Koşulu, veri kümesinin istekler arasında büyük ölçüde durağan
olması ve sayfa sayısının makul kalmasıdır. Kayma riski kabul ediliyorsa açıkça kabul
edilmelidir.

**İmleç tabanlı** sayfalama, akış biçiminde ilerleyen listelere uyar: aşağı kaydırdıkça
yüklenen kayıt listeleri, dışa aktarım işleri, eşleme işleri. Toplam sayı ve sayfa
numarası veremez; buna karşılık kaymaz ve derinlikten etkilenmez.

İmlecin kendisi de bir tasarım kararıdır. Yukarıdaki sunucuda imleç ham bir kimlik olarak
gidiyor; bu, sıralama ölçütünün kimlik olduğunu dışarıya söyler. Sıralama başka bir alana
göreyse imlecin o alanı ve benzersizleştirici alanı birlikte taşıması gerekir. İmleci
kodlanmış tek bir dizgi olarak yollamak, istemcinin onun içeriğine bağımlı hâle gelmesini
önler: imleç istemci için anlamsız bir işarettir, yalnızca geri gönderilir. Sıralama
ölçütü değiştiğinde eski imleçlerin geçersiz sayılması ve reddedilmesi de sözleşmenin
parçasıdır.

#### Özet

- Koleksiyonlar parça parça sunulur; parçanın nereden başlayacağı ya sayfa numarasıyla ya
  da en son görülen kaydın yerini bildiren bir imleçle söylenir.
- Atlama tabanlı sayfalamada araya kayıt girdiğinde kayıtlar tekrar eder, kayıt silindiğinde
  atlanır; ölçümde iki kayıt iki kez göründü ve iki kayıt hiç görünmedi.
- İmleç tabanlı sayfalama aynı iki senaryoda da aynı sonucu verdi, çünkü sıra numarasını
  değil koleksiyondaki yeri taşır.
- İmleç yalnızca belirlenimci sıralamayla çalışır; sıralama anahtarı benzersiz değilse
  yanına benzersiz bir alan eklenir.
- Derin sayfalamada atlama tabanlı sorgu dizini baştan tarar, imleç tabanlı sorgu aranan
  noktaya iner; plan satırları SCAN ile SEARCH olarak ayrışır.
- Sayfa boyutu istemciden alınır ama üst sınırla kesilir; sınırsız boyut tüm koleksiyonu
  tek istekte çekmeye açık kapı bırakır.

#### Sonraki Adım

Sayfalama, koleksiyonun ne kadarının verileceğini çözdü; hangi kayıtların verileceğini
çözmedi. Kütüphane görevlisi "yalnızca geciken ödünçler", "Merkez şubesindeki kitaplar",
"yazar adına göre sıralı" gibi isteklerle gelir. Bunların hepsi sorgu bölümünde taşınacak,
hepsi SQL'e dönüşecek ve dönüşüm sırasında iki tuzak belirecek: istemciden gelen değerin
sorguya nasıl taşındığı ve sıralamanın benzersiz olmadığında sayfalamayı nasıl bozduğu.
Sonraki ders sorgu parametrelerinin tasarımını bu iki tuzakla birlikte ele alır.

### Filtreleme, Sıralama ve Arama

Source: https://academia.sh/tr/kurslar/api-tasarimi/filtreleme-siralama-ve-arama

Sorgu parametrelerinin tasarımı: süzgeç ve sıralama adlarının izin listesiyle sütuna çevrilmesi, istemci değerlerinin bağlı değişkenle taşınması, izin listesi dışındaki isteğin reddedilmesi ve sıralama bağlarının sayfalamada kayıt kaybına yol açması.

Sayfalama dersi koleksiyonun ne kadarının verileceğini çözdü. Hangi kayıtların verileceği
ve hangi sırayla verileceği açık kaldı. Kütüphane görevlisi katalogda "Aho'nun kitapları",
"1990 sonrası basımlar", "adında Ağ geçenler" diye arar; sonucu yıla ya da yazara göre
sıralı ister.

Bunların hepsi sorgu bölümünde taşınır ve hepsi SQL'e dönüşür. Dönüşüm iki yerde tasarım
kararı ister: istemciden gelen **ad** hangi sütuna karşılık gelecek, istemciden gelen
**değer** sorguya nasıl girecek. Bu iki soruyu karıştırmamak, bu dersin ana ayrımıdır.

#### Ad ile Değerin Ayrılması

Bir sorgu parametresi iki farklı türde bilgi taşıyabilir. `?yazar=Aho` isteğinde `yazar`
bir addır — hangi sütuna bakılacağını söyler — `Aho` ise bir değerdir. İkisi SQL'e aynı
yoldan giremez.

Değer, sorgu metnine hiç girmez; **bağlı değişken** olarak ayrı taşınır. Motor sorgu
metnini bir kez çözümler, değeri sonra yerleştirir; değerin içeriği sorgunun yapısını
değiştiremez. İleri SQL kursundaki Dinamik SQL Riskleri dersi bunun neden zorunlu olduğunu
ayrıntılandırır: değerler dizgi birleştirmeyle sorgu metnine yazıldığında, değerin içindeki
karakterler sorgu yapısının parçası hâline gelir.

Ad ise bağlı değişken olamaz — sütun adı sorgunun yapısıdır, değeri değil. Bu yüzden adlar
için başka bir düzenek gerekir: **izin listesi**. İstemciden gelen ad, sunucudaki sabit bir
eşlemede aranır; eşlemede yoksa istek reddedilir. Böylece sorgu metnine yalnızca sunucunun
kendi yazdığı sütun adları girer.

```js
// filtre-sunucusu.mjs — sorgu parametrelerini izin listesi ve bagli degiskenle SQL'e tasir
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

// Izin listeleri: istemciden gelen ad, sutun adina burada cevrilir.
const SUZGECLER = {
  yazar:    "yazar = ?",
  yilEnAz:  "yil >= ?",
  sube:     "sube = ?",
  ara:      "ad LIKE ?",
};
const SIRALAMALAR = { yil: "yil", ad: "ad", yazar: "yazar" };

const sunucu = createServer((istek, yanit) => {
  const adres = new URL(istek.url, "http://127.0.0.1");
  if (adres.pathname !== "/kitaplar") return yanitla(yanit, 404, { hata: "yol_yok" });
  const s = adres.searchParams;
  const boyut = Math.min(Number(s.get("boyut") ?? 3), 50);

  // Suzgecler: yalnizca izin listesindeki adlar kosula donusur.
  const kosullar = [], degerler = [];
  for (const [ad, kalip] of Object.entries(SUZGECLER)) {
    const deger = s.get(ad);
    if (deger === null) continue;
    kosullar.push(kalip);
    degerler.push(ad === "ara" ? `%${deger}%` : deger);   // deger, sorgu metnine girmez
  }

  // Siralama: izin listesinde yoksa istek reddedilir, varsayilana dusulmez.
  const siralaAdi = s.get("sirala") ?? "yil";
  const sutun = SIRALAMALAR[siralaAdi];
  if (!sutun)
    return yanitla(yanit, 422, {
      hata: "dogrulama", alan: "sirala", izinli: Object.keys(SIRALAMALAR),
    });

  // Imlec: gevsek kip yalnizca siralama sutununu, kesin kip benzersizlestiriciyi de tasir.
  const imlec = s.get("imlec");
  const kesin = s.get("kip") !== "gevsek";
  if (imlec !== null) {
    if (kesin) {
      const [d, k] = imlec.split("|");
      kosullar.push(`(${sutun}, isbn) > (?, ?)`);
      degerler.push(d, k);
    } else {
      kosullar.push(`${sutun} > ?`);
      degerler.push(imlec);
    }
  }

  const nerede = kosullar.length ? `WHERE ${kosullar.join(" AND ")}` : "";
  const sira = kesin ? `${sutun}, isbn` : sutun;
  const satirlar = db.prepare(
    `SELECT isbn, ad, yazar, yil FROM kitap ${nerede} ORDER BY ${sira} LIMIT ?`
  ).all(...degerler, boyut);

  const son = satirlar.at(-1);
  yanitla(yanit, 200, {
    veri: satirlar.map((r) => `${r.yil} ${r.isbn}`),
    sayfalama: {
      sonrakiImlec: son ? (kesin ? `${son[sutun]}|${son.isbn}` : String(son[sutun])) : null,
    },
  });
});

sunucu.listen(8481, "127.0.0.1", () => console.log("filtre sunucusu 127.0.0.1:8481"));
```

Sorgu metninin nasıl kurulduğuna dikkat edin. `kosullar` dizisine giren her parça
sunucunun kendi yazdığı sabit bir dizgidir; istemciden gelen hiçbir karakter oraya
ulaşmaz. İstemcinin gönderdiği her şey `degerler` dizisindedir ve sorguya bağlı değişken
olarak verilir.

Katalog, sıralama bağlarını görebilmek için genişletilir:

```sql
-- katalog.sql — siralama baglarini iceren genisletilmis katalog
DELETE FROM kitap;
INSERT INTO kitap (isbn, ad, yazar, yil, sube) VALUES
  ('K-01','Bilgisayar Aglari','Tanenbaum',1978,'S-01'),
  ('K-02','Veri Yapilari','Aho',1983,'S-01'),
  ('K-03','Derleyiciler','Aho',1983,'S-02'),
  ('K-04','Isletim Sistemleri','Tanenbaum',1983,'S-02'),
  ('K-05','Kabuk Betikleri',"O'Reilly",1986,'S-01'),
  ('K-06','Cizge Algoritmalari','Sedgewick',1990,'S-01'),
  ('K-07','Sayisal Yontemler','Press',1990,'S-02'),
  ('K-08','Ag Programlama','Stevens',1990,'S-01'),
  ('K-09','Bicimsel Diller','Hopcroft',1995,'S-02');
```

Sayfaları dolaşan istemci de ayrı bir dosyadır; imleci kendisi kurmaz, yanıttan alır.

```js
// yuru.mjs — imleci izleyerek butun sayfalari dolasan istemci
// Kullanim: node yuru.mjs <gevsek|kesin>
const KIP = process.argv[2];
let imlec = null, sayfa = 0;
const toplananlar = [];

while (sayfa < 10) {
  const adres = new URL("http://127.0.0.1:8481/kitaplar");
  adres.searchParams.set("kip", KIP);
  adres.searchParams.set("sirala", "yil");
  if (imlec !== null) adres.searchParams.set("imlec", imlec);

  const yanit = await (await fetch(adres)).json();
  if (yanit.veri.length === 0) break;
  sayfa++;
  toplananlar.push(...yanit.veri);
  console.log(`${KIP.padEnd(7)} sayfa ${sayfa}: ${yanit.veri.join("  ")}`);
  imlec = yanit.sayfalama.sonrakiImlec;
}
console.log(`${KIP.padEnd(7)} toplam : ${toplananlar.length} satir\n`);
```

```bash
# Izin listesi, bagli degisken ve siralama baginin sayfalamaya etkisi.
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql && sqlite3 kutuphane.db < katalog.sql
node filtre-sunucusu.mjs & sunucu=$!
sleep 0.4

al() { curl -sS -G "http://127.0.0.1:8481/kitaplar" "$@"; echo; }
echo "--- suzgec ve izin listesi ---"
printf 'yazar=Aho      : '; al --data-urlencode "yazar=Aho"
printf "yazar=O'Reilly : "; al --data-urlencode "yazar=O'Reilly"
printf 'ara=Ag         : '; al --data-urlencode "ara=Ag"
printf 'sirala=sube    : '; al --data-urlencode "sirala=sube"

echo "--- siralama bagi olan sutunda sayfalama (katalogda 9 kitap var) ---"
node yuru.mjs gevsek
node yuru.mjs kesin

kill $sunucu
```

```
filtre sunucusu 127.0.0.1:8481
--- suzgec ve izin listesi ---
yazar=Aho      : {"veri":["1983 K-02","1983 K-03"],"sayfalama":{"sonrakiImlec":"1983|K-03"}}
yazar=O'Reilly : {"veri":["1986 K-05"],"sayfalama":{"sonrakiImlec":"1986|K-05"}}
ara=Ag         : {"veri":["1978 K-01","1990 K-08"],"sayfalama":{"sonrakiImlec":"1990|K-08"}}
sirala=sube    : {"hata":"dogrulama","alan":"sirala","izinli":["yil","ad","yazar"]}
--- siralama bagi olan sutunda sayfalama (katalogda 9 kitap var) ---
gevsek  sayfa 1: 1978 K-01  1983 K-02  1983 K-03
gevsek  sayfa 2: 1986 K-05  1990 K-06  1990 K-07
gevsek  sayfa 3: 1995 K-09
gevsek  toplam : 7 satir

kesin   sayfa 1: 1978 K-01  1983 K-02  1983 K-03
kesin   sayfa 2: 1983 K-04  1986 K-05  1990 K-06
kesin   sayfa 3: 1990 K-07  1990 K-08  1995 K-09
kesin   toplam : 9 satir
```

#### Değerin Sorgu Metnine Girmemesi

İkinci satır bu ayrımın en somut kanıtıdır. `O'Reilly` değeri kesme işareti içerir ve SQL
dizgi sabitlerini bitiren karakter tam olarak odur. Değer sorgu metnine yazılsaydı, sorgu
o noktada bölünür ve kalan bölüm SQL olarak okunurdu. Bağlı değişkenle taşındığında böyle
bir şey olmaz: değer yalnızca değerdir, kaydı bulur ve döndürür.

Bu, kesme işaretli adların "kaçırılması" sorunu değildir. Kaçış karakteri eklemek de bir
çözüm gibi görünür, ama her veri tipi ve her lehçe için ayrı kural gerektirir ve bir yerde
unutulur. Bağlı değişkende unutulacak bir şey yoktur; sorgu metni istemciden bağımsız
olarak sabittir.

Üçüncü satırdaki `ara=Ag` süzgeci aynı ilkeyi bir adım öteye taşır. Joker karakterler
sunucuda eklenir — istemcinin gönderdiği `Ag` değeri `%Ag%` hâline getirilir — böylece
istemci örüntü yazamaz, yalnızca aranacak metni verir.

#### İzin Listesi Dışını Reddetmek

Dördüncü satır `sirala=sube` isteğine 422 döndü ve izinli adları yanıtta bildirdi.
Buradaki iki karar da bilinçlidir.

Birincisi, istek **reddedildi**; sessizce varsayılan sıralamaya düşülmedi. Varsayılana
düşmek, istemcinin yanlış bir ad gönderdiğini hiç öğrenmemesi demektir; ekran yanlış sırada
görünür ve kimse nedenini aramaz. Durum Kodu Seçimi dersindeki ölçüt burada da geçerlidir:
kabul edilmeyen bir alan değeri 422 ile bildirilir.

İkincisi, yanıt izinli adları sayıyor. Bu, sözleşmenin kendini anlatan bir parçasıdır;
istemciyi belgeye gitmeden düzeltmeye yönlendirir.

İzin listesi aynı zamanda sıralamanın maliyetini denetim altında tutar. Her sütuna göre
sıralamaya izin verilirse, dizini olmayan bir sütuna göre sıralama isteği tabloyu baştan
sona okutur. Listeye yalnızca desteklenmesi kararlaştırılmış — ve gerekiyorsa dizinlenmiş —
sütunlar konur.

#### Sıralama Bağı Kayıt Kaybettirir

Çıktının son bölümü sayfalama dersine geri bağlanır. Katalogda dokuz kitap var; gevşek kip
yedisini gösterdi. K-04 ve K-08 hiçbir sayfada görünmedi.

Neden ölçülebilir biçimde ortadadır. Gevşek kipte imleç yalnızca yılı taşır. Birinci sayfa
1983 yılında biter ve imleç `1983` olur; ikinci sayfa `yil > 1983` koşuluyla gelir ve
aynı yıla sahip K-04 kaydı bu koşulun dışında kalır. Aynı şey 1990 yılında yinelenir ve
K-08 kaybolur. Sıralama anahtarı benzersiz olmadığı sürece, "bu değerden sonrası" ölçütü
bağların bir bölümünü hep dışarıda bırakır.

Kesin kipte imleç iki alan taşır: sıralama sütunu ve benzersiz kimlik. Koşul da iki alan
üzerinde çalışır, sıralama da. Aynı yıla sahip kayıtlar arasında kesin bir sıra kurulduğu
için "bu noktadan sonrası" tanımlıdır ve dokuz kaydın dokuzu da görünür.

Buradan iki kural çıkar. Sıralama ölçütü ne olursa olsun, sonuna benzersiz bir alan
eklenir — **belirlenimci sıralama** koşulu budur. İmleç de sıralama ölçütünün tamamını
taşır: tek alanlık bir imleç, tek alanlık bir sıralamayı anlatabilir.

#### Özet

- Sorgu parametresi iki tür bilgi taşır: ad ve değer. Ad izin listesiyle sütuna çevrilir,
  değer bağlı değişkenle taşınır; ikisi SQL'e aynı yoldan girmez.
- Sorgu metnine yalnızca sunucunun yazdığı sabit parçalar girer; istemciden gelen hiçbir
  karakter sorgu yapısına ulaşmaz. Kesme işareti içeren bir yazar adı bu yüzden sorunsuz
  bulunur.
- Arama örüntüleri sunucuda kurulur; istemci aranacak metni verir, joker karakteri değil.
- İzin listesi dışındaki sıralama isteği 422 ile reddedilir ve izinli adlar yanıtta
  bildirilir; sessizce varsayılana düşmek hatayı görünmez kılar.
- İzin listesi sıralamanın maliyetini de sınırlar: listeye yalnızca desteklenmesi
  kararlaştırılmış sütunlar konur.
- Sıralama anahtarı benzersiz değilse imleç tabanlı sayfalama bağların bir bölümünü atlar;
  ölçümde dokuz kayıttan yedisi göründü. Sıralamaya ve imlece benzersiz bir alan eklenmesi
  bu kaybı ortadan kaldırır.

#### Sonraki Adım

Hangi kayıtların döneceği ve hangi sırayla döneceği çözüldü. Geriye her kaydın **ne
kadarının** döneceği kaldı. Katalog listesi ekranı yalnızca kitap adı ve yazarı gösterirken
sunucu her satırda tüm alanları gönderiyor; ödünç geçmişi ekranı ise tek bir yanıtta
bulamadığı bilgiler için ayrı ayrı istek atıyor. İki sorun da aynı yerden doğar: gösterimin
ayrıntı düzeyine sunucu tek başına karar veriyor. Sonraki ders istemcinin alan seçmesine
izin veren düzeneği kurar ve aktarılan bayt miktarındaki farkı ölçer.

### Kısmi Yanıt ve Alan Seçimi

Source: https://academia.sh/tr/kurslar/api-tasarimi/kismi-yanit-ve-alan-secimi

Gösterimin ayrıntı düzeyine istemcinin katılması: aşırı ve eksik veri çekmenin istek sayısı ve bayt cinsinden ölçülmesi, alan seçimi ile genişletmenin izin listesiyle kurulması ve bu esnekliğin sözleşmeye getirdiği maliyet.

Önceki iki ders koleksiyonun hangi kayıtlarının, hangi sırayla ve ne kadarının döneceğini
çözdü. Her kaydın **ne kadarının** döneceği açık kaldı.

Kütüphane arayüzünde iki ekran vardır. Ödünç listesi ekranı her satırda yalnızca kitap
adını, üye adını ve veriliş tarihini gösterir. Ödünç ayrıntısı ekranı ise kaydın tüm
alanlarını ister. İkisi de aynı koleksiyondan beslenir. Sunucu tek bir gösterim
döndürüyorsa, bu gösterim ya listeye göre dar kalır ya da ayrıntıya göre geniş olur. İki
durumun da ölçülebilir bir bedeli vardır.

#### Aşırı ve Eksik Veri Çekme

**Aşırı veri çekme** (over-fetching), istemcinin kullanmadığı alanların da gönderilmesidir.
Ödünç listesi ekranı üç alan gösterirken sunucu on alan yolluyorsa, aradaki fark ağdan
geçer, istemcide çözümlenir ve atılır.

**Eksik veri çekme** (under-fetching), tek isteğin ekranı besleyememesidir. Ödünç kaydı
yalnızca ISBN ve üye kodu taşıyorsa, kitap adını ve üye adını almak için her satır başına
iki istek daha gerekir. Kırk satırlık bir liste, seksen ek istek demektir.

İkisi zıt yönlü görünür ama aynı kökten gelir: gösterimin ayrıntı düzeyine sunucu tek
başına karar vermektedir. Çözüm, kararın bir bölümünü istemciye vermektir — ama sınırları
sunucunun çizdiği bir esneklikle.

```js
// alan-secim-sunucusu.mjs — alan secimi ve genisletme destekleyen odunc koleksiyonu
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

// Izin listesi: istemcinin adlandirdigi alan, burada sutun ifadesine cevrilir.
const TEMEL = { id: "o.id", uye: "o.uye", isbn: "o.isbn", verilis: "o.verilis", iade: "o.iade" };
const GENIS = { kitapAdi: "k.ad", kitapYazari: "k.yazar", uyeAdi: "u.ad", uyeSubesi: "u.sube" };

const sunucu = createServer((istek, yanit) => {
  const adres = new URL(istek.url, "http://127.0.0.1");
  const s = adres.searchParams;

  if (/^\/kitaplar\/[^/]+$/.test(adres.pathname)) {
    const isbn = adres.pathname.split("/")[2];
    const k = db.prepare("SELECT * FROM kitap WHERE isbn = ?").get(isbn);
    return k ? yanitla(yanit, 200, k) : yanitla(yanit, 404, { hata: "kitap_yok" });
  }
  if (/^\/uyeler\/[^/]+$/.test(adres.pathname)) {
    const kod = adres.pathname.split("/")[2];
    const u = db.prepare("SELECT * FROM uye WHERE kod = ?").get(kod);
    return u ? yanitla(yanit, 200, u) : yanitla(yanit, 404, { hata: "uye_yok" });
  }
  if (adres.pathname !== "/oduncler") return yanitla(yanit, 404, { hata: "yol_yok" });

  // Genisletme: bagli kaynaklarin alanlari yaniten icine katilir.
  const genislet = (s.get("genislet") ?? "").split(",").filter(Boolean);
  const sunulan = { ...TEMEL };
  if (genislet.includes("kitap")) Object.assign(sunulan, { kitapAdi: GENIS.kitapAdi, kitapYazari: GENIS.kitapYazari });
  if (genislet.includes("uye")) Object.assign(sunulan, { uyeAdi: GENIS.uyeAdi, uyeSubesi: GENIS.uyeSubesi });

  // Alan secimi: istenen adlar sunulanlarla kesistirilir, tanimsiz ad reddedilir.
  const istenen = (s.get("alanlar") ?? "").split(",").filter(Boolean);
  const bilinmeyen = istenen.filter((a) => !(a in sunulan));
  if (bilinmeyen.length)
    return yanitla(yanit, 422, { hata: "dogrulama", alan: "alanlar", bilinmeyen, sunulan: Object.keys(sunulan) });
  const secilen = istenen.length ? istenen : Object.keys(sunulan);

  const secim = secilen.map((a) => `${sunulan[a]} AS ${a}`).join(", ");
  const satirlar = db.prepare(
    `SELECT ${secim} FROM odunc o
       LEFT JOIN kitap k ON k.isbn = o.isbn
       LEFT JOIN uye u ON u.kod = o.uye
      ORDER BY o.id LIMIT ?`
  ).all(Number(s.get("boyut") ?? 40));

  yanitla(yanit, 200, { veri: satirlar, sayfalama: { boyut: satirlar.length } });
});

sunucu.listen(8482, "127.0.0.1", () => console.log("alan secim sunucusu 127.0.0.1:8482"));
```

Sütun adları yine izin listesinden geliyor; bir önceki dersteki ayrım burada da geçerlidir.
İstemci `kitapAdi` adını gönderir, sunucu onu `k.ad` ifadesine çevirir. Listede olmayan bir
ad sorgu metnine ulaşamaz.

#### Ölçüm

Aynı ekranı üç farklı strateji besler. Ölçüm, istek sayısını ve aktarılan gövde baytını
sayar.

```js
// olcum.mjs — uc stratejinin istek sayisini ve aktarilan bayti olcer
const T = "http://127.0.0.1:8482";
let istek = 0, bayt = 0;
const cek = async (yol) => {
  istek++;
  const metin = await (await fetch(T + yol)).text();
  bayt += Buffer.byteLength(metin);
  return JSON.parse(metin);
};
const yaz = (ad) => { console.log(`${ad.padEnd(34)} istek: ${String(istek).padStart(3)}  bayt: ${bayt}`); istek = 0; bayt = 0; };

// 1) Ayri ayri: odunc listesi, sonra her kayit icin kitap ve uye.
const liste = await cek("/oduncler");
for (const o of liste.veri) { await cek(`/kitaplar/${o.isbn}`); await cek(`/uyeler/${o.uye}`); }
yaz("ayri istekler (eksik veri cekme)");

// 2) Genisletme: tek istek, ama butun alanlar geliyor.
await cek("/oduncler?genislet=kitap,uye");
yaz("genisletme (asiri veri cekme)");

// 3) Genisletme + alan secimi: tek istek, yalnizca ekranin kullandigi alanlar.
await cek("/oduncler?genislet=kitap,uye&alanlar=id,verilis,kitapAdi,uyeAdi");
yaz("genisletme + alan secimi");
```

```bash
# Kirk odunc kaydi; ayni ekran uc farkli stratejiyle beslenir.
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql && sqlite3 kutuphane.db < katalog.sql
sqlite3 kutuphane.db <<'SQL'
DELETE FROM odunc;
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 40)
INSERT INTO odunc (uye, isbn, verilis, iade)
SELECT CASE WHEN n % 2 = 0 THEN 'U-1001' ELSE 'U-1002' END,
       'K-0' || (1 + n % 9), date('2026-01-01', '+' || n || ' day'), NULL
FROM s;
SQL
node alan-secim-sunucusu.mjs & sunucu=$!
sleep 0.4

node olcum.mjs
echo "--- ornek yanit satiri ---"
curl -sS "http://127.0.0.1:8482/oduncler?genislet=kitap,uye&alanlar=id,verilis,kitapAdi,uyeAdi&boyut=2"; echo
curl -sS "http://127.0.0.1:8482/oduncler?alanlar=id,kitapAdi&boyut=2"; echo

kill $sunucu
```

```
alan secim sunucusu 127.0.0.1:8482
ayri istekler (eksik veri cekme)   istek:  81  bayt: 8151
genisletme (asiri veri cekme)      istek:   1  bayt: 6751
genisletme + alan secimi           istek:   1  bayt: 3404
--- ornek yanit satiri ---
{"veri":[{"id":1,"verilis":"2026-01-02","kitapAdi":"Veri Yapilari","uyeAdi":"Berk Tuna"},{"id":2,"verilis":"2026-01-03","kitapAdi":"Derleyiciler","uyeAdi":"Aylin Deren"}],"sayfalama":{"boyut":2}}
{"hata":"dogrulama","alan":"alanlar","bilinmeyen":["kitapAdi"],"sunulan":["id","uye","isbn","verilis","iade"]}
```

İlk satır eksik veri çekmenin bedelini gösteriyor: otuz dokuz kayıt için seksen bir istek.
Bayt farkı burada asıl mesele değildir; asıl mesele istek sayısıdır. Her istek bir ağ turu
demektir ve turlar birbirini beklediğinde gecikme doğrudan çarpılır. Aynı kitap ve aynı üye
defalarca istendiği için tekrar da vardır.

İkinci satır tek isteğe iniyor ama 6751 bayt taşıyor. Ekranın kullanmadığı beş alan —
ISBN, üye kodu, iade tarihi, kitap yazarı, üye şubesi — her satırda geçiyor.

Üçüncü satır ikisini birlikte çözüyor: tek istek, 3404 bayt. Aynı ekran, yaklaşık yarısı kadar
veriyle besleniyor. Oran veriye bağlıdır; burada tasarruf, alan adlarının ve kullanılmayan
değerlerin toplam gövde içindeki payından gelir.

Son satır sözleşmenin kendini savunmasıdır. `alanlar=id,kitapAdi` isteği `genislet=kitap`
olmadan gelmiş; `kitapAdi` o istekte sunulan alanlar arasında değil. Sunucu isteği 422 ile
reddediyor ve hem bilinmeyen adı hem de o istekte sunulan alanları bildiriyor. Sessizce
alanı atlamak, istemcinin hiç var olmayan bir alanı beklemesine ve `undefined` bir değerle
ekran çizmesine yol açardı.

#### Esnekliğin Sözleşmedeki Maliyeti

Alan seçimi bedava değildir ve bedeli sözleşme yüzeyinde ödenir.

Birincisi, **önbellek anahtarı çoğalır**. Adres bir önbellek anahtarıdır; alan listesi
adrese girdiği için `alanlar=id,ad` ile `alanlar=ad,id` iki ayrı anahtar üretir. Aynı
veriyi iki kez saklamamak için alan listesinin kanonik biçimi belirlenmelidir: adlar
sıralanır ve yinelenenler atılır. Kanonik biçim kararı URI Tasarımı dersindeki kararla
aynı türdendir.

İkincisi, **sınanacak birleşim sayısı artar**. Beş temel ve dört genişletme alanı olan bir
uç noktada, istemcinin isteyebileceği alan kümesi sayısı elle sınanamayacak kadar çoktur.
Bu yüzden izin listesi dar tutulur: her sütun değil, ekranların gerçekten istediği alanlar
sunulur.

Üçüncüsü, **genişletmenin derinliği sınırlanmalıdır**. Bu sunucu tek düzey genişletme
sunuyor: ödünçten kitaba ve üyeye. İki düzeye izin verilseydi — kitaptan şubeye, şubeden
sorumlu üyeye — tek bir istek, sunucu tarafında dallanan bir sorgu ağacına dönüşürdü.
İstemcinin gördüğü tek istek, sunucunun ödediği bedeli gizler.

#### Alan Seçimi mi, Adlandırılmış Görünüm mü

Alan seçiminin alternatifi, sunucunun birkaç hazır gösterim tanımlamasıdır: `?gorunum=liste`
ve `?gorunum=ayrinti` gibi. Bu yaklaşımda esneklik azalır, ama üç şey kazanılır. Önbellek
anahtarı sayısı ikiye iner. Sınanacak birleşim kalmaz. Sunucu her görünüm için tek bir
sorgu yazıp onu eniyileyebilir.

Seçim ölçütü istemci sayısıdır. Sunucuyu tek bir arayüz kullanıyorsa adlandırılmış görünüm
yeter; ekran değiştikçe görünüm tanımı da değişir. Birbirinden bağımsız çok sayıda istemci
varsa — mobil uygulama, yönetim paneli, başka bir servis — her birine ayrı görünüm tanımlamak
sözleşmeyi şişirir; alan seçimi bu durumda daha az bakım ister.

İki yaklaşım birlikte de kullanılır: yaygın durumlar için adlandırılmış görünümler, geri
kalan için alan seçimi. Bu, sorgu temelli biçemlerin baştan çözdüğü bir sorunun kaynak
temelli biçemdeki karşılığıdır; API Biçemleri konusunda tartışılan ödünleşim burada somut
sayılara dönüşür.

#### Özet

- Aşırı veri çekme kullanılmayan alanların taşınmasıdır; eksik veri çekme tek isteğin
  ekranı besleyememesidir. İkisi de gösterimin ayrıntı düzeyine yalnız sunucunun karar
  vermesinden doğar.
- Ölçümde aynı ekran ayrı isteklerle 81 istek ve 8151 bayt, genişletmeyle 1 istek ve 6751
  bayt, genişletme ve alan seçimiyle 1 istek ve 3404 bayt tüketti.
- Alan adları ve genişletme adları izin listesinden geçer; sorgu metnine yalnızca sunucunun
  yazdığı sütun ifadeleri girer.
- Tanımsız alan isteği sessizce atlanmaz, 422 ile reddedilir ve o istekte sunulan alanlar
  yanıtta bildirilir.
- Alan seçimi önbellek anahtarını çoğaltır ve sınanacak birleşim sayısını artırır; alan
  listesinin kanonik biçimi belirlenmeli ve izin listesi dar tutulmalıdır.
- Az sayıda istemcisi olan servislerde adlandırılmış görünümler alan seçiminden daha az
  bakım ister; iki yaklaşım birlikte de kullanılabilir.

#### Sonraki Adım

Buraya kadarki dört ders okuma tarafını düzeltti: hangi kayıtlar, hangi sırayla, ne kadarı,
kaç istekle. Yazma tarafında ise üçüncü dersten kalan bir açık duruyor. POST etkisiz
değildir; ağ koptuğunda ya da yanıt yolda kaybolduğunda istemci isteği tekrarlar ve ikinci
bir ödünç kaydı doğar. İstemci isteğin ulaşıp ulaşmadığını bilemez, sunucu da iki isteğin
aynı niyetten geldiğini göremez. Sonraki ders bu boşluğu istemcinin ürettiği bir anahtarla
kapatır ve aynı anahtarla gönderilen iki isteğin tek etki ürettiğini sayarak gösterir.

### Etkisizlik Anahtarları

Source: https://academia.sh/tr/kurslar/api-tasarimi/etkisizlik-anahtarlari

Yeniden denemede yinelenen etkiyi önleyen tekillik anahtarı: anahtarın kapsamı ve gövde özetiyle eşleştirilmesi, aynı anahtarla gelen ikinci isteğin ilk yanıtı yeniden sunması, farklı gövdeyle çakışmanın reddi ve anahtar deposunun yaşam süresi.

Üçüncü ders POST'un etkisiz olmadığını ölçmüştü: aynı istek iki kez gönderildiğinde iki
ödünç kaydı doğdu. O zaman bu, POST'un doğası diye geçildi. Sekizinci ders okuma tarafını
kapatınca geriye bu açık kaldı.

Açık, gerçek bir durumdan doğar. İstemci ödünç kaydı oluşturma isteğini gönderir, ağ
kopar. Uygulama Mimarisi kursundaki hata sözleşmesinin ağ hatası sınıfı tam olarak bunu
anlatıyordu: **istek sunucuya ulaşmış da olabilir, ulaşmamış da.** Yanıt yolda kaybolduysa
kayıt oluşmuştur ama istemci bunu bilmez. İstemci yeniden denerse ikinci kayıt riski
vardır; denemezse üye kitabı alamamış olabilir. İki seçenek de kabul edilemez.

Bu ders üçüncü bir yol kurar: isteğe, istemcinin ürettiği bir kimlik eklemek.

#### Anahtarın İşi

**Tekillik anahtarı** (idempotency key), istemcinin bir işlem niyetine verdiği benzersiz
addır. Anahtar `Idempotency-Key` başlığıyla taşınır ve üç kuralı vardır.

Anahtar **istek başına değil, niyet başına** üretilir. Kullanıcı "ödünç ver" düğmesine
bastığında bir anahtar üretilir; ağ koptuğu için yapılan üç deneme aynı anahtarı taşır.
Kullanıcı ikinci kez ödünç almak isterse yeni bir anahtar üretilir.

Anahtar **yalnız başına yeterli değildir**. Sunucu, anahtarın hangi işlemle
ilişkilendirildiğini de saklamalıdır: hangi adrese, hangi gövdeyle. Aksi hâlde istemcinin
yanlışlıkla aynı anahtarı başka bir istekte kullanması, o isteğin sessizce yutulmasına yol
açar.

Anahtar **yanıtla birlikte saklanır**. İkinci istek geldiğinde işlem tekrar yapılmaz; ilk
seferde üretilmiş yanıt olduğu gibi geri verilir. İstemci ilk yanıtı hiç görmemiş olsa bile
sonucu öğrenir.

```js
// anahtar-sunucusu.mjs — tekillik anahtariyla korunan odunc olusturma
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";
import { createHash } from "node:crypto";

const db = new DatabaseSync("kutuphane.db");
db.exec(`CREATE TABLE IF NOT EXISTS tekillik (
  anahtar   TEXT PRIMARY KEY,
  yol       TEXT NOT NULL,
  ozet      TEXT NOT NULL,
  durum     INTEGER,
  yanit     TEXT,
  olusma    TEXT NOT NULL
)`);

const govdeOku = (istek) => new Promise((coz) => {
  let veri = ""; istek.on("data", (p) => (veri += p));
  istek.on("end", () => coz(veri));
});
const gonder = (yanit, kod, nesne, ek = {}) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8", ...ek });
  yanit.end(JSON.stringify(nesne));
};

const sunucu = createServer(async (istek, yanit) => {
  const yol = new URL(istek.url, "http://127.0.0.1").pathname;
  if (istek.method !== "POST" || yol !== "/oduncler") return gonder(yanit, 404, { hata: "yol_yok" });

  const ham = await govdeOku(istek);
  const anahtar = istek.headers["idempotency-key"];
  const ozet = createHash("sha256").update(ham).digest("hex").slice(0, 16);
  const g = JSON.parse(ham);

  // Anahtarsiz istek: her cagri yeni kayit uretir.
  if (!anahtar) return gonder(yanit, ...olustur(g));

  const kayit = db.prepare("SELECT * FROM tekillik WHERE anahtar = ?").get(anahtar);
  if (kayit) {
    // Ayni anahtar, farkli govde: istemci anahtari yeniden kullanmis.
    if (kayit.yol !== yol || kayit.ozet !== ozet)
      return gonder(yanit, 409, { hata: "anahtar_cakismasi", anahtar });
    return gonder(yanit, kayit.durum, JSON.parse(kayit.yanit), { "idempotency-replayed": "true" });
  }

  // Ayirma ve is, arada bekleme olmadan tek blokta yapilir.
  db.prepare("INSERT INTO tekillik (anahtar, yol, ozet, olusma) VALUES (?,?,?,datetime('now'))")
    .run(anahtar, yol, ozet);
  const [durum, govde] = olustur(g);
  db.prepare("UPDATE tekillik SET durum = ?, yanit = ? WHERE anahtar = ?")
    .run(durum, JSON.stringify(govde), anahtar);
  gonder(yanit, durum, govde);
});

function olustur(g) {
  if (!db.prepare("SELECT 1 FROM kitap WHERE isbn = ?").get(g.isbn))
    return [422, { hata: "dogrulama", alan: "isbn" }];
  const s = db.prepare("INSERT INTO odunc (uye, isbn, verilis, iade) VALUES (?,?,?,NULL)")
              .run(g.uye, g.isbn, g.verilis);
  return [201, { id: Number(s.lastInsertRowid), uye: g.uye, isbn: g.isbn, verilis: g.verilis }];
}

sunucu.listen(8483, "127.0.0.1", () => console.log("anahtar sunucusu 127.0.0.1:8483"));
```

#### Ölçüm

Aynı gövde iki kez gönderilir; önce anahtarsız, sonra anahtarla. Katalog, yedinci dersteki
`katalog.sql` dosyasıyla kurulur.

```bash
# Ayni istek iki kez gonderilir: once anahtarsiz, sonra tekillik anahtariyla.
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql && sqlite3 kutuphane.db < katalog.sql
sqlite3 kutuphane.db "DELETE FROM odunc;"
node anahtar-sunucusu.mjs & sunucu=$!
sleep 0.4

GOVDE='{"uye":"U-1001","isbn":"K-02","verilis":"2026-03-10"}'
say() { printf '%-30s odunc satiri: %s\n' "$1" "$(sqlite3 kutuphane.db 'SELECT COUNT(*) FROM odunc;')"; }
yolla() { curl -sS -D - -o /tmp/govde.$$ -X POST -H 'content-type: application/json' "$@" \
            -d "$GOVDE" http://127.0.0.1:8483/oduncler \
          | grep -iE '^(HTTP/|idempotency-replayed)' | tr -d '\r'; cat /tmp/govde.$$; echo; rm -f /tmp/govde.$$; }

echo "--- anahtarsiz, iki kez ---"
yolla; yolla; say "anahtarsiz x2"

sqlite3 kutuphane.db "DELETE FROM odunc; DELETE FROM tekillik;"
echo "--- ayni anahtarla, iki kez ---"
yolla -H 'Idempotency-Key: OD-7f21'
yolla -H 'Idempotency-Key: OD-7f21'
say "anahtarli x2"

echo "--- ayni anahtar, farkli govde ---"
curl -sS -o /dev/null -w 'durum: %{http_code}\n' -X POST -H 'content-type: application/json' \
  -H 'Idempotency-Key: OD-7f21' -d '{"uye":"U-1002","isbn":"K-03","verilis":"2026-03-11"}' \
  http://127.0.0.1:8483/oduncler
say "cakisma sonrasi"

kill $sunucu
```

```
anahtar sunucusu 127.0.0.1:8483
--- anahtarsiz, iki kez ---
HTTP/1.1 201 Created
{"id":1,"uye":"U-1001","isbn":"K-02","verilis":"2026-03-10"}
HTTP/1.1 201 Created
{"id":2,"uye":"U-1001","isbn":"K-02","verilis":"2026-03-10"}
anahtarsiz x2                  odunc satiri: 2
--- ayni anahtarla, iki kez ---
HTTP/1.1 201 Created
{"id":1,"uye":"U-1001","isbn":"K-02","verilis":"2026-03-10"}
HTTP/1.1 201 Created
idempotency-replayed: true
{"id":1,"uye":"U-1001","isbn":"K-02","verilis":"2026-03-10"}
anahtarli x2                   odunc satiri: 1
--- ayni anahtar, farkli govde ---
durum: 409
cakisma sonrasi                odunc satiri: 1
```

Anahtarsız iki istek iki kayıt üretti ve iki farklı kimlik döndürdü. Anahtarlı iki istek
tek kayıt üretti ve **aynı gövdeyi** döndürdü: ikinci yanıtın `id` alanı da 1. İkinci
yanıt ayrıca `Idempotency-Replayed` başlığını taşıyor; bu başlık zorunlu değildir, ama
istemcinin ve kütüklerin durumu ayırt etmesini sağlar.

Son bölüm anahtarın yeniden kullanımını sınıyor. Aynı `OD-7f21` anahtarı farklı bir gövdeyle
geldi ve istek 409 ile reddedildi; ödünç kaydı sayısı bir kaldı. Gövde özeti saklanmasaydı
bu istek sessizce yutulur, istemci ikinci ödüncün oluştuğunu sanırdı. Reddetmek doğru
davranıştır: anahtarın yanlış kullanıldığını yalnızca istemci düzeltebilir.

#### Eş Zamanlı İstekler

İki isteğin arka arkaya gelmesi kolay durumdur. Zor durum, ikisinin aynı anda gelmesidir:
istemci zaman aşımına uğrayıp yeniden denerken ilk istek hâlâ işlenmekte olabilir.

```bash
# Ayni anahtarli bes istek es zamanli gonderilir; uretilen kayit sayilir.
rm -f kutuphane.db yanit-*.json && sqlite3 kutuphane.db < sema.sql && sqlite3 kutuphane.db < katalog.sql
sqlite3 kutuphane.db "DELETE FROM odunc;"
node anahtar-sunucusu.mjs & sunucu=$!
sleep 0.4

pidler=""
for i in 1 2 3 4 5; do
  curl -sS -o "yanit-$i.json" -X POST -H 'content-type: application/json' \
    -H 'Idempotency-Key: OD-9c04' \
    -d '{"uye":"U-1002","isbn":"K-05","verilis":"2026-03-12"}' \
    http://127.0.0.1:8483/oduncler &
  pidler="$pidler $!"
done
wait $pidler

for f in yanit-*.json; do cat "$f"; echo; done | sort -u > cesitler.txt
echo "donen govde cesidi : $(wc -l < cesitler.txt | tr -d ' ')"
echo "govde              : $(cat cesitler.txt)"
echo "odunc satiri       : $(sqlite3 kutuphane.db 'SELECT COUNT(*) FROM odunc;')"

echo "--- anahtar deposunun yasi ---"
sqlite3 kutuphane.db "
  INSERT INTO tekillik (anahtar, yol, ozet, durum, yanit, olusma)
  VALUES ('OD-eski','/oduncler','0000',201,'{}',datetime('now','-30 day'));
  SELECT anahtar || '  yas: ' || CAST(julianday('now') - julianday(olusma) AS INT) || ' gun'
    FROM tekillik ORDER BY olusma;
  DELETE FROM tekillik WHERE olusma < datetime('now','-7 day');
  SELECT 'temizlik sonrasi kalan: ' || COUNT(*) FROM tekillik;"

kill $sunucu
```

```
anahtar sunucusu 127.0.0.1:8483
donen govde cesidi : 1
govde              : {"id":1,"uye":"U-1002","isbn":"K-05","verilis":"2026-03-12"}
odunc satiri       : 1
--- anahtar deposunun yasi ---
OD-eski  yas: 30 gun
OD-9c04  yas: 0 gun
temizlik sonrasi kalan: 1
```

Beş eş zamanlı istek tek kayıt üretti ve beşi de aynı gövdeyi aldı. Bunu sağlayan şey
kodun yapısıdır: anahtarın kayda geçirilmesi ile işin yapılması arasında hiçbir bekleme
noktası yok. Araya bir bekleme girerse iki istek de "anahtar yok" görüp ikisi de kayıt
oluşturabilir.

Bu düzenin tek süreçli bir sunucuda yeterli olması, çok süreçli bir dağıtımda yeterli
olacağı anlamına gelmez. Genel çözüm, anahtarın **birincil anahtar olarak yazılmasıdır**:
ikinci yazma girişimi veritabanı tarafından reddedilir ve o istek bekleyip yeniden okumaya
ya da "işlem sürüyor" yanıtına yönlendirilir. Bu tür yarışların işlem düzeyinde nasıl
yalıtıldığı İleri SQL kursundaki işlemler konusunun kapsamındadır.

#### Anahtar Deposunun Yaşam Süresi

Çıktının son bölümü anahtar deposunun yaşlanmasını gösteriyor. Otuz gün önce yazılmış bir
anahtar, yedi günlük saklama penceresiyle temizlendi.

Süre kararının iki sınırı vardır. Alt sınırı **istemcinin en uzun deneme penceresi**
belirler: bir istemci yirmi dört saat sonra hâlâ yeniden deniyorsa, anahtar o süre boyunca
saklanmalıdır. Uygulama Mimarisi kursundaki Yeniden Deneme ve Geri Çekilme dersinde
tanımlanan deneme bütçesi bu pencereyi verir. Üst sınırı depolama maliyeti ve tabloya
yazılan yanıt gövdelerinin büyüklüğü belirler.

Pencere dolduktan sonra aynı anahtarla gelen bir istek yeni bir işlem sayılır ve yeni kayıt
üretir. Bu, tasarımın kabul edilmiş sınırıdır; süre seçilirken bu bilinerek seçilir.

Son bir sınır da kapsamdır. Anahtar tek başına değil, kimliği doğrulanmış istemciyle
birlikte benzersiz olmalıdır: iki farklı istemcinin aynı anahtar dizgisini üretmesi,
birinin isteğinin diğerinin yanıtını almasına yol açmamalıdır. Yukarıdaki tabloda anahtar
tek başına birincil anahtardır; gerçek bir dağıtımda istemci kimliği de anahtarın parçası
olur.

#### Özet

- Ağ hatasında isteğin sunucuya ulaşıp ulaşmadığı bilinmez; yeniden denemek yinelenen etki,
  dememek eksik etki riski taşır.
- Tekillik anahtarı istek başına değil niyet başına üretilir; aynı işlemin bütün denemeleri
  aynı anahtarı taşır.
- Sunucu anahtarı yol ve gövde özetiyle birlikte saklar; ölçümde anahtarsız iki istek iki
  kayıt, anahtarlı iki istek bir kayıt üretti ve ikinci yanıt ilkinin aynısı oldu.
- Aynı anahtarın farklı gövdeyle gelmesi 409 ile reddedilir; sessizce yutmak istemciye
  olmayan bir işlemi olmuş gibi gösterir.
- Anahtarın kayda geçirilmesi ile işin yapılması arasına bekleme girmemelidir; eş zamanlı
  beş istek bu yapıyla tek kayıt üretti.
- Anahtar deposunun saklama süresi istemcinin en uzun deneme penceresinden kısa olamaz;
  pencere dolduktan sonra aynı anahtar yeni bir işlem sayılır.

#### Sonraki Adım

Dokuz ders boyunca istemcinin her adresi kendisi kurduğu varsayıldı: koleksiyon adresini,
sayfa imlecini, alan listesini, hatta yeni oluşturulan kaydın adresini. İstemci bunu
yapabilmek için sözleşmenin biçimini kendi içine gömer — ve sözleşme değiştiğinde her
istemci ayrı ayrı güncellenir. Sonraki ders bu bağımlılığı gevşetir: yanıt, istemcinin
bir sonraki isteğinde kullanacağı adresleri kendi içinde taşır. Ödünç kaydının hangi
işlemlere açık olduğu da aynı yerden okunur; iade edilmiş bir kayıt artık iade bağlantısı
sunmaz.

### Bağlantı Odaklı Yanıtlar

Source: https://academia.sh/tr/kurslar/api-tasarimi/baglanti-odakli-yanitlar

Yanıtın kendi içinde gezinme ve işlem bağlantıları taşıması: sayfaların yalnızca sonraki bağlantısı izlenerek dolaşılması, kaynağın durumuna göre açılıp kapanan işlem bağlantıları ve istemcinin adres kurma yükünün ölçülmesi.

Dokuz ders boyunca istemcinin adresleri kendisi kurduğu varsayıldı. Koleksiyon adresini,
sayfa imlecini, alan listesini, hatta yeni oluşturulan kaydın adresini istemci biliyordu.
Bunun bir bedeli var: sözleşmenin biçimi istemcinin içine gömülür. İmleç parametresinin adı
değişirse, iade işleminin hangi yöntemle yapıldığı değişirse ya da bir işlem koşula
bağlanırsa, her istemci ayrı ayrı güncellenmek zorundadır.

Bu ders bağımlılığı gevşetir. Yanıt, istemcinin bir sonraki isteğinde kullanacağı adresleri
kendi içinde taşır; istemci yalnızca giriş adresini bilir, gerisini okur. İkinci ve daha
önemli sonuç şudur: kaynağın **hangi işlemlere açık olduğu** da yanıttan okunur. İade
edilmiş bir ödünç kaydı artık iade bağlantısı sunmaz.

#### Bağlantının Yapısı

Bir bağlantı iki bilgiyi taşımalıdır: hangi adrese ve hangi yöntemle gidileceği. Yalnızca
adres taşıyan bir bağlantı, okuma ile yazmayı ayırt edilemez kılar. İşlem bağlantıları için
üçüncü bir bilgi daha gerekir: gövdede hangi alanların beklendiği.

Yanıtın hangi bağlantıları taşıyacağı kaynağın türüne göre değişir. Koleksiyon yanıtı
gezinme bağlantıları taşır: kendisi ve varsa sonraki sayfa. Tekil kaynak yanıtı ise kendi
adresini, bağlı kaynakların adreslerini ve o an uygulanabilir işlemleri taşır.

```js
// baglanti-sunucusu.mjs — yanitlarin icinde gezinme ve islem baglantilari tasidigi servis
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const govdeOku = (istek) => new Promise((coz) => {
  let veri = ""; istek.on("data", (p) => (veri += p));
  istek.on("end", () => coz(veri ? JSON.parse(veri) : {}));
});
const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

// Kaydin o anki durumu, hangi islemlerin acik oldugunu belirler.
const kayitGosterimi = (o) => ({
  id: o.id, uye: o.uye, isbn: o.isbn, verilis: o.verilis, iade: o.iade,
  baglantilar: {
    kendisi: { yol: `/oduncler/${o.id}`, yontem: "GET" },
    uye:     { yol: `/uyeler/${o.uye}`, yontem: "GET" },
    ...(o.iade === null
      ? { iade: { yol: `/oduncler/${o.id}`, yontem: "PATCH", alanlar: ["iade"] } }
      : {}),
  },
});

const sunucu = createServer(async (istek, yanit) => {
  const adres = new URL(istek.url, "http://127.0.0.1");
  const tekil = /^\/oduncler\/(\d+)$/.exec(adres.pathname);
  const uye = /^\/uyeler\/([\w-]+)$/.exec(adres.pathname);

  if (istek.method === "GET" && uye) {
    const u = db.prepare("SELECT * FROM uye WHERE kod = ?").get(uye[1]);
    return u ? yanitla(yanit, 200, u) : yanitla(yanit, 404, { hata: "uye_yok" });
  }

  if (istek.method === "GET" && adres.pathname === "/oduncler") {
    const boyut = Math.min(Number(adres.searchParams.get("boyut") ?? 3), 50);
    const imlec = adres.searchParams.get("imlec");
    const satirlar = imlec === null
      ? db.prepare("SELECT * FROM odunc ORDER BY id LIMIT ?").all(boyut)
      : db.prepare("SELECT * FROM odunc WHERE id > ? ORDER BY id LIMIT ?").all(Number(imlec), boyut);
    const son = satirlar.at(-1);
    // Sonraki sayfa yalnizca gercekten varsa bildirilir.
    const kalan = son
      ? db.prepare("SELECT COUNT(*) AS n FROM odunc WHERE id > ?").get(son.id).n : 0;
    return yanitla(yanit, 200, {
      veri: satirlar.map(kayitGosterimi),
      baglantilar: {
        kendisi: { yol: `/oduncler?boyut=${boyut}`, yontem: "GET" },
        ...(kalan > 0
          ? { sonraki: { yol: `/oduncler?boyut=${boyut}&imlec=${son.id}`, yontem: "GET" } }
          : {}),
      },
    });
  }

  if (istek.method === "GET" && tekil) {
    const o = db.prepare("SELECT * FROM odunc WHERE id = ?").get(Number(tekil[1]));
    return o ? yanitla(yanit, 200, kayitGosterimi(o)) : yanitla(yanit, 404, { hata: "odunc_yok" });
  }

  if (istek.method === "PATCH" && tekil) {
    const g = await govdeOku(istek);
    const id = Number(tekil[1]);
    if ("iade" in g) db.prepare("UPDATE odunc SET iade = ? WHERE id = ?").run(g.iade, id);
    return yanitla(yanit, 200, kayitGosterimi(db.prepare("SELECT * FROM odunc WHERE id = ?").get(id)));
  }

  yanitla(yanit, 404, { hata: "yol_yok" });
});

sunucu.listen(8484, "127.0.0.1", () => console.log("baglanti sunucusu 127.0.0.1:8484"));
```

İki ayrıntı özellikle önemlidir. `sonraki` bağlantısı yalnızca gerçekten kalan kayıt varsa
üretiliyor; istemcinin "boş sayfa gelirse dur" gibi bir kural yazmasına gerek kalmıyor.
`iade` bağlantısı ise yalnızca kaydın `iade` alanı boşken üretiliyor; iş kuralı gösterimin
içine yerleşmiş oluyor.

#### Yalnızca Giriş Adresini Bilen İstemci

Karşı taraftaki istemci tek bir adres yazıyor ve başka hiçbir adres kurmuyor.

```js
// gezgin.mjs — yalnizca giris adresini bilen, geri kalanini yanittan okuyan istemci
const TABAN = "http://127.0.0.1:8484";
const GIRIS = "/oduncler?boyut=3";
const kurulanAdres = 1;   // yalnizca giris adresi istemcide yaziliyor

const cagir = async (baglanti, govde) => {
  const secenek = { method: baglanti.yontem };
  if (govde) {
    secenek.headers = { "content-type": "application/json" };
    secenek.body = JSON.stringify(govde);
  }
  return (await fetch(TABAN + baglanti.yol, secenek)).json();
};

// 1) Sayfalar yalnizca "sonraki" baglantisi izlenerek dolasilir.
let sayfa = await (await fetch(TABAN + GIRIS)).json();
let sayfaSayisi = 1;
const kayitlar = [];
while (true) {
  kayitlar.push(...sayfa.veri);
  console.log(`sayfa ${sayfaSayisi}: ${sayfa.veri.map((k) => k.id).join(", ")}` +
              `   sonraki: ${sayfa.baglantilar.sonraki?.yol ?? "yok"}`);
  if (!sayfa.baglantilar.sonraki) break;
  sayfa = await cagir(sayfa.baglantilar.sonraki);
  sayfaSayisi++;
}
console.log(`toplam ${kayitlar.length} kayit, ${sayfaSayisi} sayfa`);

// 2) Islem baglantisi acik olan ilk kayit bulunur ve o baglanti izlenir.
const acik = kayitlar.find((k) => k.baglantilar.iade);
console.log(`\nislem oncesi  id=${acik.id} baglantilar: ${Object.keys(acik.baglantilar).join(", ")}`);
const guncel = await cagir(acik.baglantilar.iade, { iade: "2026-03-15" });
console.log(`islem sonrasi id=${guncel.id} baglantilar: ${Object.keys(guncel.baglantilar).join(", ")}`);

// 3) Kaydin kendi adresi de yanittan okunur.
const yeniden = await cagir(guncel.baglantilar.kendisi);
console.log(`yeniden okuma id=${yeniden.id} iade=${yeniden.iade} ` +
            `baglantilar: ${Object.keys(yeniden.baglantilar).join(", ")}`);
console.log(`\nistemcide kurulan adres sayisi: ${kurulanAdres}`);
```

```bash
# Yedi odunc kaydi; istemci yalnizca giris adresini bilerek butun akisi yurutur.
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql && sqlite3 kutuphane.db < katalog.sql
sqlite3 kutuphane.db <<'SQL'
DELETE FROM odunc;
INSERT INTO odunc (id, uye, isbn, verilis, iade) VALUES
  (1,'U-1001','K-01','2026-02-01','2026-02-14'),
  (2,'U-1002','K-02','2026-02-05','2026-02-19'),
  (3,'U-1001','K-03','2026-03-01',NULL),
  (4,'U-1002','K-04','2026-03-02',NULL),
  (5,'U-1001','K-05','2026-03-03',NULL),
  (6,'U-1002','K-06','2026-03-04',NULL),
  (7,'U-1001','K-07','2026-03-05',NULL);
SQL
node baglanti-sunucusu.mjs & sunucu=$!
sleep 0.4

node gezgin.mjs
echo "--- tek kaydin ham gosterimi ---"
curl -sS http://127.0.0.1:8484/oduncler/4; echo

kill $sunucu
```

```
baglanti sunucusu 127.0.0.1:8484
sayfa 1: 1, 2, 3   sonraki: /oduncler?boyut=3&imlec=3
sayfa 2: 4, 5, 6   sonraki: /oduncler?boyut=3&imlec=6
sayfa 3: 7   sonraki: yok
toplam 7 kayit, 3 sayfa

islem oncesi  id=3 baglantilar: kendisi, uye, iade
islem sonrasi id=3 baglantilar: kendisi, uye
yeniden okuma id=3 iade=2026-03-15 baglantilar: kendisi, uye

istemcide kurulan adres sayisi: 1
--- tek kaydin ham gosterimi ---
{"id":4,"uye":"U-1002","isbn":"K-04","verilis":"2026-03-02","iade":null,"baglantilar":{"kendisi":{"yol":"/oduncler/4","yontem":"GET"},"uye":{"yol":"/uyeler/U-1002","yontem":"GET"},"iade":{"yol":"/oduncler/4","yontem":"PATCH","alanlar":["iade"]}}}
```

Üç sayfa, yedi kayıt, tek bir yazılı adres. İstemcinin kodunda `imlec` sözcüğü hiç geçmiyor;
imlecin nasıl kodlandığı, hangi parametre adıyla taşındığı, hatta sayfalamanın imleç tabanlı
mı atlama tabanlı mı olduğu istemciyi ilgilendirmiyor. Sunucu bu kararı değiştirdiğinde
istemci değişmez.

İkinci bölüm daha ilginçtir. İade işleminden **önce** kaydın bağlantıları arasında `iade`
vardı; işlemden sonra kayboldu. İstemci "bu kayıt iade edilebilir mi" sorusunu bir alanın
değerine bakıp kendi kuralıyla yanıtlamıyor; bağlantının varlığına bakıyor. Kural sunucuda
duruyor ve tek yerde duruyor.

Bunun arayüz tarafındaki karşılığı doğrudandır: iade düğmesinin görünürlüğü, `iade`
bağlantısının varlığına bağlanır. İş kuralı değiştiğinde — örneğin cezalı üyelerin iade
işlemini şubede yapması gerektiğinde — sunucu bağlantıyı üretmez ve düğme kendiliğinden
kaybolur.

#### Ham Gösterim ve Sözleşmenin Sınırı

Çıktının son satırı tek bir kaydın ham gösterimini veriyor. Bağlantılar gövdenin ayrı bir
alanında toplanmış; kaydın kendi alanlarıyla karışmıyor. Bu ayrım korunmalıdır, yoksa
`iade` adı hem bir tarih alanı hem de bir bağlantı adı olur.

Bağlantı odaklı tasarımın da bir maliyeti vardır ve abartılmamalıdır.

Gövde büyür. Yedi kayıtlık bir listede her kaydın üç bağlantı taşıması, gövdenin önemli bir
bölümünü bağlantılara ayırır. Kısmi yanıt dersindeki ölçüt burada da geçerlidir: bağlantı
bloğu alan seçimiyle kapatılabilmelidir.

İstemci bağımlılığı yok olmaz, yer değiştirir. İstemci artık adres biçimine değil,
**bağlantı adlarına** bağımlıdır. `sonraki` adının değişmesi, imleç parametresinin adının
değişmesi kadar kırıcıdır. Kazanç, bağlantı adlarının adres biçimlerinden daha az
değişmesindedir.

Bağlantı üretmek sunucuya iş çıkarır. Yukarıdaki koleksiyon uç noktası, sonraki sayfanın
var olup olmadığını anlamak için fazladan bir sayım sorgusu çalıştırıyor. Büyük
koleksiyonlarda bu sayım pahalıdır; yaygın çözüm, sayfa boyutundan bir fazla kayıt çekip
fazlalık gelirse sonraki sayfanın var olduğuna karar vermektir.

Son olarak, bağlantıların hepsi aynı düzeyde zorunlu değildir. Gezinme bağlantıları
olmadan istemci sayfalama biçimini bilmek zorunda kalır; işlem bağlantıları olmadan iş
kurallarını yeniden yazmak zorunda kalır. İlki neredeyse her serviste karşılığını verir;
ikincisi, iş kurallarının karmaşık ve değişken olduğu servislerde değerlidir.

#### Özet

- Bağlantı en az iki bilgi taşır: adres ve yöntem; işlem bağlantıları ayrıca gövdede
  beklenen alanları bildirir.
- Koleksiyon yanıtı gezinme bağlantıları, tekil kaynak yanıtı bağlı kaynak ve işlem
  bağlantıları taşır; bağlantılar gövdenin ayrı bir alanında toplanır.
- Ölçümde istemci tek bir adres yazarak üç sayfayı dolaştı ve yedi kaydı topladı; imleç
  parametresinin adı istemcinin kodunda hiç geçmedi.
- İşlem bağlantıları kaynağın o anki durumundan üretilir: iade edilmiş kayıtta `iade`
  bağlantısı yoktur, dolayısıyla iş kuralı tek yerde durur.
- Sonraki sayfa bağlantısı yalnızca kalan kayıt varken üretilir; istemcinin boş sayfayla
  durma kuralı yazmasına gerek kalmaz.
- Maliyeti gövdenin büyümesi, sunucunun bağlantı üretme yükü ve bağımlılığın adres
  biçiminden bağlantı adlarına kaymasıdır.

#### Sonraki Adım

Kaynak ve sözleşme tasarımı tamamlandı: kaynaklar belirlendi, adresleri yerleşti,
yöntemler ve durum kodları eşlendi, gövde biçimi kararlaştırıldı, koleksiyonlar sayfalandı
ve süzüldü, yazma istekleri yinelenen etkiye karşı korundu, yanıtlar kendi gezinme
bilgisini taşır oldu. Bütün bunlar işler yolunda gittiğinde geçerlidir. Hata durumunda ise
her uç nokta kendi gövdesini uyduruyor: bir yerde `hata` alanı, başka yerde `alan` ve
`neden`, bir başka yerde yalnız bir kod. Sonraki konu buradan başlar ve ilk dersi hata
yanıtlarının tek bir yapıya oturtulmasıdır: standart bir problem ayrıntısı biçimi, hangi
alanları taşır ve istemci onu nasıl kullanır.

## Hata, Sürüm ve Belge

Hata yanıtlarının tek biçime bağlanması, alan düzeyi doğrulama bildirimi, yol/başlık/içerik tabanlı sürümleme, kırıcı değişikliklerin şema farkından saptanması, kullanımdan kaldırma penceresinin ölçülmesi ve sözleşmenin makine okunur belge, sözleşme testi ve sahte sunucuyla sınanması.

### Hata Yanıt Biçimi

Source: https://academia.sh/tr/kurslar/api-tasarimi/hata-yanit-bicimi

Hata gövdesinin uç nokta başına uydurulmasının istemciye maliyeti, standart problem ayrıntısı biçiminin beş çekirdek alanı ve gövdeyi tek katmandan üreten sorun kataloğunun ölçülerek doğrulanması.

Bağlantı odaklı yanıtlar dersi, başarılı yanıtın içine gezinme bilgisini koydu: istemci bir
ödünç kaydını aldığında, o kayıttan iade işlemine ve üyeye nasıl gidileceğini yanıtın
kendisinden okuyabiliyordu. Kaynak modeli, adres düzeni, durum kodu seçimi, gövde
adlandırması ve bağlantılar — buraya kadar tasarlanan her şey **isteğin başarıyla
sonuçlandığı** durumu anlatıyordu.

Başarısız istekler bu tasarımın dışında kaldı. Ödünç servisinin gerçek trafiğinde kitabı
bulunamayan, kopyası kalmayan, sınırı aşan, gövdesi bozuk istekler vardır. Bu ders tek bir
soruyu yanıtlar: hata yanıtının gövdesi nasıl olmalıdır ki istemci onu uç nokta başına
yeniden öğrenmek zorunda kalmasın?

#### İki Hata Biçemi, Tek Servis

Aşağıdaki sunucu ödünç servisinin bilinen çekirdeğini sunar — kitap sorgulama, ödünç verme,
iade — ve hata gövdesini iki ayrı biçemde üretebilir. Yönlendirme kodu ikisinde de aynıdır;
değişen yalnızca `hata()` çağrısının ürettiği gövdedir. `DAGINIK` tablosu, hata gövdesi
sözleşmeye yazılmadığında bir kod tabanında zamanla oluşan hâldir: her durum, o satırı
yazan kişinin aklına gelen adlarla bildirilir. `PROBLEM` tablosu ise her durumu bir sorun
türü kataloğuna eşler.

```js
// sorun.mjs — problem ayrintisi katmani: govdeyi ureten tek yer
export const TABAN_TIP = "https://ornek.kutuphane/sorunlar/";

// Katalog: her sorun turu bir kez tanimlanir. Tur -> kalici kimlik, baslik, durum kodu.
export const KATALOG = {
  kaynak_yok:  { baslik: "Kaynak bulunamadı",           durum: 404 },
  govde_bozuk: { baslik: "İstek gövdesi çözülemedi",    durum: 400 },
  kopya_yok:   { baslik: "Ödünç verilebilir kopya yok", durum: 409 },
  uye_siniri:  { baslik: "Üye ödünç sınırına ulaştı",   durum: 409 },
  dogrulama:   { baslik: "İstek gövdesi doğrulanamadı", durum: 422 },
};

let sayac = 0;
const olayKimligi = () => `ol-${String(++sayac).padStart(4, "0")}`;

// sorun(kod, ayrinti, ek) -> { govde, durum }
export function sorun(kod, ayrinti, ek = {}) {
  const kayit = KATALOG[kod];
  if (!kayit) throw new Error(`katalogda olmayan sorun türü: ${kod}`);
  return {
    durum: kayit.durum,
    govde: {
      type: TABAN_TIP + kod.replaceAll("_", "-"),
      title: kayit.baslik,
      status: kayit.durum,
      detail: ayrinti,
      instance: olayKimligi(),
      ...ek,
    },
  };
}
```

```js
// sunucu.mjs — odunc servisi; ayni yollar, iki ayri hata bicemi
// Kullanim: node sunucu.mjs dagink   (uc nokta basina uydurulmus govde)
//           node sunucu.mjs problem  (problem ayrintisi bicimi)
import { createServer } from "node:http";
import { sorun } from "./sorun.mjs";

const BICEM = process.argv[2] ?? "dagink";

// Dagink bicem: her hata durumu icin o gun yazilmis govde.
const DAGINIK = {
  kitap_yok:   (b) => [404, { error: "not_found", resource: "kitap", id: b.id }],
  odunc_yok:   (b) => [404, { message: `odunc bulunamadi: ${b.id}` }],
  uye_yok:     ()  => [404, { hata: { kod: 4041, aciklama: "uye yok" } }],
  govde_bozuk: ()  => [400, { ok: false, reason: "gecersiz JSON" }],
  kopya_yok:   ()  => [409, { conflict: "kopya_yok", kalan: 0 }],
  uye_siniri:  (b) => [409, { hataMesaji: "uye siniri asildi", limit: b.sinir }],
  yol_yok:     (b) => [404, { status: 404, path: b.yol }],
};

// Problem bicemi: her durum, katalogdaki bir sorun turune eslenir.
const PROBLEM = {
  kitap_yok:   (b) => sorun("kaynak_yok", `ISBN ${b.id} katalogda yok.`, { kaynak: "kitap" }),
  odunc_yok:   (b) => sorun("kaynak_yok", `${b.id} numaralı ödünç kaydı yok.`, { kaynak: "odunc" }),
  uye_yok:     (b) => sorun("kaynak_yok", `${b.id} numaralı üye yok.`, { kaynak: "uye" }),
  govde_bozuk: ()  => sorun("govde_bozuk", "Gövde geçerli JSON değil.", { kaynak: "govde" }),
  kopya_yok:   (b) => sorun("kopya_yok", `${b.ad} kitabının kopyaları ödünçte.`, { isbn: b.id, kalan: 0 }),
  uye_siniri:  (b) => sorun("uye_siniri", `Üye ${b.acik} kitap tutuyor, sınır ${b.sinir}.`, { sinir: b.sinir, acik: b.acik }),
  yol_yok:     (b) => sorun("kaynak_yok", `${b.yol} yolu tanımlı değil.`, { kaynak: "yol" }),
};

const hata = (yanit, kod, baglam = {}) => {
  if (BICEM === "problem") {
    const { durum, govde } = PROBLEM[kod](baglam);
    yanit.writeHead(durum, { "content-type": "application/problem+json; charset=utf-8" });
    return yanit.end(JSON.stringify(govde));
  }
  const [durum, govde] = DAGINIK[kod](baglam);
  yanit.writeHead(durum, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(govde));
};

const KITAPLAR = new Map([
  ["978-0201896831", { ad: "Programlama Sanati", kopya: 2 }],
  ["978-0262033848", { ad: "Algoritmalara Giris", kopya: 1 }],
]);
const UYELER = new Set(["U-1001", "U-1002"]);
const UYE_SINIRI = 2;
const oduncler = [{ id: "O-1", uye: "U-1001", isbn: "978-0262033848" }];

const govdeOku = (istek) =>
  new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); });

const json = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

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

  if (istek.method === "GET" && yol.startsWith("/kitaplar/")) {
    const isbn = yol.slice("/kitaplar/".length);
    const kitap = KITAPLAR.get(isbn);
    if (!kitap) return hata(yanit, "kitap_yok", { id: isbn });
    return json(yanit, 200, { isbn, ...kitap });
  }

  if (istek.method === "GET" && yol.startsWith("/odunc/")) {
    const id = yol.slice("/odunc/".length);
    const kayit = oduncler.find((o) => o.id === id);
    if (!kayit) return hata(yanit, "odunc_yok", { id });
    return json(yanit, 200, kayit);
  }

  if (istek.method === "POST" && yol === "/odunc") {
    let govde;
    try { govde = JSON.parse((await govdeOku(istek)) || "{}"); }
    catch { return hata(yanit, "govde_bozuk"); }

    if (!UYELER.has(govde.uye)) return hata(yanit, "uye_yok", { id: govde.uye });
    const kitap = KITAPLAR.get(govde.isbn);
    if (!kitap) return hata(yanit, "kitap_yok", { id: govde.isbn });

    const elde = oduncler.filter((o) => o.isbn === govde.isbn).length;
    if (elde >= kitap.kopya) return hata(yanit, "kopya_yok", { id: govde.isbn, ad: kitap.ad });

    const acik = oduncler.filter((o) => o.uye === govde.uye).length;
    if (acik >= UYE_SINIRI) return hata(yanit, "uye_siniri", { sinir: UYE_SINIRI, acik });

    const id = `O-${oduncler.length + 1}`;
    oduncler.push({ id, uye: govde.uye, isbn: govde.isbn });
    return json(yanit, 201, { id, uye: govde.uye, isbn: govde.isbn });
  }

  if (istek.method === "POST" && yol === "/iade") {
    const govde = JSON.parse((await govdeOku(istek)) || "{}");
    const sira = oduncler.findIndex((o) => o.id === govde.oduncId);
    if (sira < 0) return hata(yanit, "odunc_yok", { id: govde.oduncId });
    oduncler.splice(sira, 1);
    return json(yanit, 200, { durum: "iade edildi" });
  }

  hata(yanit, "yol_yok", { yol });
}).listen(8431, "127.0.0.1", () => console.log(`sunucu 127.0.0.1:8431 biçem=${BICEM}`));
```

#### Problem Ayrıntısı Biçimi

`PROBLEM` tablosunun ürettiği yapı uydurulmuş değildir: hata gövdesi için standartlaşmış
bir biçim vardır. **Problem ayrıntısı** (problem details) RFC 9457 ile tanımlanır,
`application/problem+json` içerik tipiyle taşınır ve beş çekirdek alanı bulunur.

- **`type`** — sorun türünün kalıcı kimliği, bir URI. İstemcinin dallandığı alan budur.
  Metin değil kimliktir; başlık değişse bile aynı kalır.
- **`title`** — sorun türünün insan okur kısa adı. Yerelleştirme dışında aynı `type` için
  hep aynı kalır.
- **`status`** — HTTP durum kodunun gövdedeki kopyası.
- **`detail`** — bu **örneğe** özgü açıklama: hangi ISBN, hangi üye, hangi sınır.
- **`instance`** — bu tek olayın kimliği; kütükteki kaydı bulmayı sağlar.

`type` ile `detail` arasındaki ayrım biçimin çekirdeğidir. `type` **sınıfı** adlandırır,
`detail` **örneği** anlatır. İstemci `type` alanına bakarak karar verir ("kopya yoksa
rezervasyon düğmesini göster"), `detail` alanını yalnızca kullanıcıya gösterir ya da kütüğe
yazar. Bu ayrım korunmazsa istemci metin karşılaştırmasına mecbur kalır ve sunucudaki bir
yazım düzeltmesi istemci mantığını bozar.

`status` alanının gövdede yinelenmesi ilk bakışta gereksiz görünür — durum satırında zaten
vardır. İki durumda gerekli olur: yanıt bir kütüğe ya da kuyruğa gövde olarak alındığında
durum satırı kaybolur; bir ara katman durum kodunu değiştirdiğinde gövdedeki değerle durum
satırı arasındaki uyuşmazlık bu değişikliği görünür kılar.

Standart, çekirdeğin dışına **ek üye** (extension member) koymaya izin verir. Kalan kopya
sayısı, sınır değeri, hangi kaynağın aranıp bulunamadığı — bunlar ek üyedir ve sorun
türüne bağlıdır: aynı `type` her zaman aynı ek üyelerle gelir.

#### Ölçüm

Aşağıdaki betik sunucudaki sekiz hata durumunu sırayla üretir, gövdeleri toplar ve üç şey
sayar: kaç ayrı içerik tipi döndüğünü, kaç ayrı anahtar kümesi çıktığını ve çekirdek beş
alanın kaç ayrı bileşimde göründüğünü.

```js
// denetle.mjs — bir sunucunun hata yanitlarindaki bicim tutarliligini olcer
// Kullanim: node denetle.mjs <taban-adres>
const TABAN = process.argv[2];
const CEKIRDEK = ["type", "title", "status", "detail", "instance"];

// Her satir bir hata durumu uretir: [ad, yontem, yol, govde]
const DURUMLAR = [
  ["kitap yok (GET)", "GET", "/kitaplar/978-0000000000", null],
  ["odunc yok (GET)", "GET", "/odunc/O-99", null],
  ["bozuk govde", "POST", "/odunc", "{bu json degil"],
  ["uye yok", "POST", "/odunc", { uye: "U-9999", isbn: "978-0201896831" }],
  ["kitap yok (POST)", "POST", "/odunc", { uye: "U-1001", isbn: "978-0000000000" }],
  ["kopya yok", "POST", "/odunc", { uye: "U-1002", isbn: "978-0262033848" }],
  ["odunc yok (iade)", "POST", "/iade", { oduncId: "O-99" }],
  ["yol yok", "GET", "/rafler", null],
];

const anahtarlar = (n, onek = "") =>
  Object.entries(n).flatMap(([a, d]) =>
    d && typeof d === "object" && !Array.isArray(d) ? anahtarlar(d, `${onek}${a}.`) : [`${onek}${a}`]);

const cekirdekBicimleri = new Set();
const tamBicimler = new Set();
const tipler = new Set();
console.log("durum                içerik tipi              çekirdek       ek üyeler");
for (const [ad, yontem, yol, govde] of DURUMLAR) {
  const cevap = await fetch(TABAN + yol, {
    method: yontem,
    headers: govde == null ? {} : { "content-type": "application/json" },
    body: govde == null ? undefined : typeof govde === "string" ? govde : JSON.stringify(govde),
  });
  const metin = await cevap.text();
  const tip = (cevap.headers.get("content-type") ?? "").split(";")[0];
  tipler.add(tip);
  let liste;
  try { liste = anahtarlar(JSON.parse(metin)); } catch { liste = ["<ayrıştırılamadı>"]; }
  const cekirdek = CEKIRDEK.filter((a) => liste.includes(a));
  const ek = liste.filter((a) => !CEKIRDEK.includes(a)).sort();
  cekirdekBicimleri.add(cekirdek.join(","));
  tamBicimler.add([...cekirdek, ...ek].join(","));
  const durumu = cekirdek.length === CEKIRDEK.length ? "tam" : `eksik(${cekirdek.length}/5)`;
  console.log(`${ad.padEnd(20)} ${tip.padEnd(24)} ${durumu.padEnd(14)} ${ek.join(",") || "-"}`);
}

console.log(`\nsınanan hata durumu:     ${DURUMLAR.length}`);
console.log(`ayrı içerik tipi:        ${tipler.size}  (${[...tipler].join(", ")})`);
console.log(`ayrı tam anahtar kümesi: ${tamBicimler.size}`);
console.log(`ayrı çekirdek biçimi:    ${cekirdekBicimleri.size}`);
```

```bash
#!/usr/bin/env bash
# sunucu.mjs'yi iki bicemle sirayla baslatir ve ayni denetimi calistirir.
for bicem in dagink problem; do
  node sunucu.mjs "$bicem" & s=$!
  sleep 0.5
  node denetle.mjs http://127.0.0.1:8431
  kill "$s"; wait "$s" 2>/dev/null
  echo
done
```

```
sunucu 127.0.0.1:8431 biçem=dagink
durum                içerik tipi              çekirdek       ek üyeler
kitap yok (GET)      application/json         eksik(0/5)     error,id,resource
odunc yok (GET)      application/json         eksik(0/5)     message
bozuk govde          application/json         eksik(0/5)     ok,reason
uye yok              application/json         eksik(0/5)     hata.aciklama,hata.kod
kitap yok (POST)     application/json         eksik(0/5)     error,id,resource
kopya yok            application/json         eksik(0/5)     conflict,kalan
odunc yok (iade)     application/json         eksik(0/5)     message
yol yok              application/json         eksik(1/5)     path

sınanan hata durumu:     8
ayrı içerik tipi:        1  (application/json)
ayrı tam anahtar kümesi: 6
ayrı çekirdek biçimi:    2

sunucu 127.0.0.1:8431 biçem=problem
durum                içerik tipi              çekirdek       ek üyeler
kitap yok (GET)      application/problem+json tam            kaynak
odunc yok (GET)      application/problem+json tam            kaynak
bozuk govde          application/problem+json tam            kaynak
uye yok              application/problem+json tam            kaynak
kitap yok (POST)     application/problem+json tam            kaynak
kopya yok            application/problem+json tam            isbn,kalan
odunc yok (iade)     application/problem+json tam            kaynak
yol yok              application/problem+json tam            kaynak

sınanan hata durumu:     8
ayrı içerik tipi:        1  (application/problem+json)
ayrı tam anahtar kümesi: 2
ayrı çekirdek biçimi:    1
```

Dağınık biçemde sekiz hata durumu altı ayrı anahtar kümesi üretiyor ve hiçbirinde ortak bir
çekirdek yok. `error`, `message`, `reason`, `hataMesaji`, `hata.aciklama` — hepsi aynı
şeyi, "ne oldu" bilgisini taşır, her biri başka addadır. Uygulama Mimarisi kursunda kurulan
istek katmanı yanıtı bir **hata sözleşmesine** (error contract) eşlemek zorundadır ve bu
tabloyla eşleme yazılamaz. İstemci ya altı ayrı ayrıştırıcı yazar ya da tek bir alana
bakıp diğerlerini görmezden gelir; ikinci seçenek yaygındır ve kullanıcının ekranında "bir
hata oluştu" cümlesine dönüşür.

Problem biçeminde ayrı çekirdek biçim sayısı bire iner ve her satırda beş alanın tamamı
bulunur. Tam anahtar kümesi ikidir, çünkü ek üyeler farklıdır ve farklı olmaları gerekir:
`kopya_yok` sorununun `kalan` alanı vardır, `kaynak_yok` sorununun yoktur. Ama bu fark
**sorun türüyle** bağlıdır, uç noktayla değil. İstemci `type` alanını okuduğunda hangi ek
üyelerin geleceğini bilir; dağınık biçemde hangi uç noktaya gittiğini bilmek zorundaydı.

İçerik tipi satırı ikinci bir kazanç gösterir. Dağınık biçemde hata yanıtı ile başarı
yanıtı aynı içerik tipini taşır; aradaki ara katmanlar ve kütükleme araçları gövdeyi
ayrıştırmadan bunun bir hata olup olmadığını anlayamaz. `application/problem+json` bu ayrımı
gövdeye bakmadan verir.

#### Aynı Sorunun Farklı Uç Noktalardan Dönüşü

Tabloda `kaynak_yok` sorunu altı satırda görünüyor. İstemcinin buna güvenebilmesi için o
satırların `type`, `title` ve `status` alanları birebir aynı olmalıdır. Aşağıdaki betik iki
farklı uç noktadan aynı sorunu üretir ve ardından kataloğa yazılmamış bir tür ister.

```bash
#!/usr/bin/env bash
# Ayni sorun turunu iki ayri uc noktadan uretir; katalog disi turu dener.
node sunucu.mjs problem & sunucu=$!
sleep 0.5

for yol in /kitaplar/978-0000000000 /odunc/O-99; do
  echo "--- $yol ---"
  curl -sS -D - -o /tmp/govde "http://127.0.0.1:8431$yol" | grep -i '^HTTP\|^content-type'
  cat /tmp/govde; echo
done

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

echo "--- katalog disi bir sorun turu istenirse ---"
node -e 'import("./sorun.mjs").then(({ sorun }) => {
  try { sorun("rafta_toz_var", "Uydurulmus bir hata."); }
  catch (h) { console.log(h.message); }
});'
```

```
sunucu 127.0.0.1:8431 biçem=problem
--- /kitaplar/978-0000000000 ---
HTTP/1.1 404 Not Found
content-type: application/problem+json; charset=utf-8
{"type":"https://ornek.kutuphane/sorunlar/kaynak-yok","title":"Kaynak bulunamadı","status":404,"detail":"ISBN 978-0000000000 katalogda yok.","instance":"ol-0001","kaynak":"kitap"}
--- /odunc/O-99 ---
HTTP/1.1 404 Not Found
content-type: application/problem+json; charset=utf-8
{"type":"https://ornek.kutuphane/sorunlar/kaynak-yok","title":"Kaynak bulunamadı","status":404,"detail":"O-99 numaralı ödünç kaydı yok.","instance":"ol-0002","kaynak":"odunc"}
--- katalog disi bir sorun turu istenirse ---
katalogda olmayan sorun türü: rafta_toz_var
```

İlk üç alan birebir aynı, `detail` ve `instance` farklı. İstemcinin durum kodu eşlemesi
`type` alanına bakar ve iki uç nokta için tek dal yazar; kullanıcıya gösterilecek metin
`detail` alanından gelir; destek kaydına `instance` yazılır.

Son satır kataloğun ikinci işlevini gösteriyor. Katalog yalnızca `type` ile `title` ve
`status` eşlemesini tutmaz; kendisinde olmayan bir tür istendiğinde hata verir. Kataloğa
yazılmamış bir sorun sessizce yeni bir biçim üretemez. Biçimin zamanla yeniden dağılmasını
engelleyen şey, biçimin doğru tasarlanmış olması değil, yanlış kullanımın çalışma anında
görünür olmasıdır.

#### Gövdeye Neyin Konmayacağı

Hata gövdesinin tek yerden üretilmesi, ne konacağı kadar ne konmayacağını da denetlenebilir
kılar. Yığıt izleri, veritabanı sorgu metinleri, dosya yolları ve iç kimlikler istemciye
gitmez; bunlar `instance` alanıyla eşleştirilerek sunucu kütüğünde tutulur. İstemci bir
sorun bildirdiğinde olay kimliğini verir, tam bağlam kütükten bulunur.

Ölçüt, kimin ne yapabileceğine dayanır: istemcinin **davranış değiştirebileceği** bilgi
gövdeye girer (kopya kalmadı, sınır iki, ISBN yok); istemcinin hiçbir şey yapamayacağı
bilgi kütüğe girer. Bu ölçüt hem güvenlik hem kullanışlılık gerekçesiyle aynı sonucu verir.

#### Özet

- Hata gövdesi uç nokta başına yazıldığında sekiz hata durumu altı ayrı anahtar kümesi
  üretir ve hiçbirinde ortak çekirdek bulunmaz; istemci bu tabloyla tek bir hata sözleşmesi
  kuramaz.
- Problem ayrıntısı biçiminin beş çekirdek alanı ayrı işlere hizmet eder: `type` sınıfı
  adlandırır, `title` sınıfın sabit adıdır, `status` durum kodunu gövdede yineler, `detail`
  örneği anlatır, `instance` tek olayı kütükle eşler.
- İstemci `type` alanına göre dallanır; `detail` metnine göre dallanmak, sunucudaki bir
  yazım düzeltmesini kırıcı değişiklik hâline getirir.
- Gövdenin sorun türü kataloğuna dayanan tek katmandan üretilmesi ayrı çekirdek biçim
  sayısını bire indirir; ek üyeler uç noktaya değil sorun türüne bağlı olduğu için farklı
  kalabilir.
- Katalog, kendisinde olmayan bir tür istendiğinde hata vererek biçimin zamanla yeniden
  dağılmasını çalışma anında görünür kılar.
- İstemcinin davranışını değiştirebileceği bilgi gövdeye, değiştiremeyeceği bilgi
  `instance` ile eşlenmiş kütük kaydına yazılır.

#### Sonraki Adım

Katalogdaki `dogrulama` türü bu derste hiç kullanılmadı. Nedeni, doğrulama hatasının tek
bir cümleye sığmamasıdır: gövdesinde dört alanı birden yanlış olan bir ödünç isteği "istek
gövdesi doğrulanamadı" başlığıyla yeterince anlatılamaz. İstemcinin her yanlış alanı kendi
giriş kutusunun yanında göstermesi gerekir, bunun için de sunucunun hangi alanın neden
reddedildiğini alan alan bildirmesi şarttır. Sonraki ders problem ayrıntısına alan düzeyinde
hata listesi ekler, alan adlarının istemcideki alanlarla hangi kurala göre eşleşeceğini
belirler ve eşleşmenin bozulduğu durumu ölçer.

### Doğrulama Hataları

Source: https://academia.sh/tr/kurslar/api-tasarimi/dogrulama-hatalari

Alan düzeyinde hata listesinin problem ayrıntısına eklenmesi, ilk hatada durmakla hepsini toplamanın istek turu farkı ve alan adlarının istemcideki giriş kutularıyla eşleşmesini sağlayan gövde yolu kuralı.

Bir önceki ders hata gövdesini tek biçime bağladı ama sorun kataloğundaki `dogrulama`
türünü kullanmadan bıraktı. Nedeni, bu türün diğerlerinden yapısal olarak farklı olmasıdır.
"Kopya yok" tek bir olgudur; `detail` alanındaki bir cümle onu tümüyle anlatır. "İstek
gövdesi doğrulanamadı" ise bir olgu değil, bir **olgular kümesidir**: gövdenin hangi
alanları, hangi kurallara göre reddedildi?

Ödünç servisinin ödünç verme isteği artık tek kitaplık değildir; bir üye, birden çok kalem
ve bir iade tarihi taşır. Bu ders alan düzeyinde hata bildirimini kurar ve iki kararı
ölçerek verir: hataların kaçının bir seferde bildirileceği ve alanların hangi adla
anılacağı.

#### Bir Seferde Kaç Hata

Doğrulama, ilk kuralı çiğneyen alanda durabilir ya da bütün gövdeyi tarayıp hataları
toplayabilir. Aradaki fark, kullanıcının formu düzeltmek için kaç kez istek yollayacağıdır.

Aşağıdaki doğrulayıcı şemayı veri olarak alır. Şema anahtarları **gövde yolu**dur ve JSON
Pointer biçiminde yazılır: `/uye`, `/kalemler/1/isbn`. `*` işareti dizi öğeleri üzerinde
açılır, yani `/kalemler/*/isbn` kalıbı gövdedeki her kaleme uygulanır.

```js
// dogrula.mjs — govdeyi semaya gore dogrular, alan duzeyinde hata listesi uretir
// Alan adi JSON Pointer (RFC 6901): "/uye", "/kalemler/1/isbn"

export const KURALLAR = {
  zorunlu: (d) => (d === undefined || d === null || d === "" ? { kod: "zorunlu" } : null),
  desen: (d, p) => (new RegExp(p).test(String(d)) ? null : { kod: "bicim", param: p }),
  secenek: (d, p) => (p.includes(d) ? null : { kod: "secenek_disi", param: p }),
  dizi: (d, p) =>
    !Array.isArray(d) ? { kod: "dizi_degil" }
      : d.length < p.enAz ? { kod: "cok_az", param: p }
      : d.length > p.enCok ? { kod: "cok_cok", param: p }
      : null,
  tarih: (d) => (/^\d{4}-\d{2}-\d{2}$/.test(String(d)) ? null : { kod: "tarih_degil" }),
  enCokGun: (d, p, bugun) => {
    const fark = Math.round((Date.parse(d) - Date.parse(bugun)) / 86400000);
    if (Number.isNaN(fark)) return null;                 // tarih kurali zaten bildirdi
    if (fark < 0) return { kod: "gecmiste" };
    return fark > p ? { kod: "cok_ileri", param: p } : null;
  },
};

// Yol icindeki * gercek dizi dizinleriyle acilir: /kalemler/*/isbn -> /kalemler/0/isbn
const oku = (govde, yol) =>
  yol.split("/").slice(1).reduce((d, p) => (d == null ? undefined : d[p]), govde);

function yollariAc(govde, kalip) {
  const yildiz = kalip.indexOf("/*");
  if (yildiz < 0) return [kalip];
  const dizi = oku(govde, kalip.slice(0, yildiz));
  if (!Array.isArray(dizi)) return [];
  return dizi.flatMap((_, i) => yollariAc(govde, kalip.slice(0, yildiz) + `/${i}` + kalip.slice(yildiz + 2)));
}

// dogrula(sema, govde, bugun) -> [{ yol, kod, param }]
export function dogrula(sema, govde, bugun) {
  const hatalar = [];
  for (const [kalip, kurallar] of Object.entries(sema)) {
    for (const yol of yollariAc(govde, kalip)) {
      const deger = oku(govde, yol);
      for (const [ad, param] of kurallar) {
        if (ad !== "zorunlu" && (deger === undefined || deger === null || deger === "")) break;
        const sonuc = KURALLAR[ad](deger, param, bugun);
        if (sonuc) { hatalar.push({ yol, ...sonuc }); break; }  // alan basina tek hata
      }
    }
  }
  return hatalar;
}
```

İki toplama biçimini karşılaştırmak için, dört alanı birden yanlış olan bir gövde alınır ve
kullanıcının yalnızca **kendisine bildirilen** hataları düzelttiği varsayılır.

```js
// tur-tur.mjs — ilk hatada duran dogrulama ile hepsini toplayan dogrulamayi karsilastirir
import { dogrula } from "./dogrula.mjs";

const BUGUN = "2026-03-01";
const SEMA = {
  "/uye":             [["zorunlu"], ["desen", "^U-\\d{4}$"]],
  "/kalemler":        [["zorunlu"], ["dizi", { enAz: 1, enCok: 3 }]],
  "/kalemler/*/isbn": [["zorunlu"], ["desen", "^97[89]-\\d{10}$"]],
  "/kalemler/*/sube": [["zorunlu"], ["secenek", ["merkez", "sahil", "tepe"]]],
  "/iadeTarihi":      [["zorunlu"], ["tarih"], ["enCokGun", 30]],
};

// Kullanicinin ilk gonderdigi govde: dort alan birden yanlis.
const ILK = {
  uye: "1001",
  kalemler: [
    { isbn: "978-0262033848", sube: "merkez" },
    { isbn: "0262033848", sube: "deniz" },
  ],
  iadeTarihi: "2026-06-01",
};

// Kullanicinin duzeltme sirasi: hangi hata bildirilirse o duzeltilir.
const DUZELTME = {
  "/uye": (g) => (g.uye = "U-1001"),
  "/kalemler/1/isbn": (g) => (g.kalemler[1].isbn = "978-0201896831"),
  "/kalemler/1/sube": (g) => (g.kalemler[1].sube = "sahil"),
  "/iadeTarihi": (g) => (g.iadeTarihi = "2026-03-20"),
};

function turSayisi(hepsiniTopla) {
  const govde = structuredClone(ILK);
  let tur = 0;
  while (tur < 10) {
    tur++;
    const hatalar = dogrula(SEMA, govde, BUGUN);
    if (hatalar.length === 0) return tur;
    const bildirilen = hepsiniTopla ? hatalar : [hatalar[0]];
    console.log(`  tur ${tur}: ${bildirilen.map((h) => `${h.yol}=${h.kod}`).join("  ")}`);
    for (const h of bildirilen) DUZELTME[h.yol](govde);
  }
  return tur;
}

console.log("ilk hatada duran doğrulama:");
const a = turSayisi(false);
console.log(`  başarılı olana kadar istek turu: ${a}\n`);

console.log("hepsini toplayan doğrulama:");
const b = turSayisi(true);
console.log(`  başarılı olana kadar istek turu: ${b}`);
```

```
ilk hatada duran doğrulama:
  tur 1: /uye=bicim
  tur 2: /kalemler/1/isbn=bicim
  tur 3: /kalemler/1/sube=secenek_disi
  tur 4: /iadeTarihi=cok_ileri
  başarılı olana kadar istek turu: 5

hepsini toplayan doğrulama:
  tur 1: /uye=bicim  /kalemler/1/isbn=bicim  /kalemler/1/sube=secenek_disi  /iadeTarihi=cok_ileri
  başarılı olana kadar istek turu: 2
```

Beş tura karşı iki tur. Sayının kendisi gövdedeki yanlış alan sayısına bağlıdır — $n$
yanlış alan, ilk hatada duran doğrulamada $n+1$ tur demektir — ama davranış farkı sayıdan
bağımsızdır: ilk hatada durmak, kullanıcıya bir sonraki hatanın varlığını **saklar**.
Kullanıcı her düzeltmeden sonra işin bittiğini sanır ve yeni bir ret alır.

Bir ayrıntı korunmuştur: doğrulayıcı **alan başına** ilk hatada durur. `/uye` alanı hem
zorunlu hem desene uygun olmalıdır; boş bırakıldığında "zorunlu" ve "biçim" hatalarının
ikisini birden bildirmenin bir yararı yoktur, ikincisi birincinin sonucudur. Kural şudur:
alanlar arasında tarama sürer, alan içinde ilk hata yeter.

#### Hata Kaydının Üç Alanı

Her hata kaydı üç şey taşır. **`yol`**, gövdedeki alanın adresidir. **`kod`**, hatanın
kararlı kimliğidir — Frontend müfredatındaki Uygulama Mimarisi kursunda kurulan doğrulama
şemalarıyla aynı ayrım: kod dallanmak içindir, ileti göstermek için. **`param`**, kodun
gerektirdiği sayıdır: hangi desen, hangi seçenekler, kaç gün.

Kodun ileti yerine geçmesi, kullanıcıya gösterilecek metni **istemci tarafında** üretmeyi
mümkün kılar. Sunucu `cok_ileri` ve `30` gönderir; istemci bunu kendi dilinde ve kendi
tonunda cümleye çevirir. Sunucu metin gönderseydi çeviri, çoğul kuralı ve ton kararı
sunucuya taşınırdı; hiçbiri sunucunun işi değildir.

Bu üçlü, problem ayrıntısına bir **ek üye** olarak eklenir. Çekirdek beş alan yerinde
kalır; `errors` dizisi onların yanına gelir.

```js
// sunucu.mjs — odunc istegini dogrulayip 422 problem ayrintisi donduren servis
import { createServer } from "node:http";
import { dogrula } from "./dogrula.mjs";

const BUGUN = "2026-03-01";                 // ornegin yinelenebilir olmasi icin sabit
const SEMA = {
  "/uye":             [["zorunlu"], ["desen", "^U-\\d{4}$"]],
  "/kalemler":        [["zorunlu"], ["dizi", { enAz: 1, enCok: 3 }]],
  "/kalemler/*/isbn": [["zorunlu"], ["desen", "^97[89]-\\d{10}$"]],
  "/kalemler/*/sube": [["zorunlu"], ["secenek", ["merkez", "sahil", "tepe"]]],
  "/iadeTarihi":      [["zorunlu"], ["tarih"], ["enCokGun", 30]],
};

// Bir onceki derste yazilan sorun katalogunun bu derse dusen tek girdisi.
let sayac = 0;
const dogrulamaSorunu = (hatalar) => ({
  type: "https://ornek.kutuphane/sorunlar/dogrulama",
  title: "İstek gövdesi doğrulanamadı",
  status: 422,
  detail: `${hatalar.length} alan kabul edilmedi.`,
  instance: `ol-${String(++sayac).padStart(4, "0")}`,
  errors: hatalar,
});

const govdeOku = (istek) =>
  new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); });

// Cekirdek alanlar alt alta, her hata tek satir: govde okunabilir kalsin.
const bicimle = (s) => [
  "{",
  ...["type", "title", "status", "detail", "instance"].map((a) => `  "${a}": ${JSON.stringify(s[a])},`),
  '  "errors": [',
  ...s.errors.map((h, i) => `    ${JSON.stringify(h)}${i < s.errors.length - 1 ? "," : ""}`),
  "  ]",
  "}",
].join("\n");

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const govde = JSON.parse((await govdeOku(istek)) || "{}");
  const hatalar = dogrula(SEMA, govde, BUGUN);

  if (hatalar.length) {
    yanit.writeHead(422, { "content-type": "application/problem+json; charset=utf-8" });
    return yanit.end(bicimle(dogrulamaSorunu(hatalar)));
  }
  yanit.writeHead(201, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify({ id: "O-2", uye: govde.uye, kalem: govde.kalemler.length }));
}).listen(8433, "127.0.0.1", () => console.log("dogrulama sunucusu 127.0.0.1:8433"));
```

```bash
#!/usr/bin/env bash
# Dort alani yanlis bir odunc istegi yollar, 422 govdesini gosterir.
node sunucu.mjs & sunucu=$!
sleep 0.5

curl -sS -w '\n[%{http_code}] %{content_type}\n' -X POST -H 'content-type: application/json' \
  -d '{"uye":"1001","kalemler":[{"isbn":"978-0262033848","sube":"merkez"},{"isbn":"0262033848","sube":"deniz"}],"iadeTarihi":"2026-06-01"}' \
  http://127.0.0.1:8433/odunc

echo "--- duzeltilmis govde ---"
curl -sS -w '\n[%{http_code}] %{content_type}\n' -X POST -H 'content-type: application/json' \
  -d '{"uye":"U-1001","kalemler":[{"isbn":"978-0262033848","sube":"merkez"},{"isbn":"978-0201896831","sube":"sahil"}],"iadeTarihi":"2026-03-20"}' \
  http://127.0.0.1:8433/odunc

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

```
dogrulama sunucusu 127.0.0.1:8433
{
  "type": "https://ornek.kutuphane/sorunlar/dogrulama",
  "title": "İstek gövdesi doğrulanamadı",
  "status": 422,
  "detail": "4 alan kabul edilmedi.",
  "instance": "ol-0001",
  "errors": [
    {"yol":"/uye","kod":"bicim","param":"^U-\\d{4}$"},
    {"yol":"/kalemler/1/isbn","kod":"bicim","param":"^97[89]-\\d{10}$"},
    {"yol":"/kalemler/1/sube","kod":"secenek_disi","param":["merkez","sahil","tepe"]},
    {"yol":"/iadeTarihi","kod":"cok_ileri","param":30}
  ]
}
[422] application/problem+json; charset=utf-8
--- duzeltilmis govde ---
{"id":"O-2","uye":"U-1001","kalem":2}
[201] application/json; charset=utf-8
```

Durum kodunun 400 değil **422** olması bilinçli bir seçimdir. 400, isteğin sunucu
tarafından **anlaşılamadığını** bildirir; bozuk JSON, eksik başlık, tanınmayan içerik tipi.
Buradaki gövde eksiksiz anlaşılmıştır — ayrıştırılmış, alanları okunmuş, kurallara
sokulmuştur — ve **anlamı** reddedilmiştir. Ayrım kütüğe yansıdığında da işe yarar: 400
oranındaki artış istemcinin isteği yanlış kurduğunu, 422 oranındaki artış kullanıcının
yanlış veri girdiğini gösterir; ikisinin nedeni ve çözümü farklıdır.

#### Alan Adının İstemcideki Karşılığı

Hata listesinin işe yaraması, istemcinin her hatayı **hangi giriş kutusunun yanında**
göstereceğini bilmesine bağlıdır. Erişilebilir Bileşen Kalıpları kursundaki hata sunumu bu
eşleşmeyi varsayar: her hata kendi alanının ek açıklamasına bağlanır, hata özeti odağı o
alana taşır. Eşleşme kurulamazsa geriye tek bir genel ileti kalır.

Aşağıdaki ölçüm aynı dört hatayı üç ayrı sunucu sözleşmesiyle bildirir ve istemcinin
kaçını kutusuyla eşleştirebildiğini sayar.

```js
// eslesme.mjs — uc ayri alan adlandirma sozlesmesinin istemcide eslesme oranini olcer
// Istemcideki giris kutulari, sozlesmedeki kurala gore adlandirilmistir:
// ad = JSON Pointer'in bas egik cizgisi atilmis, "/" yerine "." konmus hali.
const KUTULAR = ["uye", "kalemler.0.isbn", "kalemler.0.sube", "kalemler.1.isbn", "kalemler.1.sube", "iadeTarihi"];
const adaCevir = (yol) => yol.replace(/^\//, "").replaceAll("/", ".");

// Ayni dort hata, uc ayri sunucu sozlesmesiyle bildirilmis hali.
const SOZLESMELER = {
  "alan adı yok": {
    hatalar: [{ ileti: "Gönderdiğiniz bilgilerde dört hata var." }],
    cevir: () => null,
  },
  "sunucunun iç adları": {
    hatalar: [
      { alan: "member_id", kod: "bicim" },
      { alan: "items[1].isbn", kod: "bicim" },
      { alan: "items[1].branch", kod: "secenek_disi" },
      { alan: "due_date", kod: "cok_ileri" },
    ],
    cevir: (h) => h.alan,
  },
  "gövde yolu (JSON Pointer)": {
    hatalar: [
      { yol: "/uye", kod: "bicim" },
      { yol: "/kalemler/1/isbn", kod: "bicim" },
      { yol: "/kalemler/1/sube", kod: "secenek_disi" },
      { yol: "/iadeTarihi", kod: "cok_ileri" },
    ],
    cevir: (h) => adaCevir(h.yol),
  },
};

console.log("sözleşme                    bildirilen  eşleşen  kutusunun yanında gösterilebilen");
for (const [ad, { hatalar, cevir }] of Object.entries(SOZLESMELER)) {
  const eslesen = hatalar.filter((h) => KUTULAR.includes(cevir(h)));
  console.log(
    `${ad.padEnd(27)} ${String(hatalar.length).padStart(9)}  ${String(eslesen.length).padStart(7)}  ` +
    `${eslesen.length ? eslesen.map((h) => cevir(h)).join(", ") : "hiçbiri"}`
  );
}
```

```
sözleşme                    bildirilen  eşleşen  kutusunun yanında gösterilebilen
alan adı yok                        1        0  hiçbiri
sunucunun iç adları                 4        0  hiçbiri
gövde yolu (JSON Pointer)           4        4  uye, kalemler.1.isbn, kalemler.1.sube, iadeTarihi
```

İkinci satır ilkinden daha kötüdür, çünkü aldatıcıdır: sunucu dört ayrı hata bildirmiş
görünür, alan adları da vardır, ama o adlar sunucunun kendi iç veri modelinden gelir.
`member_id`, `items[1].branch` — istemcinin gövdesinde böyle bir alan yoktur. İstemci ya
sunucunun iç adlarıyla kendi alan adları arasında elle bir çeviri tablosu tutar ya da
hataları eşleştirmeden gösterir. Çeviri tablosu tutulursa sunucunun her iç yeniden
adlandırması istemciyi bozar; yani sözleşmede olmayan bir şey sözleşme gibi davranmaya
başlar.

Kural şudur: **hata yolu, istemcinin gönderdiği gövdedeki alanın adresidir.** Sunucunun iç
model adları değil, istek gövdesinin yapısı. Bu kural iki özelliği garanti eder. Toplamdır
— gövdedeki her alanın bir yolu vardır, dizi öğeleri dâhil. Ve tek yönlü türetilebilir:
istemci gövdeyi kendisi kurduğu için yolu kutu adına çevirecek kuralı yazabilir, ters
yönde bir tablo tutmasına gerek kalmaz.

Kalan tek karar, gövde yolunun hangi biçimde yazılacağıdır. JSON Pointer tanımlı bir
standarttır, dizi dizinlerini doğal biçimde ifade eder ve kaçırma kuralları belirlidir.
Yerine nokta ayırıcı da seçilebilir; belirleyici olan biçimin kendisi değil, **belgelenmiş
ve her alan için tanımlı** olmasıdır.

#### Özet

- Doğrulama ilk hatada durduğunda $n$ yanlış alan $n+1$ istek turu gerektirir; hatalar
  toplu bildirildiğinde iki tur yeter ve kullanıcıdan bir sonraki hatanın varlığı
  saklanmaz.
- Alanlar arasında tarama sürer, alan içinde ilk hata yeter: aynı alanın ardışık kural
  ihlalleri birbirinin sonucudur.
- Hata kaydı üç alan taşır — yol, kod, parametre; kullanıcıya gösterilecek metin sunucudan
  değil koddan ve parametreden istemcide üretilir.
- Anlaşılamayan istek 400, anlaşılıp anlamı reddedilen istek 422 ile bildirilir; ayrım
  kütükteki artışın nedenini de ayırır.
- Hata yolu istemcinin gönderdiği gövdedeki adrestir, sunucunun iç model adı değil; iç
  adlar bildirildiğinde eşleşme oranı dörtte sıfıra düşer.
- Alan adlandırma sözleşmesi toplam ve tek yönlü türetilebilir olmalıdır; hangi biçimin
  seçildiği değil, belgelenmiş ve her alan için tanımlı olması belirleyicidir.

#### Sonraki Adım

Doğrulama şeması bu derste sabit kabul edildi: `/kalemler` en çok üç öğe alır, iade tarihi
en çok otuz gün ileri olabilir. Kütüphane bu sınırları değiştirmeye karar verdiğinde ne
olacak? Kalem sınırının beşe çıkması hiçbir istemciyi bozmaz; üçe düşmesi, dört kalemlik
istek yollayan her istemciyi bozar. Aynı gövde alanının adının değişmesi ise bütün
istemcileri aynı anda bozar. Sözleşmenin değişmesi kaçınılmazdır; sonraki ders bu
değişikliği yönetilebilir kılan aracı ele alır ve aynı kaynağın iki sürümünü tek sunucuda
yayımlayarak yol, başlık ve içerik tabanlı sürümlemenin istemci koduna maliyetini
karşılaştırır.

### Sürümleme Stratejileri

Source: https://academia.sh/tr/kurslar/api-tasarimi/surumleme-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.

### Geriye Dönük Uyumluluk

Source: https://academia.sh/tr/kurslar/api-tasarimi/geriye-donuk-uyumluluk

Aynı değişikliğin istek ve yanıt yönünde ters sınıflandırılması, iki şema sürümünü karşılaştırıp kırıcı değişiklikleri saptayan bir fark alıcı ve sürüm numarasının tartışmadan farktan türetilmesi.

Bir önceki 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 kırıcı sayıldı, `sube` alanına yeni bir seçenek
eklemek konuşulmadı. Bu sorular sezgiye bırakıldığında her ekip başka yanıt verir; sürüm
numarası kanıya dayanır ve "bu küçük bir değişiklik" cümlesiyle atlanır.

Bu ders kırıcılığı tanımlar, bir fark alıcı yazar ve sürüm numarasını farktan türetir.
Tanımın çıkış noktası basit bir gözlemdir: **aynı değişiklik, istek yönünde uyumluyken
yanıt yönünde kırıcı olabilir.**

#### Yönün Belirlediği Kırıcılık

Bir sözleşmenin iki yönü vardır. İstemci gövde **gönderir**, sunucu gövde **üretir**.
İstemcinin gönderdiği için sunucu bir kabul kümesi tanımlar; sunucunun ürettiği için de bir
üretim kümesi.

Sunucunun **kabul ettiği daralırsa** eski istemciler dışarıda kalır. Kabul edilenler
genişlerse eski istemciler etkilenmez; gönderdikleri hâlâ kümenin içindedir. Yani istek
yönünde daralma kırıcı, genişleme uyumludur.

Sunucunun **ürettiği genişlerse** eski istemciler tanımadıkları bir şeyle karşılaşır.
Ürettiği daralırsa istemciler etkilenmez; okudukları alanlar hâlâ yerindedir. Yani yanıt
yönünde genişleme kırıcı, daralma uyumludur.

İki yönün kuralı tam olarak birbirinin tersidir. Bu ters ilişki, `/isbn` alanının kaldırılması
gibi tek bir değişikliğin istekte uyumlu, yanıtta kırıcı olmasının nedenidir: istekten
kaldırılan alanı eski istemci göndermeye devam eder, sunucu yok sayar; yanıttan kaldırılan
alanı ise eski istemci aramaya devam eder ve bulamaz.

Aşağıdaki iki şema, ödünç kaynağının bir sürüm aralığındaki hâlini verir. Her alan üç şey
bildirir: tipi, zorunlu olup olmadığı ve varsa alabileceği değerler.

```js
// semalar.mjs — odunc kaynaginin iki sema surumu
// Alan: { tip, zorunlu, degerler }.  degerler yoksa deger kumesi sinirsizdir.
export const V1 = {
  surum: "1.4.2",
  istek: {
    "/uye":       { tip: "dizgi", zorunlu: true },
    "/isbn":      { tip: "dizgi", zorunlu: true },
    "/sube":      { tip: "dizgi", zorunlu: false, degerler: ["merkez", "sahil"] },
    "/gunSayisi": { tip: "sayi",  zorunlu: false },
  },
  yanit: {
    "/id":         { tip: "dizgi", zorunlu: true },
    "/uye":        { tip: "dizgi", zorunlu: true },
    "/isbn":       { tip: "dizgi", zorunlu: true },
    "/iadeTarihi": { tip: "dizgi", zorunlu: true },
    "/durum":      { tip: "dizgi", zorunlu: true, degerler: ["acik", "kapali"] },
  },
};

export const V2 = {
  istek: {
    "/uye":       { tip: "dizgi",   zorunlu: true },
    "/kalemler":  { tip: "dizi",    zorunlu: true },                                    // yeni zorunlu alan
    "/sube":      { tip: "dizgi",   zorunlu: true, degerler: ["merkez", "sahil", "tepe"] },
    "/gunSayisi": { tip: "tamsayi", zorunlu: false },                                   // tip daraldi
    "/not":       { tip: "dizgi",   zorunlu: false },                                   // yeni istege bagli alan
  },
  yanit: {
    "/id":         { tip: "dizgi", zorunlu: true },
    "/uye":        { tip: "nesne", zorunlu: true },                                     // tip degisti
    "/kalemler":   { tip: "dizi",  zorunlu: true },                                     // yeni alan
    "/iadeTarihi": { tip: "dizgi", zorunlu: false },                                    // artik bulunmayabilir
    "/durum":      { tip: "dizgi", zorunlu: true, degerler: ["acik", "kapali", "gecikmis"] },
  },
};
```

#### Fark Alıcı

Fark alıcı iki şemayı gezer, her farkı bulur ve yön kuralını uygular. Tip değişiklikleri
için küçük bir alt tip ilişkisi gerekir: her tam sayı bir sayıdır, tersi doğru değildir.
Aralarında böyle bir ilişki olmayan tipler her iki yönde de kırıcıdır.

```js
// fark.mjs — iki sema surumunu karsilastirir, kirici degisiklikleri saptar, surumu turetir
// Kullanim: node fark.mjs [hosgoru]   ("hosgoru": istemciler bilinmeyen degeri hos gorur)

import { V1, V2 } from "./semalar.mjs";

const HOSGORU = process.argv[2] === "hosgoru";

// Tip kafesi: tamsayi, sayi'nin alt tipidir. Alt tip, ust tipin kabul ettiginin bir bolumudur.
const ALT_TIPLER = { tamsayi: ["sayi"], tarih: ["dizgi"] };
const altTipMi = (a, b) => a === b || (ALT_TIPLER[a] ?? []).includes(b);

// Iki yon, iki kural. Istek: sunucunun kabul ettigi daralirsa kirici.
// Yanit: sunucunun urettigi genislerse kirici.
function karsilastir(eski, yeni, yon) {
  const kirici = yon === "istek"
    ? { daralma: true, genisleme: false, zorunluEkleme: true, zorunluKalkma: false, kaldirma: false }
    : { daralma: false, genisleme: true, zorunluEkleme: false, zorunluKalkma: true, kaldirma: true };
  const bulgular = [];
  const ekle = (sinif, ad, aciklama) => bulgular.push({ sinif, yon, ad, aciklama });

  for (const [ad, e] of Object.entries(eski)) {
    const y = yeni[ad];
    if (!y) { ekle(kirici.kaldirma ? "kirici" : "uyumlu", ad, "alan kaldırıldı"); continue; }
    if (!e.zorunlu && y.zorunlu) ekle(kirici.zorunluEkleme ? "kirici" : "uyumlu", ad, "isteğe bağlıydı, zorunlu oldu");
    if (e.zorunlu && !y.zorunlu) ekle(kirici.zorunluKalkma ? "kirici" : "uyumlu", ad, "zorunluydu, isteğe bağlı oldu");
    if (e.tip !== y.tip) {
      const daralma = altTipMi(y.tip, e.tip), genisleme = altTipMi(e.tip, y.tip);
      const sinif = daralma ? (kirici.daralma ? "kirici" : "uyumlu")
        : genisleme ? (kirici.genisleme ? "kirici" : "uyumlu")
        : "kirici";                                       // ilgisiz tipler her yonde kirici
      ekle(sinif, ad, `tip ${e.tip} → ${y.tip}` + (daralma ? " (daralma)" : genisleme ? " (genişleme)" : " (ilgisiz)"));
    }
    const eD = e.degerler, yD = y.degerler;
    if (eD && yD) {
      const kalkan = eD.filter((d) => !yD.includes(d)), gelen = yD.filter((d) => !eD.includes(d));
      if (kalkan.length) ekle(kirici.daralma ? "kirici" : "uyumlu", ad, `değer kümesi daraldı: ${kalkan.join(",")} kalktı`);
      if (gelen.length) {
        const kiriciMi = kirici.genisleme && !(yon === "yanit" && HOSGORU);
        ekle(kiriciMi ? "kirici" : "uyumlu", ad, `değer kümesi genişledi: ${gelen.join(",")} eklendi`);
      }
    }
  }
  for (const [ad, y] of Object.entries(yeni)) {
    if (eski[ad]) continue;
    const kiriciMi = y.zorunlu && kirici.zorunluEkleme;
    ekle(kiriciMi ? "kirici" : "ekleme", ad, y.zorunlu ? "zorunlu alan eklendi" : "isteğe bağlı alan eklendi");
  }
  return bulgular;
}

const bulgular = [...karsilastir(V1.istek, V2.istek, "istek"), ...karsilastir(V1.yanit, V2.yanit, "yanit")];
const SIRA = { kirici: 0, ekleme: 1, uyumlu: 2 };
bulgular.sort((a, b) => SIRA[a.sinif] - SIRA[b.sinif] || a.yon.localeCompare(b.yon));

console.log(`hoşgörü bildirimi: ${HOSGORU ? "var" : "yok"}\n`);
console.log("sınıf    yön     alan          değişiklik");
for (const b of bulgular) console.log(`${b.sinif.padEnd(8)} ${b.yon.padEnd(7)} ${b.ad.padEnd(13)} ${b.aciklama}`);

const sayim = { kirici: 0, ekleme: 0, uyumlu: 0 };
for (const b of bulgular) sayim[b.sinif]++;
const [ana, ikincil, yama] = V1.surum.split(".").map(Number);
const sonraki = sayim.kirici ? `${ana + 1}.0.0` : sayim.ekleme ? `${ana}.${ikincil + 1}.0` : `${ana}.${ikincil}.${yama + 1}`;

console.log(`\nkırıcı: ${sayim.kirici}   ekleme: ${sayim.ekleme}   uyumlu: ${sayim.uyumlu}`);
console.log(`mevcut sürüm: ${V1.surum}   türetilen sürüm: ${sonraki}`);
console.log(`kırıcılar ayıklansaydı: ${ana}.${ikincil + 1}.0`);
```

```
hoşgörü bildirimi: yok

sınıf    yön     alan          değişiklik
kirici   istek   /sube         isteğe bağlıydı, zorunlu oldu
kirici   istek   /gunSayisi    tip sayi → tamsayi (daralma)
kirici   istek   /kalemler     zorunlu alan eklendi
kirici   yanit   /uye          tip dizgi → nesne (ilgisiz)
kirici   yanit   /isbn         alan kaldırıldı
kirici   yanit   /iadeTarihi   zorunluydu, isteğe bağlı oldu
kirici   yanit   /durum        değer kümesi genişledi: gecikmis eklendi
ekleme   istek   /not          isteğe bağlı alan eklendi
ekleme   yanit   /kalemler     zorunlu alan eklendi
uyumlu   istek   /isbn         alan kaldırıldı
uyumlu   istek   /sube         değer kümesi genişledi: tepe eklendi

kırıcı: 7   ekleme: 2   uyumlu: 2
mevcut sürüm: 1.4.2   türetilen sürüm: 2.0.0
kırıcılar ayıklansaydı: 1.5.0
```

Son iki satır ters ilişkiyi doğrudan gösteriyor. `/isbn` alanının kaldırılması istekte
uyumlu, yanıtta kırıcı. `/sube` alanının değer kümesine `tepe` eklenmesi istekte uyumlu;
`/durum` alanının değer kümesine `gecikmis` eklenmesi yanıtta kırıcı. Aynı işlem, karşı
yönde karşıt sınıfta.

`ekleme` satırlarındaki `/kalemler` alanı da bu kuralın sonucudur: yanıta zorunlu bir alan
eklemek uyumludur, çünkü eski istemci o alanı zaten okumuyordu. İsteğe zorunlu alan
eklemek ise kırıcıdır ve tabloda öyle görünüyor.

Sürüm numarası artık tartışılan değil, **türetilen** bir değerdir. Yedi kırıcı değişiklik
ana sürümü yükseltir. Son satır ayrı bir seçenek gösterir: kırıcılar yayından ayıklanıp
yalnız eklemeler çıkarılsaydı sürüm 1.5.0 olurdu. Bu iki satır, "sürümü ne yapalım"
tartışmasını "yayına neyi koyacağız" tartışmasına çevirir; ikincisi yanıtlanabilir bir
sorudur.

#### Hoşgörü Bildirimi

Yanıttaki değer kümesinin genişlemesi, listedeki en tartışmalı kalemdir. `gecikmis`
durumunun eklenmesi eski istemcileri bozar mı? Yanıt, sözleşmede ne yazdığına bağlıdır.
Sözleşme "bilinmeyen bir durum değeri gelirse istemci onu tanınmayan olarak ele almalıdır"
diyorsa, genişleme kırıcı değildir; demiyorsa kırıcıdır.

```bash
node fark.mjs hosgoru | tail -4
```

```
kırıcı: 6   ekleme: 2   uyumlu: 3
mevcut sürüm: 1.4.2   türetilen sürüm: 2.0.0
kırıcılar ayıklansaydı: 1.5.0
```

Kırıcı sayısı yediden altıya iniyor. Buradaki ders, hoşgörünün bir **sözleşme maddesi**
olmasıdır. Sunucu tarafında alınan bir karar değildir; istemcilerin gerçekten öyle
yazılmış olmasını gerektirir ve bu ancak sözleşmede önceden duyurulmuşsa beklenebilir.
Sözleşmeye sonradan eklenen bir hoşgörü maddesi, kendisi kırıcı bir değişikliktir.

#### Farkın Söylemediği

Fark alıcı **neyin bozulabileceğini** söyler, **neyin bozulacağını** değil. Ayrımı görmek
için aynı şemalar gerçek bir eski istemciyle sınanır.

```js
// sinama.mjs — siniflandirmayi eski istemciyi calistirarak sinar
// Eski istemci v1 govdesi yollar ve v1 yanitini okur; sunucu v2 semasindadir.
import { V1, V2 } from "./semalar.mjs";

const ESKI_ISTEK = { "/uye": "U-1001", "/isbn": "978-0262033848", "/sube": "sahil", "/gunSayisi": 14.5 };
const YENI_YANIT = { "/id": "O-1", "/uye": { kimlik: "U-1001" }, "/kalemler": [{}], "/durum": "gecikmis" };

const tipi = (d) =>
  Array.isArray(d) ? "dizi" : typeof d === "object" ? "nesne"
    : typeof d === "number" ? (Number.isInteger(d) ? "tamsayi" : "sayi") : "dizgi";

// 1) Eski istemcinin govdesi yeni semaya sokulur.
const istekHatalari = [];
for (const [ad, k] of Object.entries(V2.istek)) {
  const d = ESKI_ISTEK[ad];
  if (d === undefined) { if (k.zorunlu) istekHatalari.push(`${ad}: zorunlu alan gönderilmemiş`); continue; }
  if (k.tip !== tipi(d) && !(k.tip === "sayi" && tipi(d) === "tamsayi")) istekHatalari.push(`${ad}: ${tipi(d)} gönderildi, ${k.tip} bekleniyor`);
  if (k.degerler && !k.degerler.includes(d)) istekHatalari.push(`${ad}: ${d} kabul edilmiyor`);
}

// 2) Yeni yanit, eski istemcinin bekledigi semayla okunur.
const yanitHatalari = [];
for (const [ad, k] of Object.entries(V1.yanit)) {
  const d = YENI_YANIT[ad];
  if (d === undefined) { if (k.zorunlu) yanitHatalari.push(`${ad}: beklenen alan yanıtta yok`); continue; }
  if (k.tip !== tipi(d)) yanitHatalari.push(`${ad}: ${tipi(d)} geldi, ${k.tip} bekleniyordu`);
  if (k.degerler && !k.degerler.includes(d)) yanitHatalari.push(`${ad}: ${d} tanınmayan değer`);
}

console.log("— eski istemcinin gövdesi yeni sunucuda —");
for (const h of istekHatalari) console.log(`  ${h}`);
console.log(`  reddedilen alan sayısı: ${istekHatalari.length}`);

console.log("\n— yeni sunucunun yanıtı eski istemcide —");
for (const h of yanitHatalari) console.log(`  ${h}`);
console.log(`  okunamayan alan sayısı: ${yanitHatalari.length}`);
```

```
— eski istemcinin gövdesi yeni sunucuda —
  /kalemler: zorunlu alan gönderilmemiş
  /gunSayisi: sayi gönderildi, tamsayi bekleniyor
  reddedilen alan sayısı: 2

— yeni sunucunun yanıtı eski istemcide —
  /uye: nesne geldi, dizgi bekleniyordu
  /isbn: beklenen alan yanıtta yok
  /iadeTarihi: beklenen alan yanıtta yok
  /durum: gecikmis tanınmayan değer
  okunamayan alan sayısı: 4
```

Fark alıcı istek yönünde üç kırıcı değişiklik bulmuştu; bu istemcide yalnız ikisi
gerçekleşti. Farkı yaratan `/sube` alanıdır: isteğe bağlıyken zorunlu olmuştur, ama bu
istemci onu zaten gönderiyordu. Değişiklik sözleşme gereği kırıcıdır — sürüm numarasını
yükseltir — ama bu istemci için maliyeti sıfırdır.

Sözleşme ile maliyet ayrı ölçülerdir. Sürüm numarası sözleşmeye bakar ve tek bir istemci
etkilenmese bile yükselir; yayın planı ise maliyete bakar. Tasarım Sistemleri kursundaki
göç maliyeti hesabı da bu ayrımı kurmuştu: kırıcılık sözleşmenin, etkilenen çağrı yeri
sayısı gerçekliğin ölçüsüdür.

Yanıt yönündeki dört kalem ise tam olarak gerçekleşmiştir. Bu, yönler arasındaki başka bir
asimetriyi gösterir: istek yönündeki kırıcılık istemcinin **ne gönderdiğine** bağlıdır,
yanıt yönündeki kırıcılık istemcinin **ne okuduğuna**. İstemciler genellikle şemadaki her
alanı gönderme zorunda değildir ama okudukları alanlar sabittir; bu yüzden yanıt yönündeki
kırıcı değişiklikler daha yüksek oranda gerçek maliyete dönüşür.

#### Özet

- Sözleşmenin iki yönü ters kurallara tabidir: sunucunun kabul ettiği daralırsa istek
  yönünde kırıcı, ürettiği genişlerse yanıt yönünde kırıcıdır.
- Aynı değişiklik iki yönde karşıt sınıfa düşer; alanın istekten kaldırılması uyumlu,
  yanıttan kaldırılması kırıcıdır.
- Yanıta zorunlu alan eklemek uyumludur, isteğe zorunlu alan eklemek kırıcıdır; tip
  daralması istekte, tip genişlemesi yanıtta kırıcıdır.
- Sürüm numarası fark sonucundan türetilir; kırıcılar yayından ayıklandığında türetilen
  numara da değişir, böylece tartışma sürümden yayın kapsamına taşınır.
- Bilinmeyen değere hoşgörü bir sözleşme maddesidir ve önceden duyurulmuş olmalıdır;
  sonradan eklenmesi kendisi kırıcı bir değişikliktir.
- Fark alıcı neyin bozulabileceğini söyler; hangi istemcinin gerçekten bozulacağı ayrı bir
  ölçümdür ve yanıt yönündeki kırıcılıklar daha yüksek oranda gerçek maliyete dönüşür.

#### Sonraki Adım

Fark alıcı yedi kırıcı değişiklik saptadı ve sürümü 2.0.0'a çıkardı, ama bir soruyu
yanıtsız bıraktı: eski sürüm ne zamana kadar yaşayacak? Sürüm numarasını yükseltmek eski
istemcileri kendiliğinden taşımaz; taşınmaları için önce durumdan haberdar olmaları, sonra
da geçiş için süre bulmaları gerekir. Sonraki ders bu süreyi bir politikaya çevirir:
kullanımdan kaldırmayı yanıtın kendisinde duyuran başlıkları ekler, kullanım telemetrisini
modelleyip geçiş süresinin sonunda hâlâ eski sürümde kalan tüketici oranını hesaplar ve
kapatma tarihinin bu orana göre nasıl seçileceğini gösterir.

### Kullanımdan Kaldırma Politikası

Source: https://academia.sh/tr/kurslar/api-tasarimi/kullanimdan-kaldirma-politikasi

Kullanımdan kaldırmanın yanıtın kendisinde duyurulması, tüketici başına sürüm telemetrisinin toplanması ve geçiş süresi sonunda eski sürümde kalan tüketici ile trafik oranının hesaplanması.

Fark alıcı yedi kırıcı değişiklik saptadı ve sürümü 2.0.0'a çıkardı. Sürüm numarasının
yükselmesi eski istemcileri kendiliğinden taşımaz; v1 hâlâ çalışır ve onu çağıran her
istemci çalışmaya devam eder. Kütüphanenin v1'i sonsuza kadar sürdürmesi de bir seçenek
değildir: iki sürüm demek, her hata düzeltmesinin iki kez yapılması ve her yeni alanın iki
biçimde üretilmesi demektir.

Aradaki yol **kullanımdan kaldırmadır**: sürüm önce kullanımdan kaldırılmış ilan edilir,
bir süre daha çalışır, sonra kapatılır. Bu ders üç şeyi kurar — duyurunun nereye
yazılacağı, kimin hâlâ eski sürümde olduğunun nasıl ölçüleceği ve kapanış tarihinin hangi
sayıya bakarak seçileceği.

#### Duyurunun Yanıta Yazılması

Duyuru belgeye yazılırsa yalnızca belgeyi okuyanlar görür. Duyurunun ulaşması gereken
taraf, eski sürümü **çağıran koddur**; o kodu yazan kişinin belgeyi tam o hafta okuması
beklenemez. Bu yüzden duyuru yanıtın kendisine konur.

İki başlık bunu tanımlar. `Deprecation` kaynağın kullanımdan kaldırıldığı anı bildirir;
`Sunset` erişimin sonlanacağı anı. İkisi ayrı bilgidir: birincisi "artık bunu kullanmayın",
ikincisi "şu tarihte çalışmayacak". Yanına konan `Link` başlıkları da yerine geçen kaynağı
ve göç kılavuzunu gösterir; duyuru yalnızca "bu kalkacak" diyorsa hiçbir göçü hızlandırmaz.

```js
// sunucu.mjs — v1 kaynagini kullanimdan kaldirma duyurusuyla sunar
// Kullanim: node sunucu.mjs <bugun>   ornek: node sunucu.mjs 2026-04-10
import { createServer } from "node:http";

const BUGUN = new Date(process.argv[2] ?? "2026-04-10");
const DUYURU = new Date("2026-03-01T00:00:00Z");   // kullanimdan kaldirmanin ilan tarihi
const KAPANIS = new Date("2026-09-01T00:00:00Z");  // erisimin sonlanacagi tarih

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

// Telemetri: hangi tuketicinin hangi surumu kac kez cagirdigi.
const sayim = new Map();   // "tuketici|surum" -> istek sayisi

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

  if (yol === "/telemetri") {
    yanit.writeHead(200, { "content-type": "application/json; charset=utf-8" });
    return yanit.end(JSON.stringify([...sayim].map(([a, s]) => ({ tuketici: a.split("|")[0], surum: a.split("|")[1], istek: s }))));
  }

  const surum = /^\/v(\d+)\//.exec(yol)?.[1];
  const tuketici = istek.headers["api-tuketici"] ?? "bilinmiyor";
  if (surum) sayim.set(`${tuketici}|v${surum}`, (sayim.get(`${tuketici}|v${surum}`) ?? 0) + 1);

  if (yol === "/v2/odunc/O-1") {
    yanit.writeHead(200, { "content-type": "application/json; charset=utf-8" });
    return yanit.end(JSON.stringify({ id: KAYIT.id, uye: { kimlik: KAYIT.uye }, kalemler: [{ isbn: KAYIT.isbn }], sonTarih: KAYIT.iadeTarihi }));
  }

  if (yol === "/v1/odunc/O-1") {
    // Kapanis gecmisse kaynak artik yok: 410, kalici bir sonlanma bildirir.
    if (BUGUN >= KAPANIS) {
      yanit.writeHead(410, { "content-type": "application/problem+json; charset=utf-8", link: '</v2/odunc/O-1>; rel="successor-version"' });
      return yanit.end(JSON.stringify({
        type: "https://ornek.kutuphane/sorunlar/surum-kapandi", title: "Sürüm kapatıldı", status: 410,
        detail: `v1 ${KAPANIS.toUTCString()} tarihinde kapatıldı; /v2/odunc kullanılır.`, instance: "ol-0001",
      }));
    }
    // Duyuru: Deprecation kullanimdan kaldirma anini, Sunset erisimin bitecegi ani verir.
    yanit.writeHead(200, {
      "content-type": "application/json; charset=utf-8",
      deprecation: `@${Math.floor(DUYURU.getTime() / 1000)}`,
      sunset: KAPANIS.toUTCString(),
      link: '</v2/odunc/O-1>; rel="successor-version", </belgeler/goc-v2>; rel="deprecation"',
    });
    return yanit.end(JSON.stringify(KAYIT));
  }

  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-0002" }));
}).listen(8436, "127.0.0.1", () => console.log(`sunucu 127.0.0.1:8436 bugun=${BUGUN.toISOString().slice(0, 10)}`));
```

```bash
#!/usr/bin/env bash
# Kapanis oncesi/sonrasi ayni istek; ardindan telemetri dokumu.
for bugun in 2026-04-10 2026-09-02; do
  node sunucu.mjs "$bugun" & s=$!
  sleep 0.5
  echo "--- $bugun ---"
  curl -sS -D - -o /tmp/g -H 'Api-Tuketici: mobil-uygulama' http://127.0.0.1:8436/v1/odunc/O-1 \
    | grep -iE '^HTTP|^deprecation|^sunset|^link' | tr -d '\r'
  cat /tmp/g; echo
  if [ "$bugun" = "2026-04-10" ]; then
    curl -sS -o /dev/null -H 'Api-Tuketici: raf-terminali' http://127.0.0.1:8436/v1/odunc/O-1
    curl -sS -o /dev/null -H 'Api-Tuketici: mobil-uygulama' http://127.0.0.1:8436/v2/odunc/O-1
    echo "telemetri: $(curl -sS http://127.0.0.1:8436/telemetri)"
  fi
  kill "$s"; wait "$s" 2>/dev/null
done
```

```
sunucu 127.0.0.1:8436 bugun=2026-04-10
--- 2026-04-10 ---
HTTP/1.1 200 OK
deprecation: @1772323200
sunset: Tue, 01 Sep 2026 00:00:00 GMT
link: </v2/odunc/O-1>; rel="successor-version", </belgeler/goc-v2>; rel="deprecation"
{"id":"O-1","uye":"U-1001","isbn":"978-0262033848","iadeTarihi":"2026-03-20"}
telemetri: [{"tuketici":"mobil-uygulama","surum":"v1","istek":1},{"tuketici":"raf-terminali","surum":"v1","istek":1},{"tuketici":"mobil-uygulama","surum":"v2","istek":1}]
sunucu 127.0.0.1:8436 bugun=2026-09-02
--- 2026-09-02 ---
HTTP/1.1 410 Gone
link: </v2/odunc/O-1>; rel="successor-version"
{"type":"https://ornek.kutuphane/sorunlar/surum-kapandi","title":"Sürüm kapatıldı","status":410,"detail":"v1 Tue, 01 Sep 2026 00:00:00 GMT tarihinde kapatıldı; /v2/odunc kullanılır.","instance":"ol-0001"}
```

Üç ayrıntı önemlidir. Duyuru başlıkları **başarılı** yanıtla birlikte gelir; istek yerine
getirilmiştir, yalnızca bir uyarı eşlik eder. Kapanıştan sonra dönen kod 404 değil
**410**'dur: 404 "burada bir şey yok" der, 410 "burada bir şey vardı ve kalıcı olarak
kaldırıldı" der; ikincisi istemcinin yanlış adres kurduğunu sanmasını önler. Ve 410
yanıtında da `Link` başlığı durur, çünkü kapanış anı, göç bilgisine en çok ihtiyaç
duyulacak andır.

Telemetri satırı üçüncü parçadır. Sunucu her isteği tüketici kimliği ve sürümle sayar; bu
sayaç olmadan kapanış tarihi tahminle seçilir. Tüketici kimliği burada bir başlıkla
gelmektedir; kimlik doğrulama kurulduğunda bu bilgi belirteçten okunur ve istemcinin
bildirmesine bağlı kalmaz.

#### Geçiş Süresinin Sonunda Kalan

Telemetri, kaç tüketicinin hangi hızda göç ettiğini verir. Bu hız kesime göre değişir:
etkin geliştirilen büyük tüketiciler hızlı, yıllardır dokunulmamış küçük tüketiciler yavaş
göç eder. Aşağıdaki program üç duyuru biçimi için geçiş süresi sonunda kalanı hesaplar.

```js
// gecis.mjs — gecis suresi sonunda eski surumde kalan tuketici ve trafik oranini hesaplar
// Model: her hafta, bir kesimin kalan tuketicilerinin sabit bir orani goc eder.

const KESIMLER = [
  { ad: "büyük", sayi: 4,   trafik: 0.60 },
  { ad: "orta",  sayi: 20,  trafik: 0.30 },
  { ad: "küçük", sayi: 176, trafik: 0.10 },
];

// Haftalik goc orani: duyurunun nasil yapildigina gore degisir.
const DUYURULAR = {
  "yalnız belgede":            { büyük: 0.05, orta: 0.03, küçük: 0.01 },
  "yanıt başlığında":          { büyük: 0.15, orta: 0.10, küçük: 0.04 },
  "başlık + doğrudan uyarı":   { büyük: 0.35, orta: 0.10, küçük: 0.04 },
};

const HAFTA = 26;   // 1 Mart - 1 Eylul arasi

const kalan = (oran, hafta) => (1 - oran) ** hafta;

console.log(`geçiş süresi: ${HAFTA} hafta   toplam tüketici: ${KESIMLER.reduce((t, k) => t + k.sayi, 0)}\n`);
console.log("duyuru biçimi              kalan tüketici  kalan trafik  büyük  orta  küçük");
for (const [ad, oranlar] of Object.entries(DUYURULAR)) {
  const kalanlar = KESIMLER.map((k) => kalan(oranlar[k.ad], HAFTA));
  const tuketici = KESIMLER.reduce((t, k, i) => t + k.sayi * kalanlar[i], 0);
  const trafik = KESIMLER.reduce((t, k, i) => t + k.trafik * kalanlar[i], 0);
  console.log(
    `${ad.padEnd(26)} ${Math.round(tuketici).toString().padStart(14)}  ${(trafik * 100).toFixed(1).padStart(11)}%  ` +
    kalanlar.map((o, i) => Math.round(KESIMLER[i].sayi * o).toString().padStart(5)).join(" ")
  );
}

// Kapanis olcutu trafik payiysa: pencerenin ne kadar uzamasi gerekir?
const ESIK = 0.005;
console.log(`\nkalan trafik %${ESIK * 100} altına inene kadar gereken hafta:`);
for (const [ad, oranlar] of Object.entries(DUYURULAR)) {
  let h = 0;
  while (KESIMLER.reduce((t, k) => t + k.trafik * kalan(oranlar[k.ad], h), 0) > ESIK && h < 500) h++;
  console.log(`  ${ad.padEnd(26)} ${String(h).padStart(3)} hafta (${(h / 4.35).toFixed(1)} ay)`);
}

// Kesim basina: kalan trafigin hangi kesimden geldigi
console.log("\n'yanıt başlığında' duyuruda kalan trafiğin kesimlere dağılımı:");
const o = DUYURULAR["yanıt başlığında"];
const kalanTrafik = KESIMLER.map((k) => k.trafik * kalan(o[k.ad], HAFTA));
const toplam = kalanTrafik.reduce((a, b) => a + b, 0);
KESIMLER.forEach((k, i) =>
  console.log(`  ${k.ad.padEnd(6)} ${(kalanTrafik[i] * 100).toFixed(2)}%  (kalan trafiğin %${((kalanTrafik[i] / toplam) * 100).toFixed(0)}'i)`));
```

```
geçiş süresi: 26 hafta   toplam tüketici: 200

duyuru biçimi              kalan tüketici  kalan trafik  büyük  orta  küçük
yalnız belgede                        146         37.1%      1     9   136
yanıt başlığında                       62          6.3%      0     1    61
başlık + doğrudan uyarı                62          5.4%      0     1    61

kalan trafik %0.5 altına inene kadar gereken hafta:
  yalnız belgede             299 hafta (68.7 ay)
  yanıt başlığında            75 hafta (17.2 ay)
  başlık + doğrudan uyarı     74 hafta (17.0 ay)

'yanıt başlığında' duyuruda kalan trafiğin kesimlere dağılımı:
  büyük  0.88%  (kalan trafiğin %14'i)
  orta   1.94%  (kalan trafiğin %31'i)
  küçük  3.46%  (kalan trafiğin %55'i)
```

Tablo üç sonuç veriyor.

Birincisi, duyurunun yanıta yazılmasının etkisi büyüktür: yalnız belgede duyurulduğunda
altı ay sonunda trafiğin yüzde 37'si hâlâ eski sürümdeyken, yanıt başlığıyla duyurulduğunda
bu oran yüzde 6,3'e iner. Aradaki fark bir politika değişikliği değil, duyurunun ulaştığı
yerin değişmesidir.

İkincisi, büyük tüketicilere doğrudan gidilmesi toplam trafiği yüzde 6,3'ten yalnız 5,4'e
indiriyor. Doğrudan uyarı büyük tüketicilerin göçünü belirgin biçimde hızlandırır, ama
onların payı zaten altı ay içinde erimiştir; kalan trafiğin yüzde 55'i küçük tüketicilerden
gelir ve onlara tek tek ulaşmanın maliyeti yüksektir. **Kalan trafiği azaltmanın yolu, en
büyük tüketicilerle konuşmaktan geçmez.**

Üçüncüsü ve en önemlisi, iki sayının birbirinden ayrılmasıdır. Yanıt başlığıyla duyuruda
geçiş süresi sonunda **62 tüketici** hâlâ eski sürümdedir ama bunlar trafiğin yalnız yüzde
6,3'ünü üretir. "Kaç tüketici kaldı" sorusuna verilen yanıt kapanışı erteletir, "trafiğin
ne kadarı kaldı" sorusuna verilen yanıt kapanışı mümkün kılar. İkisi aynı gerçekliğin iki
ölçüsüdür ve karar hangisine bakıldığına göre değişir.

#### Kapanış Tarihinin Seçilmesi

Son tablo, ölçütün eşik olarak konmasının sonucunu gösteriyor: kalan trafiği yüzde 0,5'in
altına indirmek, yanıt başlığıyla duyuruda 75 hafta — on yedi ay — sürer. Altı aylık pencere
bu eşiği tutturamaz.

Buradan üç seçenek çıkar ve üçü de meşrudur. Pencere on yedi aya uzatılır ve iki sürüm o
kadar süre birlikte yaşar. Eşik gevşetilir; yüzde 6,3 kabul edilebilir sayılır ve kalan
tüketiciler kapanışta 410 alır. Ya da kapanış kademelendirilir: erişim önce belirli
saatlerde kesilir, sonra tümüyle kapatılır — böylece göç etmemiş tüketiciler kapanışı
üretim ortamında fark eder, ama tam kapanma anında hazırlıksız yakalanmazlar.

Seçimi belirleyen, kalan tüketicilerin **kim** olduğudur. Kalan yüzde 6,3 kütüphanenin
kendi raf terminalleriyse pencere kısa tutulur, çünkü göçü yaptıracak taraf da
kütüphanedir. Dışarıdaki ilçe kütüphanelerinin yazılımıysa pencere uzatılır. Telemetrinin
tüketici başına toplanmasının nedeni tam olarak budur: toplam oran bu kararı vermeye
yetmez.

Pencerenin takvimle değil **yayınla** ölçülmesi ayrıca yararlıdır: "bir ana sürüm boyunca"
ifadesi, yayın hızı değişse bile anlamını korur. Ama kapanış anı yanıtta bildirilecekse bir
tarihe çevrilmek zorundadır; `Sunset` başlığı bir tarih taşır, yayın numarası değil. İkisi
birlikte kullanılır: politika yayınla yazılır, duyuru tarihe çevrilir.

#### Özet

- Duyurunun ulaşması gereken yer belge değil, eski sürümü çağıran koddur; bu yüzden
  `Deprecation` ve `Sunset` başlıkları başarılı yanıtla birlikte gönderilir.
- `Deprecation` kullanımdan kaldırma anını, `Sunset` erişimin sonlanacağı anı bildirir;
  yanlarındaki `Link` başlıkları yerine geçen kaynağı ve göç kılavuzunu gösterir.
- Kapanıştan sonra 404 değil 410 döner ve göç bağlantısı 410 yanıtında da durur.
- Duyurunun yanıta yazılması, altı ay sonunda eski sürümde kalan trafiği yüzde 37'den
  yüzde 6,3'e indirir.
- Kalan trafiğin yarıdan fazlası küçük tüketicilerden gelir; en büyük tüketicilere doğrudan
  gitmek toplam kalan trafiği yalnız yüzde 0,9 puan azaltır.
- Kalan tüketici sayısı ile kalan trafik oranı ayrı ölçülerdir ve kapanış kararı hangisine
  bakıldığına göre değişir; telemetrinin tüketici başına toplanması bu ayrımı yapabilmek
  içindir.

#### Sonraki Adım

Buraya kadar kurulan her şey — hata biçimi, alan düzeyi doğrulama, sürümler, kırıcılık
kuralları, kullanımdan kaldırma penceresi — sözleşmenin parçasıdır ve hepsi bugüne kadar
düzyazıyla anlatıldı. Düzyazı sözleşmenin iki sorunu vardır: makineye verilemez ve
gerçeklikle karşılaştırılamaz. Fark alıcı bir şema üzerinde çalışıyordu; o şema nereden
geliyor ve sunucunun gerçekten ürettiği yanıtla aynı olduğu nasıl bilinir? Sonraki ders
sözleşmeyi makine okunur bir tanıma çevirir, o tanımdan bir doğrulayıcı üretip gerçek
istekleri sınar ve şema ile gerçek yanıtın ayrıştığı durumu yakalar.

### Makine Okunur Belgeler

Source: https://academia.sh/tr/kurslar/api-tasarimi/makine-okunur-belgeler

Sözleşmenin şema olarak yazılması, aynı tanımdan hem doğrulayıcı hem insan okur belge üretilmesi ve sunucunun ürettiği yanıt ile tanımın ayrıştığı durumun çalışma anında yakalanması.

Buraya kadar kurulan her şey — hata biçimi, alan düzeyi doğrulama, sürümler, kırıcılık
kuralları, kullanımdan kaldırma penceresi — sözleşmenin parçasıdır. Sözleşmenin nerede
yazılı olduğu ise açık kalmıştır. Düzyazıyla yazılırsa iki şey yapılamaz: makineye
verilemez ve gerçeklikle karşılaştırılamaz.

Fark alıcı zaten bir şema üzerinde çalışıyordu. Bu ders o şemayı sözleşmenin **tek
kaynağı** hâline getirir: aynı tanımdan doğrulayıcı ve belge üretilir, sonra sunucunun
gerçekten ürettiği yanıt aynı tanıma karşı sınanır.

#### Sözleşmenin Şema Olarak Yazılması

Gövde şemaları için **JSON Schema** kullanılır; tip, zorunluluk, izin verilen değerler ve
desen bu sözcük dağarcığıyla ifade edilir. Şemaları saran yol/yöntem/yanıt düzeni ise
arayüz tanım biçimlerinin (OpenAPI) yaptığı işi yapar: hangi yolun hangi yöntemi
desteklediğini ve her durum kodu için hangi gövdenin döneceğini bildirir.

```js
// tanim.mjs — odunc servisinin makine okunur tanimi
// Govde semalari JSON Schema sozcukleriyle yazilmistir; disindaki sarmal, arayuz
// tanim bicimlerinin (OpenAPI) yol/yontem/yanit duzenini yansitir.

const ODUNC_ISTEK = {
  type: "object",
  required: ["uye", "kalemler"],
  additionalProperties: false,
  properties: {
    uye: { type: "string", pattern: "^U-\\d{4}$" },
    kalemler: {
      type: "array",
      items: { type: "object", required: ["isbn"], additionalProperties: false,
               properties: { isbn: { type: "string", pattern: "^97[89]-\\d{10}$" } } },
    },
    sube: { type: "string", enum: ["merkez", "sahil", "tepe"] },
  },
};

const ODUNC_YANIT = {
  type: "object",
  required: ["id", "uye", "kalemler", "iadeTarihi", "durum"],
  additionalProperties: false,
  properties: {
    id: { type: "string", pattern: "^O-\\d+$" },
    uye: { type: "string" },
    kalemler: { type: "array", items: { type: "object", required: ["isbn"], additionalProperties: false,
                properties: { isbn: { type: "string" } } } },
    iadeTarihi: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
    durum: { type: "string", enum: ["acik", "kapali"] },
  },
};

const SORUN = {
  type: "object",
  required: ["type", "title", "status", "detail", "instance"],
  properties: {
    type: { type: "string" }, title: { type: "string" }, status: { type: "integer" },
    detail: { type: "string" }, instance: { type: "string" },
  },
};

export const TANIM = {
  "POST /odunc": { istek: ODUNC_ISTEK, yanit: { 201: ODUNC_YANIT, 422: SORUN } },
  "GET /odunc/{id}": { istek: null, yanit: { 200: ODUNC_YANIT, 404: SORUN } },
};
```

Tanımın hata yanıtlarını da kapsadığına dikkat edilmeli. Sözleşme yalnız başarı gövdesini
tanımlarsa, istemci hata durumunda ne bekleyeceğini yine bilemez; ilk derste kurulan
problem ayrıntısı biçimi de burada şema olarak yazılır ve böylece denetlenebilir hâle
gelir.

#### Tanımdan Doğrulayıcı Üretmek

Şemanın makine okunur olması, ondan bir doğrulayıcı **üretilebilmesi** demektir. Aşağıdaki
program JSON Schema'nın kullanılan alt kümesini yorumlar ve her şema için bir doğrulama
işlevi döndürür.

```js
// dogrulayici.mjs — JSON Schema alt kumesinden dogrulayici uretir
// Desteklenen sozcukler: type, required, properties, items, enum, pattern, additionalProperties

const tipi = (d) =>
  d === null ? "null" : Array.isArray(d) ? "array"
    : typeof d === "number" ? (Number.isInteger(d) ? "integer" : "number") : typeof d;

function sina(sema, deger, yol, hatalar) {
  const t = tipi(deger);
  if (sema.type && t !== sema.type && !(sema.type === "number" && t === "integer")) {
    hatalar.push(`${yol || "/"}: ${sema.type} bekleniyordu, ${t} geldi`);
    return;
  }
  if (sema.enum && !sema.enum.includes(deger)) hatalar.push(`${yol}: ${JSON.stringify(deger)} izin verilen değerlerde yok`);
  if (sema.pattern && !new RegExp(sema.pattern).test(String(deger))) hatalar.push(`${yol}: ${JSON.stringify(deger)} desene uymuyor`);

  if (sema.type === "object") {
    for (const ad of sema.required ?? []) if (!(ad in deger)) hatalar.push(`${yol}/${ad}: zorunlu alan yok`);
    if (sema.additionalProperties === false) {
      for (const ad of Object.keys(deger)) {
        if (!(ad in (sema.properties ?? {}))) hatalar.push(`${yol}/${ad}: tanımda olmayan alan`);
      }
    }
    for (const [ad, alt] of Object.entries(sema.properties ?? {})) {
      if (ad in deger) sina(alt, deger[ad], `${yol}/${ad}`, hatalar);
    }
  }
  if (sema.type === "array" && sema.items) deger.forEach((o, i) => sina(sema.items, o, `${yol}/${i}`, hatalar));
}

// Semadan bir dogrulayici islev uretir: (deger) -> hata listesi
export const dogrulayiciUret = (sema) => (deger) => {
  const hatalar = [];
  sina(sema, deger, "", hatalar);
  return hatalar;
};
```

Hata iletilerinin gövde yolunu taşıdığına dikkat edilmeli: doğrulama hataları dersinde
kurulan alan adlandırma sözleşmesi burada kendiliğinden korunur, çünkü yol şemanın
gezilmesi sırasında üretilir. Şema tek kaynak olduğunda, alan adları da tek kaynaktan
gelir.

Aynı tanımdan insan okur belge de üretilir. Belge ile doğrulayıcının aynı dosyadan gelmesi,
belgelendirme sapmasını yapısal olarak imkânsız kılar.

```js
// belge.mjs — dogrulayiciyi ureten tanimdan insan okur belge uretir
import { TANIM } from "./tanim.mjs";

const nitelik = (s) => [
  s.type,
  s.enum ? `değerler: ${s.enum.join("|")}` : null,
  s.pattern ? `desen: ${s.pattern}` : null,
].filter(Boolean).join(", ");

function yaz(sema, girinti = "  ") {
  if (sema.type === "object") {
    for (const [ad, alt] of Object.entries(sema.properties ?? {})) {
      const zorunlu = (sema.required ?? []).includes(ad) ? "zorunlu" : "isteğe bağlı";
      console.log(`${girinti}${ad.padEnd(12)} ${zorunlu.padEnd(12)} ${nitelik(alt)}`);
      if (alt.type === "object" || alt.type === "array") yaz(alt.type === "array" ? alt.items : alt, girinti + "  ");
    }
    if (sema.additionalProperties === false) console.log(`${girinti}(tanımda olmayan alan kabul edilmez)`);
  }
}

const SUZGEC = process.argv[2];   // istege bagli: yalniz bu anahtar yazilir

for (const [anahtar, t] of Object.entries(TANIM)) {
  if (SUZGEC && anahtar !== SUZGEC) continue;
  console.log(`\n## ${anahtar}`);
  if (t.istek) { console.log(" istek gövdesi:"); yaz(t.istek); }
  for (const [kod, sema] of Object.entries(t.yanit)) { console.log(` yanıt ${kod}:`); yaz(sema); }
}
```

```bash
node belge.mjs "POST /odunc"
```

```
## POST /odunc
 istek gövdesi:
  uye          zorunlu      string, desen: ^U-\d{4}$
  kalemler     zorunlu      array
    isbn         zorunlu      string, desen: ^97[89]-\d{10}$
    (tanımda olmayan alan kabul edilmez)
  sube         isteğe bağlı string, değerler: merkez|sahil|tepe
  (tanımda olmayan alan kabul edilmez)
 yanıt 201:
  id           zorunlu      string, desen: ^O-\d+$
  uye          zorunlu      string
  kalemler     zorunlu      array
    isbn         zorunlu      string
    (tanımda olmayan alan kabul edilmez)
  iadeTarihi   zorunlu      string, desen: ^\d{4}-\d{2}-\d{2}$
  durum        zorunlu      string, değerler: acik|kapali
  (tanımda olmayan alan kabul edilmez)
 yanıt 422:
  type         zorunlu      string
  title        zorunlu      string
  status       zorunlu      integer
  detail       zorunlu      string
  instance     zorunlu      string
```

#### Yanıtın Tanıma Karşı Denetlenmesi

İsteği doğrulamak yaygın bir uygulamadır. Asıl sapma, sunucunun **ürettiği** yanıtta
oluşur: bir alan eklenir, bir değer kümesi genişler, bir alan bazı durumlarda dönmemeye
başlar. Bunların hiçbiri isteği doğrulayarak yakalanmaz.

Aşağıdaki sunucu yanıtı yazan tek bir noktaya sahiptir ve o noktada yanıtı da tanıma karşı
sınar. Sapmalar bir listeye kaydedilir ve `/denetim` yolundan okunur. İçine bilerek bir
sapma konmuştur: `O-1` kaydı için tanımda olmayan bir alan ve tanımda olmayan bir durum
değeri üretilir.

```js
// sunucu.mjs — istegi tanimdan uretilen dogrulayiciyla siner, yaniti da tanima karsi denetler
import { createServer } from "node:http";
import { TANIM } from "./tanim.mjs";
import { dogrulayiciUret } from "./dogrulayici.mjs";

// Tanimdaki her sema icin bir kez dogrulayici uretilir.
const ISTEK_DOGRULAYICI = Object.fromEntries(
  Object.entries(TANIM).filter(([, t]) => t.istek).map(([a, t]) => [a, dogrulayiciUret(t.istek)]));
const YANIT_DOGRULAYICI = Object.fromEntries(
  Object.entries(TANIM).flatMap(([a, t]) => Object.entries(t.yanit).map(([k, s]) => [`${a} ${k}`, dogrulayiciUret(s)])));

const sapmalar = [];   // yanit ile tanimin ayristigi yerler

const oduncler = new Map([["O-1", { id: "O-1", uye: "U-1001", kalemler: [{ isbn: "978-0262033848" }], iadeTarihi: "2026-03-20", durum: "acik" }]]);
let sayac = 1;

const govdeOku = (istek) =>
  new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); });

// Yanit yazan tek nokta: tanima karsi denetim burada yapilir.
const yanitla = (yanit, anahtar, kod, govde, tip = "application/json") => {
  const dogrula = YANIT_DOGRULAYICI[`${anahtar} ${kod}`];
  const hatalar = dogrula ? dogrula(govde) : ["tanımda bu yanıt kodu yok"];
  if (hatalar.length) sapmalar.push({ anahtar, kod, hatalar });
  yanit.writeHead(kod, { "content-type": `${tip}; charset=utf-8` });
  yanit.end(JSON.stringify(govde));
};

const sorun = (tur, baslik, kod, ayrinti) =>
  ({ type: `https://ornek.kutuphane/sorunlar/${tur}`, title: baslik, status: kod, detail: ayrinti, instance: `ol-${sayac++}` });

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

  if (yol === "/denetim") {
    yanit.writeHead(200, { "content-type": "application/json; charset=utf-8" });
    return yanit.end(JSON.stringify(sapmalar, null, 1));
  }

  if (istek.method === "POST" && yol === "/odunc") {
    const anahtar = "POST /odunc";
    const govde = JSON.parse((await govdeOku(istek)) || "{}");
    const hatalar = ISTEK_DOGRULAYICI[anahtar](govde);
    if (hatalar.length) {
      const g = sorun("dogrulama", "İstek gövdesi doğrulanamadı", 422, hatalar.join("; "));
      return yanitla(yanit, anahtar, 422, g, "application/problem+json");
    }
    const kayit = { id: `O-${oduncler.size + 1}`, uye: govde.uye, kalemler: govde.kalemler, iadeTarihi: "2026-04-15", durum: "acik" };
    oduncler.set(kayit.id, kayit);
    return yanitla(yanit, anahtar, 201, kayit);
  }

  if (istek.method === "GET" && yol.startsWith("/odunc/")) {
    const anahtar = "GET /odunc/{id}";
    const kayit = oduncler.get(yol.slice("/odunc/".length));
    if (!kayit) return yanitla(yanit, anahtar, 404, sorun("kaynak-yok", "Kaynak bulunamadı", 404, `${yol} yok.`), "application/problem+json");
    // SAPMA: gecikmis kayitlar icin tanimda olmayan alan ve deger uretiliyor.
    const cikti = kayit.id === "O-1"
      ? { ...kayit, durum: "gecikmis", gecikmeGunu: 12 }
      : kayit;
    return yanitla(yanit, anahtar, 200, cikti);
  }

  yanit.writeHead(404, { "content-type": "application/problem+json; charset=utf-8" });
  yanit.end(JSON.stringify(sorun("kaynak-yok", "Kaynak bulunamadı", 404, `${yol} yok.`)));
}).listen(8437, "127.0.0.1", () => console.log("sunucu 127.0.0.1:8437"));
```

```bash
#!/usr/bin/env bash
# Gecerli istek, gecersiz istek ve tanimla ayrisan yanit; sonra denetim dokumu.
node sunucu.mjs & s=$!
sleep 0.5

echo "--- gecerli istek ---"
curl -sS -w '  [%{http_code}]\n' -X POST -H 'content-type: application/json' \
  -d '{"uye":"U-1002","kalemler":[{"isbn":"978-0201896831"}],"sube":"sahil"}' http://127.0.0.1:8437/odunc

echo "--- gecersiz istek ---"
curl -sS -w '  [%{http_code}]\n' -X POST -H 'content-type: application/json' \
  -d '{"uye":"1002","kalemler":[{"isbn":"0201896831","adet":2}],"sube":"deniz","not":"acele"}' http://127.0.0.1:8437/odunc

echo "--- tanimla ayrisan yanit ---"
curl -sS -w '  [%{http_code}]\n' http://127.0.0.1:8437/odunc/O-1

echo "--- denetim ---"
curl -sS http://127.0.0.1:8437/denetim

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

```
sunucu 127.0.0.1:8437
--- gecerli istek ---
{"id":"O-2","uye":"U-1002","kalemler":[{"isbn":"978-0201896831"}],"iadeTarihi":"2026-04-15","durum":"acik"}  [201]
--- gecersiz istek ---
{"type":"https://ornek.kutuphane/sorunlar/dogrulama","title":"İstek gövdesi doğrulanamadı","status":422,"detail":"/not: tanımda olmayan alan; /uye: \"1002\" desene uymuyor; /kalemler/0/adet: tanımda olmayan alan; /kalemler/0/isbn: \"0201896831\" desene uymuyor; /sube: \"deniz\" izin verilen değerlerde yok","instance":"ol-1"}  [422]
--- tanimla ayrisan yanit ---
{"id":"O-1","uye":"U-1001","kalemler":[{"isbn":"978-0262033848"}],"iadeTarihi":"2026-03-20","durum":"gecikmis","gecikmeGunu":12}  [200]
--- denetim ---
[
 {
  "anahtar": "GET /odunc/{id}",
  "kod": 200,
  "hatalar": [
   "/gecikmeGunu: tanımda olmayan alan",
   "/durum: \"gecikmis\" izin verilen değerlerde yok"
  ]
 }
]
```

Geçersiz istek beş ayrı ihlalle reddedildi ve hiçbir kural elle yazılmadı; hepsi tanımdan
geldi. Asıl önemli olan üçüncü satırdır: sunucu tanımda olmayan bir alan ve tanımda olmayan
bir durum değeri içeren bir yanıt üretti, istemci bu yanıtı **200 kodu ve tam gövdeyle**
aldı, ama sapma kayda geçti.

Bu, bir önceki dersteki kırıcılık sınıflandırmasının çalışma anındaki karşılığıdır.
`gecikmis` değerinin eklenmesi, yanıt yönünde değer kümesinin genişlemesidir ve hoşgörü
bildirimi yoksa kırıcıdır. Fark alıcı bunu iki şema karşılaştırıldığında bulur; buradaki
denetim ise şema hiç güncellenmeden kodun değiştiği durumu bulur. İkisi farklı kaçakları
kapatır: biri bilinçli değişikliği sınıflandırır, diğeri bilinçsiz değişikliği görünür
kılar.

#### Denetimin Engelleyici Olmaması

Yanıt tanımdan saptığında iki davranış seçilebilir. Denetim **engelleyici** olursa sunucu
kendi yanıtını reddedip 500 üretir; **kaydedici** olursa yanıtı gönderir ve sapmayı
kütüğe yazar. Yukarıdaki sunucu ikincisini yapar.

Seçim, sapmanın kime zarar verdiğine bakar. Tanımda olmayan bir alanın eklenmesi çoğu
istemci için zararsızdır; o yanıtı engellemek, çalışan bir işlevi kullanılamaz hâle
getirir. Buna karşılık geliştirme ve sınama ortamlarında engelleyici kip yerindedir: sapma
üretim ortamına ulaşmadan kırılır. Aynı denetim, ortama göre iki farklı davranışla
çalıştırılır.

Kaydedici kipin bir koşulu vardır: sapma listesinin okunması. Kimsenin bakmadığı bir kütüğe
yazılan sapma, hiç saptanmamış sapmayla aynı şeydir.

#### Özet

- Sözleşme düzyazıyla yazıldığında makineye verilemez ve gerçeklikle karşılaştırılamaz;
  şema olarak yazıldığında ikisi de yapılabilir.
- Gövde şemaları JSON Schema ile, yol/yöntem/yanıt düzeni arayüz tanım biçimlerinin
  yaptığı işi yapan bir sarmalla yazılır ve hata yanıtlarını da kapsar.
- Aynı tanımdan hem doğrulayıcı hem insan okur belge üretildiğinde belgelendirme sapması
  yapısal olarak olanaksızlaşır; alan adları da tek kaynaktan gelir.
- Yalnız isteği doğrulamak bir boşluk bırakır: sapma çoğunlukla sunucunun ürettiği yanıtta
  oluşur ve ancak yanıt da tanıma karşı sınanırsa yakalanır.
- Yanıt denetimi, şema hiç güncellenmeden kodun değiştiği durumu bulur; fark alıcı ise
  bilinçli olarak değiştirilmiş iki şemayı sınıflandırır.
- Denetim üretim ortamında kaydedici, geliştirme ve sınama ortamında engelleyici
  çalıştırılır; kaydedici kip yalnızca sapma listesi okunuyorsa iş görür.

#### Sonraki Adım

Tanım artık sunucunun gerçekten ne ürettiğini denetliyor, ama tek bir soruyu yanıtsız
bırakıyor: tüketicinin gerçekten **kullandığı** alanlar hangileri? Tanım `kalemler`
dizisinin her öğesinde `isbn` bulunduğunu söyler; raf terminali bu alanı okuyor mudur,
yoksa yalnız `id` ve `durum` mu ona yetiyor? Bu bilinmeden hangi değişikliğin kimi
bozacağı kestirilemez ve her değişiklik en kötü duruma göre planlanır. Sonraki ders
tüketici beklentilerini dosyaya yazar, sağlayıcıya karşı çalıştırır ve sağlayıcı kırıcı bir
değişiklik yaptığında hangi tüketicinin sınamasının düştüğünü gösterir.

### Sözleşme Testleri

Source: https://academia.sh/tr/kurslar/api-tasarimi/sozlesme-testleri

Tüketici beklentilerinin dosyaya yazılması, sağlayıcıya karşı çalıştırılması, kırıcı bir değişiklikte hangi tüketicinin sınamasının düştüğünün gösterilmesi ve etki alanının değişiklikten önce hesaplanması.

Makine okunur tanım, sunucunun ne ürettiğini denetliyor ama tüketicinin ne **kullandığını**
bilmiyor. Tanım ödünç kaydının beş alanı olduğunu söyler; raf terminali bunların hepsini mi
okuyor, yoksa yalnız ikisini mi? Bu bilinmeden her değişiklik en kötü duruma göre planlanır
ve hiçbir alan güvenle kaldırılamaz.

Sözleşme testleri bu boşluğu kapatır. Her tüketici, sağlayıcıdan ne beklediğini yazar;
sağlayıcı bu beklentileri kendi sınama takımının parçası olarak çalıştırır. Beklentiler
tüketicinin **gerçek kullanımından** türetildiği için tanımdan farklı bir bilgi taşır: tanım
neyin verildiğini, beklenti neyin kullanıldığını söyler.

#### Tüketici Beklentisi

Beklenti dosyası tüketicinin adını, yaptığı istekleri ve o isteklerden okuduğu alanları
içerir. Ödünç servisinin iki tüketicisi vardır: raflardaki barkod terminali ve üyelerin
telefon uygulaması.

```json
{
  "tuketici": "raf-terminali",
  "etkilesimler": [
    {
      "ad": "kaydın kimliği ve durumu okunur",
      "istek": { "yontem": "GET", "yol": "/odunc/O-1" },
      "beklenen": { "kod": 200, "alanlar": { "/id": "string", "/durum": "string" } }
    }
  ]
}
```

```json
{
  "tuketici": "mobil-uygulama",
  "etkilesimler": [
    {
      "ad": "ödünç kaydı ekranda gösterilir",
      "istek": { "yontem": "GET", "yol": "/odunc/O-1" },
      "beklenen": { "kod": 200, "alanlar": { "/uye": "string", "/kalemler/0/isbn": "string", "/iadeTarihi": "string" } }
    },
    {
      "ad": "geçersiz üye kimliği alan hatasıyla reddedilir",
      "istek": { "yontem": "POST", "yol": "/odunc", "govde": { "uye": "1001", "kalemler": [{ "isbn": "978-0262033848" }] } },
      "beklenen": { "kod": 422, "alanlar": { "/type": "string", "/errors/0/yol": "string", "/errors/0/kod": "string" } }
    }
  ]
}
```

Beklentinin ne **içermediği** en az içerdiği kadar önemlidir. Raf terminali `uye` ve
`kalemler` alanlarını hiç yazmamıştır, çünkü okumamaktadır. Mobil uygulama `id` alanını
yazmamıştır. Beklenti dosyası "yanıt şu alanları içerir" demez, "bu tüketici şu alanlara
bağımlıdır" der. Fark, sağlayıcının kaldırdığı bir alanın kimi bozacağını belirler.

Mobil uygulamanın ikinci beklentisi hata yolunu da kapsıyor. Hata sözleşmesi de
sözleşmenin parçasıdır: 422 yanıtının `errors` dizisinde yol ve kod alanlarının bulunması,
o uygulamanın hataları giriş kutularının yanında gösterebilmesinin koşuludur.

#### Beklentilerin Sağlayıcıya Karşı Çalıştırılması

Sağlayıcı iki sürümde çalışabilir: v1 ve kırıcı değişikliğin yapıldığı v2. Kırıcı
değişiklik `iadeTarihi` alanının `sonTarih` olması ve `uye` alanının nesneye dönmesidir.

```js
// saglayici.mjs — odunc servisi saglayicisi
// Kullanim: node saglayici.mjs <port> [v2]
//   v2: iadeTarihi alani sonTarih olur, uye nesneye doner (kirici degisiklik)
import { createServer } from "node:http";

const PORT = Number(process.argv[2] ?? 8438);
const V2 = process.argv[3] === "v2";

const KAYIT = { id: "O-1", uye: "U-1001", kalemler: [{ isbn: "978-0262033848" }], iadeTarihi: "2026-03-20", durum: "acik" };

const disaVer = (k) => V2
  ? { id: k.id, uye: { kimlik: k.uye }, kalemler: k.kalemler, sonTarih: k.iadeTarihi, durum: k.durum }
  : { ...k };

const govdeOku = (istek) =>
  new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); });

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const yol = istek.url.split("?")[0];
  const json = (kod, g, tip = "application/json") => {
    yanit.writeHead(kod, { "content-type": `${tip}; charset=utf-8` });
    yanit.end(JSON.stringify(g));
  };

  if (istek.method === "GET" && yol === "/odunc/O-1") return json(200, disaVer(KAYIT));

  if (istek.method === "POST" && yol === "/odunc") {
    const g = JSON.parse((await govdeOku(istek)) || "{}");
    if (!/^U-\d{4}$/.test(g.uye ?? "")) {
      return json(422, { type: "https://ornek.kutuphane/sorunlar/dogrulama", title: "İstek gövdesi doğrulanamadı",
        status: 422, detail: "Üye kimliği biçimi geçersiz.", instance: "ol-0001",
        errors: [{ yol: "/uye", kod: "bicim" }] }, "application/problem+json");
    }
    return json(201, disaVer({ ...KAYIT, id: "O-2", uye: g.uye, kalemler: g.kalemler ?? [] }));
  }

  json(404, { type: "https://ornek.kutuphane/sorunlar/kaynak-yok", title: "Kaynak bulunamadı",
    status: 404, detail: `${yol} yok.`, instance: "ol-0002" }, "application/problem+json");
}).listen(PORT, "127.0.0.1", () => console.log(`saglayici 127.0.0.1:${PORT} ${V2 ? "v2" : "v1"}`));
```

Sınama dosyası beklentileri dizinden okur, sağlayıcıyı ayrı bir süreç olarak başlatır ve
her etkileşimi gerçekten yollar.

```js
// sozlesme.test.mjs — tuketici beklentilerini saglayiciya karsi calistirir
// Kullanim: node --test sozlesme.test.mjs           (saglayici v1)
//           SAGLAYICI_SURUM=v2 node --test sozlesme.test.mjs
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { readdirSync, readFileSync } from "node:fs";

const PORT = 8438;
const TABAN = `http://127.0.0.1:${PORT}`;
let surec;

before(async () => {
  surec = spawn("node", ["saglayici.mjs", String(PORT), process.env.SAGLAYICI_SURUM ?? "v1"], { stdio: "ignore" });
  for (let i = 0; i < 40; i++) {                       // saglayici ayaga kalkana kadar bekle
    try { await fetch(`${TABAN}/odunc/O-1`); return; } catch { await new Promise((c) => setTimeout(c, 50)); }
  }
  throw new Error("sağlayıcı başlatılamadı");
});
after(() => surec.kill());

const oku = (govde, yol) => yol.split("/").slice(1).reduce((d, p) => (d == null ? undefined : d[p]), govde);
const tipi = (d) => (Array.isArray(d) ? "array" : d === null ? "null" : typeof d);

for (const dosya of readdirSync("beklentiler").sort()) {
  const { tuketici, etkilesimler } = JSON.parse(readFileSync(`beklentiler/${dosya}`, "utf8"));
  for (const e of etkilesimler) {
    test(`${tuketici}: ${e.ad}`, async () => {
      const cevap = await fetch(TABAN + e.istek.yol, {
        method: e.istek.yontem,
        headers: e.istek.govde ? { "content-type": "application/json" } : {},
        body: e.istek.govde ? JSON.stringify(e.istek.govde) : undefined,
      });
      assert.equal(cevap.status, e.beklenen.kod, "durum kodu");
      const govde = await cevap.json();
      for (const [yol, beklenenTip] of Object.entries(e.beklenen.alanlar)) {
        const deger = oku(govde, yol);
        assert.notEqual(deger, undefined, `${yol} alanı yanıtta yok`);
        assert.equal(tipi(deger), beklenenTip, `${yol} alanının tipi`);
      }
    });
  }
}
```

```bash
node --test sozlesme.test.mjs 2>&1 | head -8
```

```
✔ mobil-uygulama: ödünç kaydı ekranda gösterilir (59.873209ms)
✔ mobil-uygulama: geçersiz üye kimliği alan hatasıyla reddedilir (3.696709ms)
✔ raf-terminali: kaydın kimliği ve durumu okunur (0.830209ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
```

Süreler makineye göre değişir. Şimdi sağlayıcı kırıcı değişikliği yapar:

```bash
SAGLAYICI_SURUM=v2 node --test sozlesme.test.mjs 2>&1 | head -20
```

```
✖ mobil-uygulama: ödünç kaydı ekranda gösterilir (60.630959ms)
✔ mobil-uygulama: geçersiz üye kimliği alan hatasıyla reddedilir (4.205875ms)
✔ raf-terminali: kaydın kimliği ve durumu okunur (2.19525ms)
ℹ tests 3
ℹ suites 0
ℹ pass 2
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 127.002333

✖ failing tests:

test at sozlesme.test.mjs:28:5
✖ mobil-uygulama: ödünç kaydı ekranda gösterilir (60.630959ms)
  AssertionError [ERR_ASSERTION]: /uye alanının tipi
  
  'object' !== 'string'
```

Üç sınamadan biri düştü. Düşen sınama hangi **tüketicinin** hangi **etkileşiminin** hangi
**alanında** bozulduğunu adıyla söylüyor. Raf terminalinin sınaması geçti, çünkü değişen
alanları okumuyor.

Bu, tanıma karşı doğrulamanın veremeyeceği bir bilgidir. Tanım denetimi "yanıt tanımdan
saptı" der ve durur; sözleşme sınaması "mobil uygulamanın ödünç ekranı bozuldu, raf
terminali etkilenmedi" der. İlki bir kural ihlalini, ikincisi bir sonucu bildirir.

#### Etkinin Değişiklikten Önce Hesaplanması

Sınamanın düşmesi için değişikliğin yapılmış olması gerekir. Beklenti dosyaları
değişiklikten **önce** de okunabilir: hangi alanın hangi tüketici tarafından okunduğu zaten
yazılıdır.

```js
// etki.mjs — beklenti dosyalarindan, hangi degisikligin hangi tuketiciyi bozacagini cikarir
import { readdirSync, readFileSync } from "node:fs";

// Tuketici -> bagimli oldugu alan yollari
const bagimlilik = new Map();
for (const dosya of readdirSync("beklentiler").sort()) {
  const { tuketici, etkilesimler } = JSON.parse(readFileSync(`beklentiler/${dosya}`, "utf8"));
  const yollar = etkilesimler.flatMap((e) => Object.keys(e.beklenen.alanlar).map((y) => `${e.istek.yol} ${y}`));
  bagimlilik.set(tuketici, new Set(yollar));
}

// Tanimin GET /odunc/O-1 yanitinda bildirdigi butun alanlar
const TANIMLI = ["/id", "/uye", "/kalemler/0/isbn", "/iadeTarihi", "/durum"].map((y) => `/odunc/O-1 ${y}`);

console.log("alan                           bağımlı tüketiciler");
for (const yol of TANIMLI) {
  const bagli = [...bagimlilik].filter(([, k]) => k.has(yol)).map(([t]) => t);
  console.log(`${yol.replace("/odunc/O-1 ", "").padEnd(30)} ${bagli.length ? bagli.join(", ") : "— (kimse okumuyor)"}`);
}

// Onerilen degisiklik: iadeTarihi kalkiyor, uye nesneye donuyor.
const DEGISIKLIK = ["/odunc/O-1 /iadeTarihi", "/odunc/O-1 /uye"];
const etkilenen = [...bagimlilik].filter(([, k]) => DEGISIKLIK.some((d) => k.has(d))).map(([t]) => t);

console.log(`\nönerilen değişiklik: ${DEGISIKLIK.map((d) => d.split(" ")[1]).join(", ")}`);
console.log(`etkilenen tüketici: ${etkilenen.length}/${bagimlilik.size}  (${etkilenen.join(", ")})`);
console.log(`hiç okunmayan alan sayısı: ${TANIMLI.filter((y) => ![...bagimlilik.values()].some((k) => k.has(y))).length}`);
```

```
alan                           bağımlı tüketiciler
/id                            raf-terminali
/uye                           mobil-uygulama
/kalemler/0/isbn               mobil-uygulama
/iadeTarihi                    mobil-uygulama
/durum                         raf-terminali

önerilen değişiklik: /iadeTarihi, /uye
etkilenen tüketici: 1/2  (mobil-uygulama)
hiç okunmayan alan sayısı: 0
```

Bu tablo, kullanımdan kaldırma dersindeki telemetriyle aynı işi alan düzeyinde yapar.
Telemetri hangi tüketicinin hangi **sürümü** çağırdığını sayıyordu; beklenti dosyaları
hangi tüketicinin hangi **alanı** okuduğunu söylüyor. İkisi birlikte, bir değişikliğin
maliyetini tahmin değil hesap konusu yapar.

Son satır ayrıca bir uyarıdır. "Hiç okunmayan alan sayısı: 0" güven verici görünür ama
yalnızca **beklenti dosyası yazmış** tüketiciler için doğrudur. Bir alanın kimse
tarafından okunmadığı sonucu, ancak bütün tüketiciler beklentilerini yazdığında geçerlidir.
Sözleşme sınamalarının en kırılgan yeri budur: eksik beklenti, olmayan bağımlılık gibi
görünür.

Bu yüzden sözleşme sınaması iki tarafın da katıldığı bir düzendir. Tüketici beklentisini
yazar ve sağlayıcının deposuna verir; sağlayıcı bu dosyaları kendi sınama takımında
çalıştırır. Beklenti yazmayan tüketici, sağlayıcının kararlarında görünmez ve kırıldığında
haber vermekten başka bir gücü kalmaz. Kütüphanenin kendi raf terminali için bu düzenin
kurulması kolaydır; dışarıdaki ilçe kütüphanelerinin uygulamaları için beklenti toplamak,
düzenin en zor parçasıdır.

#### Özet

- Makine okunur tanım neyin verildiğini, tüketici beklentisi neyin kullanıldığını söyler;
  bir alanın kaldırılmasının kimi bozacağı ancak ikincisinden bilinir.
- Beklenti dosyasının içermediği alanlar, tüketicinin o alanlara bağımlı olmadığı anlamına
  gelir; hata yanıtının yapısı da beklentinin parçasıdır.
- Beklentiler sağlayıcıya karşı çalıştırıldığında kırıcı değişiklik, hangi tüketicinin
  hangi etkileşiminin hangi alanında bozulduğunu adıyla bildirir.
- Tanım denetimi bir kural ihlalini, sözleşme sınaması bir sonucu bildirir; ikisi farklı
  sorulara yanıt verir.
- Beklenti dosyaları değişiklikten önce de okunabilir ve önerilen bir değişikliğin
  etkilediği tüketici sayısı hesaplanabilir.
- "Kimsenin okumadığı alan" sonucu yalnızca beklentisini yazmış tüketiciler için geçerlidir;
  eksik beklenti, olmayan bağımlılık gibi görünür.

#### Sonraki Adım

Beklenti dosyaları şu ana kadar tek yönlü kullanıldı: tüketici yazdı, sağlayıcı sınadı. Aynı
dosyalar ters yönde de işe yarar. Yeni bir tüketici geliştirilmeye başlandığında sağlayıcı
henüz o uç noktayı yazmamış olabilir; tüketicinin beklemesi ise gereksizdir, çünkü
sözleşme zaten bellidir. Sonraki ders şemadan örnek yanıt üreten bir sunucu yazar,
istemciyi ona karşı geliştirir ve aynı istemci kodunun gerçek sunucuya geçtiğinde
değişmeden çalıştığını gösterir.

### Sahte Sunucular

Source: https://academia.sh/tr/kurslar/api-tasarimi/sahte-sunucular

Şemadan örnek yanıt üreten bir sunucunun yazılması, istemcinin ona karşı geliştirilmesi, aynı kodun gerçek sunucuya geçtiğinde çalıştığının ölçülmesi ve sahte sunucunun gizlediği dalların sayılması.

Beklenti dosyaları şu ana kadar tek yönlü kullanıldı: tüketici yazdı, sağlayıcı sınadı. Aynı
sözleşme ters yönde de iş görür. Yeni bir tüketici geliştirilmeye başlandığında sağlayıcı o
uç noktayı henüz yazmamış olabilir; tüketicinin beklemesi ise gereksizdir, çünkü sözleşme
zaten bellidir.

**Sahte sunucu** (mock server) sözleşmeyi çalıştırılabilir hâle getirir: tanımdaki şemadan
örnek yanıtlar üretir ve gerçek sunucunun yerine geçer. Bu ders böyle bir sunucu yazar,
istemciyi ona karşı geliştirir, sonra aynı kodu gerçek sunucuya bağlayarak iki şeyi ölçer —
yanıtın biçimi aynı mı ve istemcinin hangi dalları hiç çalışmamış.

#### Şemaya Örnek Eklemek

Örnek üretmek her alan için mümkün değildir. Tip bilgisi bir dizgi üretmeye yeter ama
`^U-\d{4}$` desenine uyan bir dizgi üretmeye yetmez. Bu yüzden deseni olan alanlar örnek
taşımak zorundadır; JSON Schema bunun için `examples` sözcüğünü tanımlar.

```js
// tanim.mjs — odunc servisinin makine okunur tanimi (ornek degerlerle)
// Deseni olan her alan bir ornek tasir; sahte sunucu bunlari kullanir.
const ODUNC_ISTEK = {
  type: "object", required: ["uye", "kalemler"], additionalProperties: false,
  properties: {
    uye: { type: "string", pattern: "^U-\\d{4}$", examples: ["U-1001"] },
    kalemler: { type: "array", items: { type: "object", required: ["isbn"], additionalProperties: false,
      properties: { isbn: { type: "string", pattern: "^97[89]-\\d{10}$", examples: ["978-0262033848"] } } } },
    sube: { type: "string", enum: ["merkez", "sahil", "tepe"] },
  },
};

const ODUNC_YANIT = {
  type: "object", required: ["id", "uye", "kalemler", "iadeTarihi", "durum"], additionalProperties: false,
  properties: {
    id: { type: "string", pattern: "^O-\\d+$", examples: ["O-1"] },
    uye: { type: "string", pattern: "^U-\\d{4}$", examples: ["U-1001"] },
    kalemler: { type: "array", items: { type: "object", required: ["isbn"], additionalProperties: false,
      properties: { isbn: { type: "string", pattern: "^97[89]-\\d{10}$", examples: ["978-0262033848"] } } } },
    iadeTarihi: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$", examples: ["2026-04-15"] },
    durum: { type: "string", enum: ["acik", "kapali"] },
  },
};

const SORUN = {
  type: "object", required: ["type", "title", "status", "detail", "instance"],
  properties: {
    type: { type: "string", examples: ["https://ornek.kutuphane/sorunlar/ornek"] },
    title: { type: "string", examples: ["Örnek sorun"] },
    status: { type: "integer", examples: [0] },
    detail: { type: "string", examples: ["Örnek açıklama."] },
    instance: { type: "string", examples: ["ol-0001"] },
    errors: { type: "array", items: { type: "object", required: ["yol", "kod"],
      properties: { yol: { type: "string", examples: ["/uye"] }, kod: { type: "string", examples: ["bicim"] } } } },
  },
};

export const TANIM = {
  "POST /odunc": { istek: ODUNC_ISTEK, yanit: { 201: ODUNC_YANIT, 422: SORUN } },
  "GET /odunc/{id}": { istek: null, yanit: { 200: ODUNC_YANIT, 404: SORUN } },
};
```

```js
// ornek.mjs — semadan ornek deger uretir
// Kural: enum varsa ilk deger, examples varsa ilk ornek, desen varsa ornek zorunlu.
export function ornekUret(sema, yol = "") {
  if (sema.enum) return sema.enum[0];
  if (sema.examples?.length) return sema.examples[0];
  if (sema.pattern) throw new Error(`${yol || "/"}: deseni olan alan için examples yazılmamış`);
  switch (sema.type) {
    case "string":  return "metin";
    case "integer": case "number": return 0;
    case "boolean": return false;
    case "array":   return sema.items ? [ornekUret(sema.items, `${yol}/0`)] : [];
    case "object": {
      const nesne = {};
      for (const [ad, alt] of Object.entries(sema.properties ?? {})) nesne[ad] = ornekUret(alt, `${yol}/${ad}`);
      return nesne;
    }
    default: return null;
  }
}
```

Desen varken örnek yoksa üretici hata verir. Bu, sözleşmeye konan yeni bir koşuldur:
**şema, kendisinden örnek üretilebilecek kadar eksiksiz olmalıdır.**

```js
// eksik-ornek.mjs — ornegi olmayan desenli alan sahte sunucuyu ayaga kaldirmaz
import { ornekUret } from "./ornek.mjs";

const TAM = { type: "object", required: ["barkod"],
  properties: { barkod: { type: "string", pattern: "^BK-\\d{6}$", examples: ["BK-004312"] } } };
const EKSIK = { type: "object", required: ["barkod"],
  properties: { barkod: { type: "string", pattern: "^BK-\\d{6}$" } } };

console.log("örnekli şema ->", JSON.stringify(ornekUret(TAM)));
try { ornekUret(EKSIK); } catch (h) { console.log("örneksiz şema ->", h.message); }
```

```
örnekli şema -> {"barkod":"BK-004312"}
örneksiz şema -> /barkod: deseni olan alan için examples yazılmamış
```

#### Sahte Sunucu

Sahte sunucu tanımdaki yolları tanır, örnekleri **bir kez** üretir ve döndürür. Örnekler
başlangıçta üretildiği için eksik örnek sorunu sunucu ayağa kalkarken görülür, ilk istekte
değil.

```js
// sahte.mjs — tanimdan ornek yanit ureten sunucu
// Kullanim: node sahte.mjs <port> [sorun]   "sorun": hata yollarini da uretir
import { createServer } from "node:http";
import { TANIM } from "./tanim.mjs";
import { ornekUret } from "./ornek.mjs";

const PORT = Number(process.argv[2] ?? 8439);
const SORUN_URET = process.argv[3] === "sorun";

// Ornekler bir kez uretilir; deseni olup ornegi olmayan alan burada hata verir.
const ORNEKLER = Object.fromEntries(
  Object.entries(TANIM).flatMap(([a, t]) =>
    Object.entries(t.yanit).map(([k, s]) => [`${a} ${k}`, ornekUret(s)])));

const govdeOku = (istek) =>
  new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); });

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const yol = istek.url.split("?")[0];
  const anahtar = istek.method === "POST" && yol === "/odunc" ? "POST /odunc"
    : istek.method === "GET" && /^\/odunc\/[^/]+$/.test(yol) ? "GET /odunc/{id}" : null;

  if (!anahtar) { yanit.writeHead(404).end(); return; }
  await govdeOku(istek);

  // Hangi durum kodunun uretilecegi: basari, ya da sorun kipinde hata kodu.
  const kodlar = Object.keys(TANIM[anahtar].yanit).map(Number);
  const basari = kodlar.find((k) => k < 400);
  const kod = SORUN_URET && istek.headers["ornek-sorun"] ? Number(istek.headers["ornek-sorun"]) : basari;

  const govde = structuredClone(ORNEKLER[`${anahtar} ${kod}`]);
  if (govde && kod >= 400) govde.status = kod;          // ornek govdeye gercek kod yazilir
  yanit.writeHead(kod, { "content-type": kod >= 400 ? "application/problem+json; charset=utf-8" : "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(govde));
}).listen(PORT, "127.0.0.1", () => console.log(`sahte sunucu 127.0.0.1:${PORT}${SORUN_URET ? " (sorun kipi)" : ""}`));
```

İstemci, sunucu adresini dışarıdan alır. Sahte ve gerçek sunucu arasındaki geçiş bu tek
bağımlılığın değişmesiyle olur; başka hiçbir şey değişmez.

```js
// istemci.mjs — odunc kaydini gosteren istemci; sunucu adresi disaridan verilir
export async function oduncGoster(taban, id, ekBaslik = {}) {
  const cevap = await fetch(`${taban}/odunc/${id}`, { headers: ekBaslik });
  const govde = await cevap.json();
  const alanlar = Object.keys(govde).join(",");
  if (cevap.status === 200)
    return { dal: "başarı", alanlar, satir: `${govde.id} · ${govde.uye} · ${govde.kalemler.length} kalem · ${govde.iadeTarihi} · ${govde.durum}` };
  if (cevap.status === 404) return { dal: "bulunamadı", alanlar, satir: govde.detail };
  if (cevap.status === 422) return { dal: "doğrulama", alanlar, satir: govde.errors.map((h) => `${h.yol}=${h.kod}`).join(", ") };
  return { dal: "bilinmeyen", alanlar, satir: `beklenmeyen kod ${cevap.status}` };
}
```

```js
// gercek.mjs — tanima uyan gercek sunucu
import { createServer } from "node:http";

const KAYITLAR = new Map([["O-1", { id: "O-1", uye: "U-1001", kalemler: [{ isbn: "978-0201896831" }], iadeTarihi: "2026-05-02", durum: "kapali" }]]);

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const id = istek.url.split("?")[0].replace("/odunc/", "");
  const kayit = KAYITLAR.get(id);
  if (kayit) {
    yanit.writeHead(200, { "content-type": "application/json; charset=utf-8" });
    return yanit.end(JSON.stringify(kayit));
  }
  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: `${id} numaralı ödünç kaydı yok.`, instance: "ol-0007" }));
}).listen(8440, "127.0.0.1", () => console.log("gercek sunucu 127.0.0.1:8440"));
```

#### Aynı Kod, Üç Sunucu

Aşağıdaki koşucu istemciyi verilen adrese karşı çalıştırır, dönen gövdenin alan kümesini
yazar ve istemcinin kaç ayrı dalının çalıştığını sayar.

```js
// kosu.mjs — ayni istemci kodunu verilen adrese karsi calistirir, hangi dallarin
// calistigini ve yanitin bicimini bildirir.
// Kullanim: node kosu.mjs <taban-adres> [sorun]
import { oduncGoster } from "./istemci.mjs";

const TABAN = process.argv[2];
const SORUN = process.argv[3] === "sorun";

const DURUMLAR = SORUN
  ? [["var olan kayıt", "O-1", {}], ["olmayan kayıt", "O-9", { "ornek-sorun": "404" }]]
  : [["var olan kayıt", "O-1", {}], ["olmayan kayıt", "O-9", {}]];

const dallar = new Set();
for (const [ad, id, baslik] of DURUMLAR) {
  const s = await oduncGoster(TABAN, id, baslik);
  dallar.add(s.dal);
  console.log(`${ad.padEnd(16)} dal=${s.dal.padEnd(12)} ${s.satir}`);
  console.log(`${"".padEnd(16)} alanlar: ${s.alanlar}`);
}
console.log(`çalışan dal sayısı: ${dallar.size}/4  (${[...dallar].join(", ")})`);
```

```bash
#!/usr/bin/env bash
# Ayni istemci kodu: once sahte sunucuya, sonra gercek sunucuya.
node sahte.mjs 8439 & sahte=$!
node gercek.mjs & gercek=$!
sleep 0.6

echo "== sahte sunucu (yalniz basari ornekleri) =="
node kosu.mjs http://127.0.0.1:8439
kill "$sahte"; wait "$sahte" 2>/dev/null

node sahte.mjs 8439 sorun & sahte=$!
sleep 0.5
echo "== sahte sunucu (sorun kipi) =="
node kosu.mjs http://127.0.0.1:8439 sorun
kill "$sahte"; wait "$sahte" 2>/dev/null

echo "== gercek sunucu =="
node kosu.mjs http://127.0.0.1:8440
kill "$gercek"; wait "$gercek" 2>/dev/null
```

```
gercek sunucu 127.0.0.1:8440
sahte sunucu 127.0.0.1:8439
== sahte sunucu (yalniz basari ornekleri) ==
var olan kayıt   dal=başarı       O-1 · U-1001 · 1 kalem · 2026-04-15 · acik
                 alanlar: id,uye,kalemler,iadeTarihi,durum
olmayan kayıt    dal=başarı       O-1 · U-1001 · 1 kalem · 2026-04-15 · acik
                 alanlar: id,uye,kalemler,iadeTarihi,durum
çalışan dal sayısı: 1/4  (başarı)
sahte sunucu 127.0.0.1:8439 (sorun kipi)
== sahte sunucu (sorun kipi) ==
var olan kayıt   dal=başarı       O-1 · U-1001 · 1 kalem · 2026-04-15 · acik
                 alanlar: id,uye,kalemler,iadeTarihi,durum
olmayan kayıt    dal=bulunamadı   Örnek açıklama.
                 alanlar: type,title,status,detail,instance,errors
çalışan dal sayısı: 2/4  (başarı, bulunamadı)
== gercek sunucu ==
var olan kayıt   dal=başarı       O-1 · U-1001 · 1 kalem · 2026-05-02 · kapali
                 alanlar: id,uye,kalemler,iadeTarihi,durum
olmayan kayıt    dal=bulunamadı   O-9 numaralı ödünç kaydı yok.
                 alanlar: type,title,status,detail,instance
çalışan dal sayısı: 2/4  (başarı, bulunamadı)
```

Üçüncü bölüm asıl iddiayı doğruluyor: sahte sunucuya karşı geliştirilen istemci kodu,
gerçek sunucuya bağlandığında **değişmeden** çalıştı. Başarı gövdesinin alan kümesi iki
sunucuda birebir aynı; değerler farklı, biçim aynı. İstemcinin gerçek sunucuya taşınması
için değişen tek şey adres oldu.

Birinci bölüm sahte sunucunun tuzağını gösteriyor. Yalnız başarı örnekleri üreten sahte
sunucu, olmayan bir kaydı sorduğunuzda bile 200 döndürür. İstemcinin dört dalından yalnız
biri çalışır ve geliştirici hata yollarını hiç görmeden işini bitmiş sayar. Bu, gerçek
sunucuya geçişte en sık görülen kırılma nedenidir: eksik olan gövde biçimi değil, hiç
yazılmamış hata dalıdır.

İkinci bölüm çözümü veriyor. Sahte sunucu hangi durum kodunu üreteceğini istekten
öğrenebildiğinde, istemcinin bütün yolları geliştirme sırasında çalıştırılabilir. Çalışan
dal sayısı bire karşı iki oldu ve gerçek sunucuyla aynı sayıya ulaştı. Sözleşmede tanımlı
her durum kodunun sahte sunucudan istenebilmesi, sahte sunucuyu bir gösteri aracı olmaktan
çıkarıp bir geliştirme aracı yapar.

#### Sahte Sunucunun Cömertliği

İkinci ve üçüncü bölümdeki 404 gövdelerinin alan kümeleri farklı: sahte sunucu `errors`
alanını da üretmiş, gerçek sunucu üretmemiş. İkisi de sözleşmeye uygundur, çünkü `errors`
zorunlu alanlar arasında değildir.

Buradaki risk tek yönlüdür. Sahte sunucu **isteğe bağlı alanları da** ürettiği için,
istemci onların her zaman geleceğini varsayabilir. Gerçek sunucuya geçişte o varsayım
çöker. Ters yönde bir risk yoktur: sahte sunucunun ürettiği zorunlu alanlar gerçek
sunucuda da vardır.

Bu yüzden örnek üreticinin isteğe bağlı alanları üretip üretmeyeceği bir seçenek olmalıdır.
İki kipte de çalıştırılan bir istemci, yalnız zorunlu alanlarla da ayakta kalabildiğini
göstermiş olur — sözleşmenin gerçekten söylediği budur.

#### Özet

- Sahte sunucu, sözleşmeyi çalıştırılabilir hâle getirir ve tüketicinin sağlayıcıyı
  beklemeden geliştirmesini sağlar.
- Deseni olan alanlardan örnek üretilemez; şema, kendisinden örnek üretilebilecek kadar
  eksiksiz olmalıdır ve eksiklik sunucu ayağa kalkarken görülür.
- Aynı istemci kodu sahte ve gerçek sunucuda değişmeden çalışır; başarı gövdesinin alan
  kümesi ikisinde aynıdır, yalnız değerler farklıdır.
- Yalnız başarı örneği üreten sahte sunucu istemcinin dört dalından birini çalıştırır ve
  hata yollarını hiç yazılmamış bırakır.
- Sahte sunucudan istenen durum kodunun seçilebilmesi, çalışan dal sayısını gerçek
  sunucuyla aynı düzeye çıkarır.
- Sahte sunucu isteğe bağlı alanları da ürettiği için istemciyi onların hep geleceğine
  inandırabilir; üreticinin yalnız zorunlu alanlarla çalışan bir kipi de bulunmalıdır.

#### Sonraki Adım

Bu konu boyunca sözleşme hep aynı biçimde kuruldu: sunucu hangi kaynağı hangi alanlarla
vereceğine karar verir, istemci verilenle yetinir. Kısmi yanıt ve alan seçimi bu katılığı
gevşetmişti ama sınırı sunucu çiziyordu. Sorgu temelli yaklaşım bu ilişkiyi tersine
çevirir: tip sistemi sunucuda tanımlanır, hangi alanların döneceğine **istemci** karar
verir ve yanıt sorgunun biçimini alır. Sonraki konu bu yaklaşımı ayrıntısıyla kurar; ilk
ders şema ve tip sistemini yazar, nesne, skaler, arayüz ve birleşim tiplerini tanımlar ve
tipleri gerçekten çözümleyen küçük bir çalıştırıcı üzerinde gösterir.

## GraphQL Ayrıntısı

Tip sisteminin kurulması, üç işlem türü, alan başına çözücüler, parça ve değişkenlerle sorgu yeniden kullanımı, N+1 sorununun toplu yüklemeyle ölçülen çözümü, imleç temelli bağlantı modeli, alan düzeyi yetkilendirme ve kısmi hata, sorgu maliyeti sınırı ve HTTP aktarımının önbellekleme sonuçları.

### Şema ve Tip Sistemi

Source: https://academia.sh/tr/kurslar/api-tasarimi/sema-ve-tip-sistemi

Nesne, skaler, numaralandırma, arayüz ve birleşim tiplerinin tanımlanması, sorgu belgesinin ayrıştırılması ve şemanın sorguyu çalıştırmadan önce denetleyerek hataları bulması.

Önceki konu boyunca sözleşme hep aynı biçimde kuruldu: sunucu hangi kaynağı hangi alanlarla
vereceğine karar verdi, istemci verilenle yetindi. Kısmi yanıt ve alan seçimi bu katılığı
gevşetmişti ama sınırı yine sunucu çiziyordu.

Sorgu temelli yaklaşım ilişkiyi tersine çevirir. Sunucu bir **tip sistemi** tanımlar; hangi
alanların döneceğine istemci karar verir ve yanıt sorgunun biçimini alır. Bunun karşılığında
sunucu bir güvence ister: istemcinin sorabileceği her şey tipler tarafından önceden
tanımlanmış olmalıdır.

Bu konu boyunca ödünç servisi için küçük bir GraphQL çalıştırıcısı yazılacak. Bu ders
çalıştırıcının ilk iki parçasını kurar — tip sistemi ve ayrıştırıcı — ve üçüncüsünü
gösterir: şemanın sorguyu çalıştırmadan önce denetlemesi.

#### Beş Tip Türü

Şema, adlandırılmış tiplerden oluşan bir kayıttır. GraphQL altı adlandırılmış tür tanımlar;
altıncısı, argüman nesnelerini bildiren **girdi nesnesi** tipidir ve bu kurs onu kullanmaz —
argümanlar skaler kalır. Kalan beş tür yeter.

- **Skaler**, bölünmeyen değerdir: kimlik, dizgi, sayı, tarih. Alt seçim alamaz.
- **Numaralandırma** (enum), sabit bir değer kümesidir. Skaler gibi yapraktır.
- **Nesne**, adlandırılmış alanları olan bileşik tiptir. Alt seçim zorunludur.
- **Arayüz**, birden çok nesne tipinin paylaştığı alanları bildirir. Bir arayüz üzerinden
  yalnız ortak alanlar seçilebilir; tipe özgü alanlar için somut tipe inmek gerekir.
- **Birleşim**, ortak alanı **olmayan** tiplerin bir arada dönmesini sağlar. Birleşim
  üzerinden hiçbir alan doğrudan seçilemez.

Arayüz ile birleşim arasındaki fark, sorgunun neye izin verdiğidir. Ödünç kaydı ile üye
kaydının ikisi de bir kimlik ve oluşturma tarihi taşır; bu ortaklık bir arayüzdür. Arama
sonucu ise kitap veya üye döndürür ve bu ikisinin ortak bir alanı yoktur; bu bir
birleşimdir.

```js
// sema.mjs — odunc servisinin tip sistemi
// Tip turleri: skaler, sirali, nesne, arayuz, birlesim. Alan tipi dizgiyle yazilir:
// "Odunc", "ID!", "[Kalem!]!" — ! zorunlu, [] liste demektir.

export const SEMA = {
  sorgu: "Sorgu",
  mutasyon: "Mutasyon",
  tipler: {
    ID:      { tur: "skaler" },
    String:  { tur: "skaler" },
    Int:     { tur: "skaler" },
    Tarih:   { tur: "skaler" },
    Durum:   { tur: "sirali", degerler: ["ACIK", "KAPALI", "GECIKMIS"] },

    // Arayuz: birden cok tipin ortak alanlarini bildirir.
    Kayit:   { tur: "arayuz", alanlar: { id: "ID!", olusturma: "Tarih!" } },

    Kitap:   { tur: "nesne", alanlar: { isbn: "ID!", baslik: "String!", yazar: "String!", kopya: "Int!" } },
    Uye:     { tur: "nesne", arayuzler: ["Kayit"],
               alanlar: { id: "ID!", olusturma: "Tarih!", ad: "String!",
                          oduncler: { tip: "[Odunc!]!", argumanlar: { durum: "Durum" } } } },
    Kalem:   { tur: "nesne", alanlar: { kitap: "Kitap!", sube: "String!" } },
    Odunc:   { tur: "nesne", arayuzler: ["Kayit"],
               alanlar: { id: "ID!", olusturma: "Tarih!", uye: "Uye!", kalemler: "[Kalem!]!",
                          durum: "Durum!", iadeTarihi: "Tarih" } },

    // Birlesim: ortak alani olmayan tiplerin bir arada donmesi.
    AramaSonucu: { tur: "birlesim", uyeler: ["Kitap", "Uye"] },

    Sorgu:   { tur: "nesne", alanlar: {
                 odunc: { tip: "Odunc", argumanlar: { id: "ID!" } },
                 oduncler: { tip: "[Odunc!]!", argumanlar: { durum: "Durum" } },
                 uye:   { tip: "Uye",   argumanlar: { kimlik: "ID!" } },
                 arama: { tip: "[AramaSonucu!]!", argumanlar: { terim: "String!" } },
                 kayit: { tip: "Kayit", argumanlar: { id: "ID!" } } } },
    Mutasyon: { tur: "nesne", alanlar: {
                 oduncVer: { tip: "Odunc", argumanlar: { uye: "ID!", isbn: "ID!" } },
                 iadeAl:   { tip: "Odunc", argumanlar: { id: "ID!" } } } },
  },
};

// "[Kalem!]!" -> { ad: "Kalem", liste: true, zorunlu: true }
export function tipCoz(dizgi) {
  let s = dizgi, zorunlu = false, liste = false;
  if (s.endsWith("!")) { zorunlu = true; s = s.slice(0, -1); }
  if (s.startsWith("[")) { liste = true; s = s.slice(1, -1); if (s.endsWith("!")) s = s.slice(0, -1); }
  return { ad: s, liste, zorunlu };
}

export const alanTanimi = (tip, ad) => {
  const t = tip.alanlar?.[ad];
  return t === undefined ? null : typeof t === "string" ? { tip: t, argumanlar: {} } : { argumanlar: {}, ...t };
};

// Bir arayuz ya da birlesimin altina dusebilecek somut tipler
export const olasiTipler = (sema, ad) => {
  const t = sema.tipler[ad];
  if (t.tur === "birlesim") return t.uyeler;
  if (t.tur === "arayuz") return Object.entries(sema.tipler).filter(([, x]) => x.arayuzler?.includes(ad)).map(([a]) => a);
  return [ad];
};
```

`Sorgu` ve `Mutasyon` tiplerinin özel bir yanı yoktur; sıradan nesne tipleridir. Onları
başlangıç noktası yapan, şemanın `sorgu` ve `mutasyon` alanlarında adlarının yazılı
olmasıdır. Tip sistemi bir çizgedir ve bu iki ad, çizgeye girilen düğümleri gösterir.

#### Sorgunun Ayrıştırılması

Sorgu bir metindir; denetlenebilmesi için ağaca çevrilmelidir. Aşağıdaki ayrıştırıcı
işlem türlerini, takma adları, argümanları, değişkenleri ve **sorgu parçalarını**
(fragment) tanır; kısa biçimiyle parça denecek.

```js
// ayristirici.mjs — GraphQL sorgu belgesini agaca cevirir
// Desteklenen: islem turleri, takma ad, arguman, degisken, parca ve satir ici parca.

const SIMGE = /\s+|,|#[^\n]*|(\.\.\.|[{}():$=[\]!]|"(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?|[_A-Za-z][_0-9A-Za-z]*)/g;

const simgele = (metin) => [...metin.matchAll(SIMGE)].map((e) => e[1]).filter((s) => s !== undefined);

export function ayristir(metin) {
  const s = simgele(metin);
  let i = 0;
  const bak = () => s[i];
  const al = (beklenen) => {
    if (beklenen !== undefined && s[i] !== beklenen) throw new Error(`beklenen "${beklenen}", gelen "${s[i] ?? "belge sonu"}"`);
    return s[i++];
  };

  const deger = () => {
    if (bak() === "$") { al("$"); return { tur: "degisken", ad: al() }; }
    if (bak() === "[") { al("["); const d = []; while (bak() !== "]") d.push(deger()); al("]"); return { tur: "sabit", deger: d }; }
    const t = al();
    if (t.startsWith('"')) return { tur: "sabit", deger: JSON.parse(t) };
    if (/^-?\d/.test(t)) return { tur: "sabit", deger: Number(t) };
    if (t === "true" || t === "false") return { tur: "sabit", deger: t === "true" };
    return { tur: "sirali", deger: t };                        // sirali tip degeri
  };

  const tipAdi = () => {                                       // Odunc! [Kalem!]! gibi
    let ad;
    if (bak() === "[") { al("["); ad = `[${tipAdi()}]`; al("]"); } else ad = al();
    if (bak() === "!") { al("!"); ad += "!"; }
    return ad;
  };

  const secimKumesi = () => {
    al("{");
    const secim = [];
    while (bak() !== "}") {
      if (bak() === "...") {
        al("...");
        if (bak() === "on") { al("on"); const tip = al(); secim.push({ tur: "satirIci", tip, secim: secimKumesi() }); }
        else secim.push({ tur: "parcaYayimi", ad: al() });
        continue;
      }
      let ad = al(), takma = null;
      if (bak() === ":") { al(":"); takma = ad; ad = al(); }
      const argumanlar = {};
      if (bak() === "(") { al("("); while (bak() !== ")") { const a = al(); al(":"); argumanlar[a] = deger(); } al(")"); }
      const alt = bak() === "{" ? secimKumesi() : null;
      secim.push({ tur: "alan", ad, takma: takma ?? ad, argumanlar, secim: alt });
    }
    al("}");
    return secim;
  };

  const islemler = [], parcalar = {};
  while (i < s.length) {
    if (bak() === "fragment") {
      al("fragment"); const ad = al(); al("on"); const tip = al();
      parcalar[ad] = { ad, tip, secim: secimKumesi() };
      continue;
    }
    let tur = "query", ad = null;
    if (["query", "mutation", "subscription"].includes(bak())) { tur = al(); if (bak() !== "(" && bak() !== "{") ad = al(); }
    const degiskenler = [];
    if (bak() === "(") {
      al("(");
      while (bak() !== ")") {
        al("$"); const dad = al(); al(":"); const dtip = tipAdi();
        let varsayilan = undefined;
        if (bak() === "=") { al("="); varsayilan = deger().deger; }
        degiskenler.push({ ad: dad, tip: dtip, varsayilan });
      }
      al(")");
    }
    islemler.push({ tur, ad, degiskenler, secim: secimKumesi() });
  }
  return { islemler, parcalar };
}
```

Ayrıştırıcının ürettiği ağacın tek bilmediği şey **anlamdır**. `gecikmeCezasi` diye bir
alan seçilmişse ayrıştırıcı bunu sorunsuzca ağaca koyar; böyle bir alanın olmadığını
söyleyecek olan tip sistemidir.

#### Şemanın Sorguyu Denetlemesi

Denetleyici, seçim kümesini şemadaki tiplerle birlikte gezer ve her adımda uyum arar.

```js
// denetle.mjs — sorguyu semaya karsi dogrular; calistirmadan once hatalari bulur
import { tipCoz, alanTanimi, olasiTipler } from "./sema.mjs";

const YAPRAK = new Set(["skaler", "sirali"]);

export function denetle(sema, belge) {
  const hatalar = [];
  for (const islem of belge.islemler) {
    const kok = islem.tur === "mutation" ? sema.mutasyon : sema.sorgu;
    const bildirilen = new Set(islem.degiskenler.map((d) => d.ad));
    secimDenetle(sema, belge, kok, islem.secim, kok, bildirilen, hatalar);
  }
  return hatalar;
}

function secimDenetle(sema, belge, tipAdi, secim, yol, bildirilen, hatalar) {
  const tip = sema.tipler[tipAdi];
  for (const s of secim) {
    if (s.tur === "parcaYayimi") {
      const parca = belge.parcalar[s.ad];
      if (!parca) { hatalar.push(`${yol}: "${s.ad}" adlı parça tanımlı değil`); continue; }
      if (!olasiTipler(sema, tipAdi).includes(parca.tip) && parca.tip !== tipAdi)
        hatalar.push(`${yol}: "${s.ad}" parçası ${parca.tip} için, buradaki tip ${tipAdi}`);
      else secimDenetle(sema, belge, parca.tip, parca.secim, `${yol}...${s.ad}`, bildirilen, hatalar);
      continue;
    }
    if (s.tur === "satirIci") {
      if (!olasiTipler(sema, tipAdi).includes(s.tip))
        hatalar.push(`${yol}: ${s.tip}, ${tipAdi} altına düşemez (olası tipler: ${olasiTipler(sema, tipAdi).join(", ")})`);
      else secimDenetle(sema, belge, s.tip, s.secim, `${yol}...${s.tip}`, bildirilen, hatalar);
      continue;
    }

    // Birlesim tipinde dogrudan alan secilemez; once somut tipe inilmelidir.
    if (tip.tur === "birlesim") { hatalar.push(`${yol}: ${tipAdi} bir birleşimdir, "${s.ad}" alanı doğrudan seçilemez`); continue; }

    const tanim = alanTanimi(tip, s.ad);
    if (!tanim) { hatalar.push(`${yol}: ${tipAdi} tipinde "${s.ad}" alanı yok`); continue; }

    for (const a of Object.keys(s.argumanlar)) {
      if (!(a in tanim.argumanlar)) hatalar.push(`${yol}.${s.ad}: "${a}" adlı argüman tanımlı değil`);
    }
    for (const d of Object.values(s.argumanlar)) {
      if (d.tur === "degisken" && !bildirilen.has(d.ad)) hatalar.push(`${yol}.${s.ad}: $${d.ad} değişkeni bildirilmemiş`);
    }
    for (const [a, t] of Object.entries(tanim.argumanlar)) {
      if (t.endsWith("!") && !(a in s.argumanlar)) hatalar.push(`${yol}.${s.ad}: zorunlu "${a}" argümanı verilmemiş`);
    }

    const alanTipi = sema.tipler[tipCoz(tanim.tip).ad];
    const yaprakMi = YAPRAK.has(alanTipi.tur);
    if (yaprakMi && s.secim) hatalar.push(`${yol}.${s.ad}: ${tipCoz(tanim.tip).ad} bir yapraktır, alt seçim alamaz`);
    else if (!yaprakMi && !s.secim) hatalar.push(`${yol}.${s.ad}: ${tipCoz(tanim.tip).ad} bileşiktir, alt seçim zorunludur`);
    else if (!yaprakMi) secimDenetle(sema, belge, tipCoz(tanim.tip).ad, s.secim, `${yol}.${s.ad}`, bildirilen, hatalar);
  }
}
```

```js
// kosu.mjs — bir gecerli, alti gecersiz sorguyu semaya karsi denetler
import { ayristir } from "./ayristirici.mjs";
import { denetle } from "./denetle.mjs";
import { SEMA } from "./sema.mjs";

const SORGULAR = {
  "geçerli sorgu": `
    query OduncEkrani($uye: ID!) {
      uye(kimlik: $uye) { ad oduncler(durum: ACIK) { ...Ozet } }
      arama(terim: "algoritma") { ... on Kitap { baslik } ... on Uye { ad } }
      kayit(id: "O-1") { id olusturma }
    }
    fragment Ozet on Odunc { id durum kalemler { kitap { baslik } } }`,

  "olmayan alan": `{ odunc(id: "O-1") { id gecikmeCezasi } }`,
  "yaprağa alt seçim": `{ odunc(id: "O-1") { durum { ad } } }`,
  "bileşiğe alt seçim yok": `{ odunc(id: "O-1") { id uye } }`,
  "birleşimden doğrudan alan": `{ arama(terim: "x") { baslik } }`,
  "yanlış parça tipi": `{ odunc(id: "O-1") { ...KitapOzeti } } fragment KitapOzeti on Kitap { baslik }`,
  "eksik argüman ve bildirilmemiş değişken": `{ odunc { id } uye(kimlik: $k) { ad } }`,
};

for (const [ad, metin] of Object.entries(SORGULAR)) {
  const hatalar = denetle(SEMA, ayristir(metin));
  console.log(`\n${ad}  (${hatalar.length} hata)`);
  for (const h of hatalar) console.log(`  ${h}`);
}
```

```
geçerli sorgu  (0 hata)

olmayan alan  (1 hata)
  Sorgu.odunc: Odunc tipinde "gecikmeCezasi" alanı yok

yaprağa alt seçim  (1 hata)
  Sorgu.odunc.durum: Durum bir yapraktır, alt seçim alamaz

bileşiğe alt seçim yok  (1 hata)
  Sorgu.odunc.uye: Uye bileşiktir, alt seçim zorunludur

birleşimden doğrudan alan  (1 hata)
  Sorgu.arama: AramaSonucu bir birleşimdir, "baslik" alanı doğrudan seçilemez

yanlış parça tipi  (1 hata)
  Sorgu.odunc: "KitapOzeti" parçası Kitap için, buradaki tip Odunc

eksik argüman ve bildirilmemiş değişken  (2 hata)
  Sorgu.odunc: zorunlu "id" argümanı verilmemiş
  Sorgu.uye: $k değişkeni bildirilmemiş
```

Yedi sorgudan biri geçti, altısı reddedildi ve **hiçbiri çalıştırılmadı**. Reddetmelerin
tamamı yalnızca şemadan gelen bilgiyle yapıldı; veri kaynağına bakılmadı, tek bir çözücü
çağrılmadı.

Bu, kaynak temelli tasarımdan yapısal olarak farklı bir noktadır. Orada bir uç noktanın
neyi kabul ettiği çalışma anında doğrulanır ve tüketici hangi alanların bulunduğunu belgeden
öğrenir. Burada aynı bilgi şemada durur ve sorgu gönderilmeden denetlenebilir; makine
okunur belgeler dersinde ayrıca yazılan tanım, tip sisteminin kendisidir.

#### Tip Sisteminin Kazandırdığı ve İstediği

Arayüz ve birleşim satırları, tip sisteminin en çok iş gördüğü yerlerdir. `kayit(id:)`
alanı bir arayüz döndürür; sorgu ondan yalnız `id` ve `olusturma` alanlarını seçebilir,
çünkü arayüzün bildirdiği alanlar bunlardır. Ödünç kaydına özgü `durum` alanını okumak
isteyen sorgu, `... on Odunc` yazarak somut tipe inmek zorundadır. Birleşimde ise ortak
alan hiç yoktur; her dal kendi tipini bildirmeden hiçbir şey okuyamaz.

Bu katılığın karşılığı, yanıtın biçiminin sorgudan **çıkarılabilmesidir**. İstemci
gönderdiği seçim kümesine bakarak dönecek gövdenin şeklini bilir; sunucunun ayrıca bir
yanıt şeması yayımlamasına gerek kalmaz.

İstenen bedel ise şemanın eksiksiz olmasıdır. Şemada tanımlanmamış hiçbir şey sorulamaz;
"şimdilik ham veriyi olduğu gibi döndürelim" gibi bir kaçış yolu yoktur. Ödünç servisinin
her alanı, her argümanı ve her tip ilişkisi yazılmak zorundadır.

#### Özet

- Şema beş tip türünden kurulur: skaler ve numaralandırma yapraktır, nesne bileşiktir, arayüz
  ortak alanları bildirir, birleşim ortak alanı olmayan tipleri bir arada döndürür.
- Arayüz üzerinden yalnız ortak alanlar okunur; birleşim üzerinden hiçbir alan doğrudan
  okunamaz, her iki durumda da tipe özgü alanlar için somut tipe inilir.
- `Sorgu` ve `Mutasyon` sıradan nesne tipleridir; onları başlangıç noktası yapan, şemada
  adlarının giriş düğümü olarak yazılmış olmasıdır.
- Ayrıştırıcı sorguyu ağaca çevirir ama anlamını bilmez; alanın var olup olmadığını
  söyleyen tip sistemidir.
- Şemaya karşı denetim, sorgu çalıştırılmadan ve tek bir çözücü çağrılmadan yapılır; olmayan
  alan, yaprağa alt seçim, bileşiğe eksik seçim, yanlış parça tipi ve eksik argüman bu
  aşamada yakalanır.
- Tip sisteminin karşılığı yanıt biçiminin sorgudan çıkarılabilmesi, bedeli ise şemada
  tanımlanmamış hiçbir şeyin sorulamamasıdır.

#### Sonraki Adım

Denetlenen sorgu henüz çalıştırılmadı; şema neyin sorulabileceğini söylüyor ama yanıtın
nereden geleceğini söylemiyor. Ayrıca buraya kadar yalnız tek bir işlem türü konuşuldu.
Ödünç vermek bir okuma değil, bir değiştirmedir ve sunucudaki sırayla ilgili farklı bir
güvence ister; bir kitabın durumunun değişmesini beklemek ise ne okuma ne değiştirmedir,
zaman içinde birden çok yanıt üretir. Sonraki ders üç işlem türünü tanımlar, çalıştırıcının
ilk hâlini yazar ve üçünün yürütme kuralı bakımından nerede ayrıldığını gösterir.

### Sorgu, Mutasyon ve Abonelik

Source: https://academia.sh/tr/kurslar/api-tasarimi/sorgu-mutasyon-ve-abonelik

Üç işlem türünün yürütme kuralları, mutasyon alanlarının sıralı yürütülmesinin tek kopyalı kitap üzerinde ölçülen sonucu ve aboneliğin aynı seçim kümesini zaman içinde birden çok kez uygulaması.

Şema neyin sorulabileceğini söylüyor ama yanıtın nereden geleceğini söylemiyor. Bu ders
çalıştırıcıyı yazar ve GraphQL'in üç **işlem türünü** ayıran şeyin ne olduğunu gösterir.

Fark sözdiziminde değildir; üçü de aynı seçim kümesi dilini kullanır. Fark yürütme
kurallarındadır. Sorgu alanları birbirinden bağımsız kabul edilir ve birlikte yürütülebilir.
Mutasyon alanları birbirini etkileyebilir ve **sırayla** yürütülmek zorundadır. Abonelik
tek bir yanıt değil, zaman içinde birden çok yanıt üretir.

#### Çalıştırıcı

Çalıştırıcı seçim kümesini gezer, her alan için bir çözücü çağırır ve dönen değeri alanın
tipine göre tamamlar. Yaprak tipte değer olduğu gibi döner; bileşik tipte alt seçim kümesi
aynı yordamla yürütülür.

```js
// calistirici.mjs — denetlenmis bir belgeyi calistirir
// Sorgu alanlari birlikte, mutasyon alanlari sirayla yurutulur.
import { tipCoz, alanTanimi, olasiTipler } from "./sema.mjs";

const YAPRAK = new Set(["skaler", "sirali"]);

// Degisken bildirimlerini gelen degerlerle birlestirir.
function degiskenleriCoz(islem, gelen) {
  const d = {};
  for (const b of islem.degiskenler) {
    const v = gelen[b.ad] ?? b.varsayilan;
    if (v === undefined && b.tip.endsWith("!")) throw new Error(`$${b.ad} değişkeni zorunlu`);
    d[b.ad] = v;
  }
  return d;
}

const argCoz = (argumanlar, dv) =>
  Object.fromEntries(Object.entries(argumanlar).map(([a, d]) => [a, d.tur === "degisken" ? dv[d.ad] : d.deger]));

// Parcalari ve satir ici parcalari acar; somut tipe uymayan dallari atar.
function duzle(sema, belge, tipAdi, secim, birikim = []) {
  for (const s of secim) {
    if (s.tur === "alan") { birikim.push(s); continue; }
    const parca = s.tur === "parcaYayimi" ? belge.parcalar[s.ad] : s;
    const kosul = s.tur === "parcaYayimi" ? parca.tip : s.tip;
    if (kosul === tipAdi || olasiTipler(sema, kosul).includes(tipAdi)) duzle(sema, belge, tipAdi, parca.secim, birikim);
  }
  return birikim;
}

async function secimCalistir(o, tipAdi, secim, kaynak, sirali = false) {
  const alanlar = duzle(o.sema, o.belge, tipAdi, secim);
  const sonuc = {};
  for (const a of alanlar) sonuc[a.takma] = null;      // anahtar sirasi sorgudan gelir
  const isle = async (a) => { sonuc[a.takma] = await alanCalistir(o, tipAdi, a, kaynak); };
  if (sirali) for (const a of alanlar) await isle(a);
  else await Promise.all(alanlar.map(isle));
  return sonuc;
}

async function alanCalistir(o, tipAdi, alan, kaynak) {
  const tanim = alanTanimi(o.sema.tipler[tipAdi], alan.ad);
  const cozucu = o.cozucular[tipAdi]?.[alan.ad] ?? ((k) => k?.[alan.ad]);
  const deger = await cozucu(kaynak, argCoz(alan.argumanlar, o.dv), o.baglam);
  return tamamla(o, tanim.tip, deger, alan.secim);
}

async function tamamla(o, tipDizgi, deger, secim) {
  if (deger === null || deger === undefined) return null;
  const t = tipCoz(tipDizgi);
  if (t.liste) return Promise.all(deger.map((d) => tamamla(o, t.ad, d, secim)));
  const tip = o.sema.tipler[t.ad];
  if (YAPRAK.has(tip.tur)) return deger;
  // Arayuz ve birlesimde somut tip cozuculerden sorulur.
  const somut = tip.tur === "nesne" ? t.ad : o.cozucular.__tip[t.ad](deger);
  return secimCalistir(o, somut, secim, deger);
}

export async function calistir(sema, cozucular, belge, { islemAdi, degiskenler = {}, baglam = {} } = {}) {
  const islem = islemAdi ? belge.islemler.find((i) => i.ad === islemAdi) : belge.islemler[0];
  const o = { sema, cozucular, belge, dv: degiskenleriCoz(islem, degiskenler), baglam };
  const kok = islem.tur === "mutation" ? sema.mutasyon : sema.sorgu;
  return { data: await secimCalistir(o, kok, islem.secim, null, islem.tur === "mutation") };
}

// Abonelik: cozucu bir olay akisi dondurur, her olay ayni secim kumesiyle bicimlenir.
export async function* abone(sema, cozucular, belge, { degiskenler = {}, baglam = {} } = {}) {
  const islem = belge.islemler[0];
  const o = { sema, cozucular, belge, dv: degiskenleriCoz(islem, degiskenler), baglam };
  const alan = duzle(sema, belge, sema.abonelik, islem.secim)[0];
  const akis = cozucular[sema.abonelik][alan.ad](null, argCoz(alan.argumanlar, o.dv), baglam);
  const tanim = alanTanimi(sema.tipler[sema.abonelik], alan.ad);
  for await (const olay of akis) yield { data: { [alan.takma]: await tamamla(o, tanim.tip, olay, alan.secim) } };
}
```

`calistir` işlevindeki tek dallanma, işlem türünün mutasyon olup olmadığıdır ve bu dallanma
`secimCalistir` çağrısına `sirali` bayrağı olarak geçer. Kural budur: bütün fark tek bir
`for` döngüsü ile `Promise.all` arasındaki seçimdir.

Çalıştırıcının veriye ulaşması için çözücülere ihtiyacı vardır. Aşağıdaki veri kaynağı
bellek içidir ve ödünç verme işlemi bilerek gerçekçi yazılmıştır: kalan kopya sayısı
**okunur**, bir veritabanı turu kadar beklenir, sonra kayıt **yazılır**.

```js
// veri.mjs — bellek ici odunc verisi ve cozuculer
export const KITAPLAR = new Map([
  ["978-0262033848", { isbn: "978-0262033848", baslik: "Algoritmalara Giris", yazar: "Cormen", kopya: 1 }],
  ["978-0201896831", { isbn: "978-0201896831", baslik: "Programlama Sanati", yazar: "Knuth", kopya: 2 }],
]);
export const UYELER = new Map([
  ["U-1001", { id: "U-1001", olusturma: "2024-02-11", ad: "Ayse Yilmaz" }],
  ["U-1002", { id: "U-1002", olusturma: "2025-06-03", ad: "Deniz Kaya" }],
]);
export const ODUNCLER = new Map();
let sira = 0;

export const bekle = (ms) => new Promise((c) => setTimeout(c, ms));
export const olaylar = [];   // yurutme sirasini gormek icin
export const sifirla = () => { ODUNCLER.clear(); sira = 0; olaylar.length = 0; };

const eldeki = (isbn) => [...ODUNCLER.values()].filter((o) => o.durum === "ACIK" && o.kalemler.some((k) => k.isbn === isbn)).length;

export function oduncVer(uye, isbn, gecikme = 30) {
  return (async () => {
    olaylar.push(`oduncVer(${isbn.slice(-4)}) başladı`);
    const kalan = KITAPLAR.get(isbn).kopya - eldeki(isbn);   // OKUMA
    await bekle(gecikme);                                    // veritabani turu
    if (kalan <= 0) { olaylar.push(`oduncVer(${isbn.slice(-4)}) bitti: kopya yok`); return null; }
    const kayit = { id: `O-${++sira}`, olusturma: "2026-03-01", uyeId: uye,    // YAZMA
                    kalemler: [{ isbn, sube: "merkez" }], durum: "ACIK", iadeTarihi: null };
    ODUNCLER.set(kayit.id, kayit);
    olaylar.push(`oduncVer(${isbn.slice(-4)}) bitti: ${kayit.id}`);
    return kayit;
  })();
}

export const COZUCULER = {
  __tip: { Kayit: (d) => (d.kalemler ? "Odunc" : "Uye"), AramaSonucu: (d) => (d.isbn ? "Kitap" : "Uye") },
  Sorgu: {
    odunc: (_, a) => ODUNCLER.get(a.id) ?? null,
    uye: (_, a) => UYELER.get(a.kimlik) ?? null,
    kayit: (_, a) => ODUNCLER.get(a.id) ?? UYELER.get(a.id) ?? null,
    arama: (_, a) => [...KITAPLAR.values()].filter((k) => k.baslik.toLowerCase().includes(a.terim.toLowerCase())),
  },
  Mutasyon: {
    oduncVer: (_, a) => oduncVer(a.uye, a.isbn),
    iadeAl: (_, a) => { const o = ODUNCLER.get(a.id); if (o) o.durum = "KAPALI"; return o ?? null; },
  },
  Odunc: {
    uye: (o) => UYELER.get(o.uyeId),
    kalemler: (o) => o.kalemler,
  },
  Kalem: { kitap: (k) => KITAPLAR.get(k.isbn) },
  Uye: { oduncler: (u, a) => [...ODUNCLER.values()].filter((o) => o.uyeId === u.id && (!a.durum || o.durum === a.durum)) },
};
```

#### Üç İşlem Türü Yan Yana

Şema ve Tip Sistemi dersinde kurulan şemada iki kök vardı: sorgu ve mutasyon. Abonelik
üçüncü köktür ve şemaya bir tip olarak eklenir; alanları, istemcinin izleyebileceği
olayları adlandırır.

```js
// sema-abonelik.mjs — 01. dersteki semaya abonelik koku eklenmis hali
import { SEMA } from "./sema.mjs";

export const SEMA_A = {
  ...SEMA,
  abonelik: "Abonelik",
  tipler: {
    ...SEMA.tipler,
    Abonelik: {
      tur: "nesne",
      alanlar: { oduncDurumu: { tip: "Odunc!", argumanlar: { id: "ID!" } } },
    },
  },
};
```

```js
// kosu.mjs — uc islem turunu calistirir; sorgu ile mutasyonun yurutme sirasini karsilastirir
import { ayristir } from "./ayristirici.mjs";
import { calistir, abone } from "./calistirici.mjs";
import { SEMA_A as SEMA } from "./sema-abonelik.mjs";
import { COZUCULER, ODUNCLER, olaylar, bekle, sifirla } from "./veri.mjs";

const yaz = (b, d) => console.log(b, JSON.stringify(d));

// 1) SORGU — alanlar birlikte yurutulur
ODUNCLER.set("O-9", { id: "O-9", olusturma: "2026-02-01", uyeId: "U-1001",
  kalemler: [{ isbn: "978-0201896831", sube: "sahil" }], durum: "ACIK", iadeTarihi: "2026-03-20" });

const sorgu = ayristir(`query($id: ID!) {
  odunc(id: $id) { id durum uye { ad } kalemler { kitap { baslik } sube } }
  kayit(id: "U-1002") { id olusturma ... on Uye { ad } }
}`);
yaz("sorgu     ->", (await calistir(SEMA, COZUCULER, sorgu, { degiskenler: { id: "O-9" } })).data);

// 2) MUTASYON — tek kopyali kitabi iki kez odunc vermeyi dene
const mutasyon = ayristir(`mutation {
  birinci: oduncVer(uye: "U-1001", isbn: "978-0262033848") { id durum }
  ikinci:  oduncVer(uye: "U-1002", isbn: "978-0262033848") { id durum }
}`);
sifirla();
yaz("mutasyon  ->", (await calistir(SEMA, COZUCULER, mutasyon)).data);
console.log("  yürütme sırası:", olaylar.join(" | "));

// Ayni iki alan birlikte yurutulseydi:
sifirla();
const paralel = await Promise.all([
  COZUCULER.Mutasyon.oduncVer(null, { uye: "U-1001", isbn: "978-0262033848" }),
  COZUCULER.Mutasyon.oduncVer(null, { uye: "U-1002", isbn: "978-0262033848" }),
]);
console.log("  birlikte yürütülseydi:", olaylar.join(" | "));
console.log("  birlikte yürütmede verilen kopya sayısı:", paralel.filter(Boolean).length, "(kitapta 1 kopya var)");

// 3) ABONELIK — ayni secim kumesi zaman icinde birden cok kez uygulanir
COZUCULER.Abonelik = {
  oduncDurumu: async function* (_, a) {
    for (const d of ["ACIK", "GECIKMIS", "KAPALI"]) {
      await bekle(5);
      const o = ODUNCLER.get(a.id);
      yield { ...o, durum: d };
    }
  },
};
ODUNCLER.set("O-9", { id: "O-9", olusturma: "2026-02-01", uyeId: "U-1001",
  kalemler: [{ isbn: "978-0201896831", sube: "sahil" }], durum: "ACIK", iadeTarihi: "2026-03-20" });
const abonelik = ayristir(`subscription { oduncDurumu(id: "O-9") { id durum uye { ad } } }`);
for await (const olay of abone(SEMA, COZUCULER, abonelik)) yaz("abonelik  ->", olay.data);
```

```
sorgu     -> {"odunc":{"id":"O-9","durum":"ACIK","uye":{"ad":"Ayse Yilmaz"},"kalemler":[{"kitap":{"baslik":"Programlama Sanati"},"sube":"sahil"}]},"kayit":{"id":"U-1002","olusturma":"2025-06-03","ad":"Deniz Kaya"}}
mutasyon  -> {"birinci":{"id":"O-1","durum":"ACIK"},"ikinci":null}
  yürütme sırası: oduncVer(3848) başladı | oduncVer(3848) bitti: O-1 | oduncVer(3848) başladı | oduncVer(3848) bitti: kopya yok
  birlikte yürütülseydi: oduncVer(3848) başladı | oduncVer(3848) başladı | oduncVer(3848) bitti: O-1 | oduncVer(3848) bitti: O-2
  birlikte yürütmede verilen kopya sayısı: 2 (kitapta 1 kopya var)
abonelik  -> {"oduncDurumu":{"id":"O-9","durum":"ACIK","uye":{"ad":"Ayse Yilmaz"}}}
abonelik  -> {"oduncDurumu":{"id":"O-9","durum":"GECIKMIS","uye":{"ad":"Ayse Yilmaz"}}}
abonelik  -> {"oduncDurumu":{"id":"O-9","durum":"KAPALI","uye":{"ad":"Ayse Yilmaz"}}}
```

#### Sorgu: Alanlar Bağımsızdır

Sorgu yanıtındaki iki kök alan birbirini görmez. `odunc` ve `kayit` alanlarının hangi
sırayla çözüldüğü sonucu değiştirmez, çünkü ikisi de yalnız okur. Bu bağımsızlık varsayımı
GraphQL'in kök sorgu alanlarını birlikte yürütmesini meşru kılar ve pratikte belirgin bir
kazançtır: dört bağımsız kök alanı olan bir ekran, dört ayrı isteğin toplam süresini değil
en yavaşının süresini bekler.

Yanıttaki anahtar sırası ise sorgudaki sıradır, çözülme sırası değil. Çalıştırıcı bunu
alanları önceden `null` ile yerleştirerek sağlar. İstemcinin yanıtı sorgudan okuyabilmesi
buna bağlıdır.

`kayit` alanı arayüz döndürüyor ve `... on Uye { ad }` dalı çalışmış. Somut tipin ne olduğu
`__tip` çözücüsünden sorulur; veri kaynağındaki bir kaydın hangi tip olduğunu şema
bilemez, bunu bildiren kod yazılmak zorundadır.

#### Mutasyon: Sıra Bir Güvencedir

Mutasyon çıktısı işin özünü gösteriyor. Tek kopyalı bir kitap iki kez ödünç istendi;
`birinci` başarılı oldu, `ikinci` `null` döndü. Yürütme sırası satırı nedenini söylüyor:
ikinci çözücü, birincisi **bittikten sonra** başladı ve kalan kopyayı sıfır gördü.

Aynı iki çözücü birlikte yürütüldüğünde sıra değişiyor: ikisi de başlıyor, ikisi de
başlangıçtaki tek kopyayı okuyor ve ikisi de kayıt yazıyor. Tek kopyalı kitap iki kez ödünç
verilmiş oluyor. Bu, veritabanı derslerindeki kayıp güncelleme sorununun aynısıdır ve
GraphQL'in kök mutasyon alanlarını sırayla yürütmesinin nedeni budur.

Güvencenin sınırı da belirtilmelidir: sıralılık yalnız **kök** mutasyon alanları içindir ve
yalnız **tek bir işlem** içinde geçerlidir. Aynı anda gelen iki ayrı isteğin mutasyonları
yine çakışabilir; onu önleyen şey çözücünün kendi eşzamanlılık denetimidir. İşlem içi sıra,
istemcinin tek bir belgede yazdığı adımların bildiği sırayla çalışacağını güvence altına
alır; başka bir şeyi değil.

#### Abonelik: Aynı Seçim, Birden Çok Yanıt

Abonelik çıktısındaki üç satır aynı seçim kümesinden üretildi. Çözücü tek bir değer değil
bir **olay akışı** döndürür; çalıştırıcı her olayı aynı seçim kümesiyle biçimler. `uye { ad }`
dalı üç kez çözüldü.

Yapısal fark budur: sorgu ve mutasyonda kök alan bir değere çözülür, abonelikte bir akışa.
Bunun iki sonucu vardır. Bir abonelik işleminde yalnız bir kök alan bulunabilir — akışların
nasıl birleştirileceğinin tanımlı bir yanıtı yoktur. Ve aktarım katmanı istek–yanıt
olamaz; tek bir istek üzerinden birden çok yanıt taşıyabilen bir taşıyıcı gerekir.

#### Özet

- Üç işlem türü aynı seçim kümesi dilini kullanır; ayrıldıkları yer yürütme kurallarıdır.
- Sorgu kök alanları bağımsız kabul edilir ve birlikte yürütülür; yanıttaki anahtar sırası
  yine de sorgudaki sıradır.
- Mutasyon kök alanları sırayla yürütülür; tek kopyalı kitap sıralı yürütmede bir kez,
  birlikte yürütmede iki kez ödünç verilir.
- Sıralılık güvencesi yalnız kök mutasyon alanları ve tek bir işlem içindir; ayrı
  isteklerin çakışmasını önlemek çözücünün işidir.
- Abonelik çözücüsü değer değil olay akışı döndürür; aynı seçim kümesi her olaya yeniden
  uygulanır.
- Abonelikte tek kök alan bulunabilir ve aktarım katmanı istek–yanıt olamaz.

#### Sonraki Adım

Çalıştırıcı her alan için bir çözücü çağırdı ama çözücülerin nasıl yazılacağı konuşulmadı.
Ödünç kaydının `uye` alanı için ayrı bir çözücü var, `id` alanı için yok; ikisi arasındaki
fark nedir? Bir çözücü hangi bilgilere erişir, hangi sırayla çağrılır, döndürdüğü değer bir
sonraki düzeyin çözücüsüne nasıl geçer? Sonraki ders çözücü imzasını dört parçasıyla
tanımlar, varsayılan çözücünün ne yaptığını gösterir ve bir sorgunun kaç çözücü çağrısı
ürettiğini sayarak yürütmenin biçimini ortaya koyar.

### Çözücüler

Source: https://academia.sh/tr/kurslar/api-tasarimi/cozuculer

Çözücünün dört girdisi, varsayılan çözücünün ne yaptığı, yürütmenin düzey düzey ilerlemesi ve bir sorgunun ürettiği çözücü çağrısı ile veri kaynağı turu sayısının ölçülmesi.

Çalıştırıcı her alan için bir çözücü çağırdı ama çözücülerin nasıl yazıldığı konuşulmadı.
Ödünç kaydının `uye` alanı için ayrı bir çözücü yazılmıştı, `id` alanı için yazılmamıştı;
ikisi arasındaki fark, GraphQL yürütmesinin belkemiğidir.

Bu ders çözücü imzasını tanımlar, varsayılan çözücünün ne yaptığını gösterir ve bir sorgunun
kaç çözücü çağrısı ürettiğini sayar. Son sayı, sonraki dersin konusunu doğuracak.

#### Çözücünün Dört Girdisi

Çözücü, tek bir alanın değerini üreten işlevdir ve dört girdi alır.

- **Kaynak**, üst düzeyin döndürdüğü değerdir. `Odunc.uye` çözücüsünün kaynağı, bir üst
  düzeydeki ödünç kaydıdır. Kök alanlarda kaynak yoktur.
- **Argümanlar**, alanın argümanlarıdır; değişkenler bu noktada çözülmüştür, çözücü
  `$id` diye bir şey görmez.
- **Bağlam**, istek boyunca ortak olan veridir: kimlik bilgisi, veritabanı bağlantısı,
  istek başına önbellek. Alanlar arasında taşınan tek yatay kanal budur.
- **Bilgi**, çağrının nerede yapıldığını söyler: hangi alan, hangi üst tip, yanıttaki hangi
  yol.

Çalıştırıcının bu dördünü nasıl ürettiği aşağıda görünür. Yeni olan iki şey var: `bilgi`
nesnesi ve her çağrının bildirildiği `izleyici` kancası.

```js
// calistirici.mjs — denetlenmis bir belgeyi calistirir
// Her alan icin bir cozucu cagrilir; cozucu dort girdi alir:
//   kaynak      ust duzeyin dondurdugu deger
//   argumanlar  alanin argumanlari (degiskenler cozulmus)
//   baglam      istek boyunca ortak veri
//   bilgi       { alan, ustTip, yol } — cagriyi yerinde tanimlar
import { tipCoz, alanTanimi, olasiTipler } from "./sema.mjs";

const YAPRAK = new Set(["skaler", "sirali"]);

// Degisken bildirimlerini gelen degerlerle birlestirir.
function degiskenleriCoz(islem, gelen) {
  const d = {};
  for (const b of islem.degiskenler) {
    const v = gelen[b.ad] ?? b.varsayilan;
    if (v === undefined && b.tip.endsWith("!")) throw new Error(`$${b.ad} değişkeni zorunlu`);
    d[b.ad] = v;
  }
  return d;
}

const argCoz = (argumanlar, dv) =>
  Object.fromEntries(Object.entries(argumanlar).map(([a, d]) => [a, d.tur === "degisken" ? dv[d.ad] : d.deger]));

// Parcalari ve satir ici parcalari acar; somut tipe uymayan dallari atar.
function duzle(sema, belge, tipAdi, secim, birikim = []) {
  for (const s of secim) {
    if (s.tur === "alan") { birikim.push(s); continue; }
    const parca = s.tur === "parcaYayimi" ? belge.parcalar[s.ad] : s;
    const kosul = s.tur === "parcaYayimi" ? parca.tip : s.tip;
    if (kosul === tipAdi || olasiTipler(sema, kosul).includes(tipAdi)) duzle(sema, belge, tipAdi, parca.secim, birikim);
  }
  return birikim;
}

async function secimCalistir(o, tipAdi, secim, kaynak, yol, sirali = false) {
  const alanlar = duzle(o.sema, o.belge, tipAdi, secim);
  const sonuc = {};
  for (const a of alanlar) sonuc[a.takma] = null;      // anahtar sirasi sorgudan gelir
  const isle = async (a) => { sonuc[a.takma] = await alanCalistir(o, tipAdi, a, kaynak, `${yol}.${a.takma}`); };
  if (sirali) for (const a of alanlar) await isle(a);
  else await Promise.all(alanlar.map(isle));
  return sonuc;
}

async function alanCalistir(o, tipAdi, alan, kaynak, yol) {
  const tanim = alanTanimi(o.sema.tipler[tipAdi], alan.ad);
  const ozel = o.cozucular[tipAdi]?.[alan.ad];
  const cozucu = ozel ?? ((k) => k?.[alan.ad]);       // varsayilan cozucu: ayni adli alani okur
  const bilgi = { alan: alan.ad, ustTip: tipAdi, yol, varsayilan: !ozel };
  o.izleyici?.(bilgi);
  const deger = await cozucu(kaynak, argCoz(alan.argumanlar, o.dv), o.baglam, bilgi);
  return tamamla(o, tanim.tip, deger, alan.secim, yol);
}

async function tamamla(o, tipDizgi, deger, secim, yol) {
  if (deger === null || deger === undefined) return null;
  const t = tipCoz(tipDizgi);
  if (t.liste) return Promise.all(deger.map((d, i) => tamamla(o, t.ad, d, secim, `${yol}[${i}]`)));
  const tip = o.sema.tipler[t.ad];
  if (YAPRAK.has(tip.tur)) return deger;
  // Arayuz ve birlesimde somut tip cozuculerden sorulur.
  const somut = tip.tur === "nesne" ? t.ad : o.cozucular.__tip[t.ad](deger);
  return secimCalistir(o, somut, secim, deger, yol);
}

export async function calistir(sema, cozucular, belge, { islemAdi, degiskenler = {}, baglam = {}, izleyici } = {}) {
  const islem = islemAdi ? belge.islemler.find((i) => i.ad === islemAdi) : belge.islemler[0];
  const o = { sema, cozucular, belge, dv: degiskenleriCoz(islem, degiskenler), baglam, izleyici };
  const kok = islem.tur === "mutation" ? sema.mutasyon : sema.sorgu;
  return { data: await secimCalistir(o, kok, islem.secim, null, "", islem.tur === "mutation") };
}
```

#### Varsayılan Çözücü

`alanCalistir` içindeki tek satır belirleyicidir: yazılmış bir çözücü yoksa çalıştırıcı
`(k) => k?.[alan.ad]` işlevini kullanır. Bu **varsayılan çözücüdür** ve kaynaktan aynı adlı
alanı okur.

Ödünç kaydının `id`, `durum` ve `iadeTarihi` alanları için çözücü yazılmamıştır, çünkü
veri kaynağından gelen nesne bu alanları zaten aynı adlarla taşır. `uye` alanı için
yazılmıştır, çünkü kayıtta yalnız `uyeId` vardır ve üye nesnesine dönüşmesi bir arama
gerektirir.

Kural şudur: **çözücü yalnız veri şekli ile şema şekli ayrıştığında yazılır.** Bu, veri
kaynağının şemaya benzemesi hâlinde çözücü sayısını çarpıcı biçimde azaltır ve şemayı veri
kaynağının bir kopyası olmaya iter — dikkat edilmesi gereken bir eğilimdir, çünkü şema
tüketicinin gördüğü arayüzdür, deponun değil.

Aşağıdaki veri kaynağı her erişimi ayrıca sayar; böylece çözücü çağrısı ile veri kaynağı
turu birbirinden ayırt edilebilir.

```js
// veri.mjs — bellek ici veri; her erisim sayilir
export const sayac = { kitap: 0, uye: 0, odunc: 0 };

const KITAPLAR = new Map([
  ["978-0262033848", { isbn: "978-0262033848", baslik: "Algoritmalara Giris", yazar: "Cormen", kopya: 1 }],
  ["978-0201896831", { isbn: "978-0201896831", baslik: "Programlama Sanati", yazar: "Knuth", kopya: 2 }],
  ["978-0131103627", { isbn: "978-0131103627", baslik: "C Programlama Dili", yazar: "Kernighan", kopya: 3 }],
]);
const UYELER = new Map([
  ["U-1001", { id: "U-1001", olusturma: "2024-02-11", ad: "Ayse Yilmaz" }],
  ["U-1002", { id: "U-1002", olusturma: "2025-06-03", ad: "Deniz Kaya" }],
]);
const ODUNCLER = new Map([
  ["O-1", { id: "O-1", olusturma: "2026-01-04", uyeId: "U-1001", durum: "ACIK", iadeTarihi: "2026-03-20",
            kalemler: [{ isbn: "978-0262033848", sube: "merkez" }, { isbn: "978-0201896831", sube: "sahil" }] }],
  ["O-2", { id: "O-2", olusturma: "2026-01-19", uyeId: "U-1002", durum: "ACIK", iadeTarihi: "2026-04-02",
            kalemler: [{ isbn: "978-0131103627", sube: "tepe" }] }],
  ["O-3", { id: "O-3", olusturma: "2026-02-02", uyeId: "U-1001", durum: "KAPALI", iadeTarihi: "2026-02-28",
            kalemler: [{ isbn: "978-0201896831", sube: "merkez" }] }],
]);

// Tek kayit getiren erisimler: her cagri bir "veri kaynagi turu" sayilir.
export const kitapGetir = (isbn) => { sayac.kitap++; return KITAPLAR.get(isbn); };
export const uyeGetir = (id) => { sayac.uye++; return UYELER.get(id); };
export const oduncListesi = (uyeId) => { sayac.odunc++; return [...ODUNCLER.values()].filter((o) => !uyeId || o.uyeId === uyeId); };
export const oduncGetir = (id) => { sayac.odunc++; return ODUNCLER.get(id); };
export const sifirla = () => { sayac.kitap = 0; sayac.uye = 0; sayac.odunc = 0; };

export const COZUCULER = {
  __tip: { Kayit: (d) => (d.kalemler ? "Odunc" : "Uye") },
  Sorgu: {
    odunc: (_, a) => oduncGetir(a.id) ?? null,
    oduncler: () => oduncListesi(),
    uye: (_, a) => uyeGetir(a.kimlik) ?? null,
  },
  // id, olusturma, durum, iadeTarihi icin cozucu yok: varsayilan cozucu isini gorur.
  Odunc: { uye: (o) => uyeGetir(o.uyeId) },
  Kalem: { kitap: (k) => kitapGetir(k.isbn) },
  Uye:   { oduncler: (u) => oduncListesi(u.id) },
};
```

#### Yürütmenin Biçimi

```js
// kosu.mjs — cozucu cagrilarini izler, sirasini gosterir ve sayar
import { ayristir } from "./ayristirici.mjs";
import { calistir } from "./calistirici.mjs";
import { SEMA } from "./sema.mjs";
import { COZUCULER, sayac, sifirla } from "./veri.mjs";

async function izle(baslik, sorgu, ayrintili) {
  const iz = [];
  sifirla();
  const sonuc = await calistir(SEMA, COZUCULER, ayristir(sorgu), { izleyici: (b) => iz.push(b) });

  console.log(`\n=== ${baslik} ===`);
  if (ayrintili) {
    for (const b of iz) console.log(`  ${(b.ustTip + "." + b.alan).padEnd(16)}${b.varsayilan ? "varsayılan" : "yazılmış  "}  ${b.yol}`);
  } else {
    // Derinlige gore grupla: derinlik, cagri sayisi, hangi alanlar
    const duzey = new Map();
    for (const b of iz) {
      const d = b.yol.split(".").length - 1;
      if (!duzey.has(d)) duzey.set(d, []);
      duzey.get(d).push(`${b.ustTip}.${b.alan}`);
    }
    console.log("  derinlik  çağrı  alanlar");
    for (const [d, liste] of [...duzey].sort((a, b) => a[0] - b[0])) {
      console.log(`  ${String(d).padStart(8)}  ${String(liste.length).padStart(5)}  ${[...new Set(liste)].join(", ")}`);
    }
  }
  const ozel = iz.filter((b) => !b.varsayilan).length;
  console.log(`  toplam çözücü çağrısı: ${iz.length} (yazılmış ${ozel}, varsayılan ${iz.length - ozel})`);
  console.log(`  veri kaynağı turu: kitap=${sayac.kitap} üye=${sayac.uye} ödünç=${sayac.odunc}  toplam=${sayac.kitap + sayac.uye + sayac.odunc}`);
  return sonuc;
}

await izle("küçük sorgu", `{ oduncler { id uye { ad } } }`, true);
const sonuc = await izle("büyük sorgu", `{ oduncler { id durum uye { ad } kalemler { sube kitap { baslik yazar } } } }`, false);
console.log(`\nilk kayıt: ${JSON.stringify(sonuc.data.oduncler[0])}`);
```

```
=== küçük sorgu ===
  Sorgu.oduncler  yazılmış    .oduncler
  Odunc.id        varsayılan  .oduncler[0].id
  Odunc.uye       yazılmış    .oduncler[0].uye
  Odunc.id        varsayılan  .oduncler[1].id
  Odunc.uye       yazılmış    .oduncler[1].uye
  Odunc.id        varsayılan  .oduncler[2].id
  Odunc.uye       yazılmış    .oduncler[2].uye
  Uye.ad          varsayılan  .oduncler[0].uye.ad
  Uye.ad          varsayılan  .oduncler[1].uye.ad
  Uye.ad          varsayılan  .oduncler[2].uye.ad
  toplam çözücü çağrısı: 10 (yazılmış 4, varsayılan 6)
  veri kaynağı turu: kitap=0 üye=3 ödünç=1  toplam=4

=== büyük sorgu ===
  derinlik  çağrı  alanlar
         1      1  Sorgu.oduncler
         2     12  Odunc.id, Odunc.durum, Odunc.uye, Odunc.kalemler
         3     11  Uye.ad, Kalem.sube, Kalem.kitap
         4      8  Kitap.baslik, Kitap.yazar
  toplam çözücü çağrısı: 32 (yazılmış 8, varsayılan 24)
  veri kaynağı turu: kitap=4 üye=3 ödünç=1  toplam=8

ilk kayıt: {"id":"O-1","durum":"ACIK","uye":{"ad":"Ayse Yilmaz"},"kalemler":[{"sube":"merkez","kitap":{"baslik":"Algoritmalara Giris","yazar":"Cormen"}},{"sube":"sahil","kitap":{"baslik":"Programlama Sanati","yazar":"Knuth"}}]}
```

Küçük sorgunun izi yürütmenin biçimini doğrudan gösteriyor. Önce kök alan çözülür. Sonra üç
ödünç kaydının **hepsinin** `id` ve `uye` alanları çözülür. Ancak bunlar bittikten sonra
`Uye.ad` çağrıları başlar.

Yürütme derinlik değil **düzey** sırasıyla ilerler. Nedeni `secimCalistir` içindeki
`Promise.all` çağrısıdır: bir düzeydeki bütün kardeş alanlar birlikte başlatılır ve hepsi
bitmeden bir alt düzeye geçilmez. Bu, sonraki dersteki toplu getirmenin dayanacağı
özelliktir: aynı düzeydeki çağrılar zaten aynı anda yapılıyorsa, birleştirilebilirler.

İki kısıt bu sırayı zorunlu kılar. Bir çözücünün kaynağı üst düzeyin **döndürdüğü**
değerdir; üst çözülmeden alt çözülemez. Ve kardeş alanlar arasında bir sıra güvencesi
yoktur; sorgu alanları bağımsız kabul edildiği için birbirlerinin sonucuna dayanamazlar.

#### İki Ayrı Sayı

Büyük sorgu 32 çözücü çağrısı üretti ama veri kaynağına yalnız 8 kez gidildi. İki sayı
ayrıdır ve ayrı şeyleri ölçer.

Çözücü çağrısı sayısı **sorgunun biçiminden** gelir: seçilen alan sayısı ile listelerin
uzunluğunun çarpımı. Yirmi dördü varsayılan çözücüdür; bunlar bellekteki bir nesneden alan
okur, maliyetleri yok sayılabilir.

Veri kaynağı turu sayısı ise **yazılmış çözücülerden** gelir ve asıl maliyet buradadır. Sekiz
turun dağılımı dikkat çekicidir: bir ödünç listesi çağrısı, üç üye çağrısı, dört kitap
çağrısı. Oysa veritabanında yalnız iki ayrı üye ve üç ayrı kitap vardır.

Aynı üye iki ödünç kaydında geçtiği için iki kez getirilmiş, aynı kitap iki kalemde geçtiği
için iki kez getirilmiştir. Liste uzadıkça bu sayı listeyle birlikte büyür: yüz ödünç kaydı
yüz üye çağrısı demektir, üyelerin çoğu aynı kişi olsa bile.

#### Özet

- Çözücü dört girdi alır: üst düzeyin döndürdüğü kaynak, çözülmüş argümanlar, istek boyunca
  ortak bağlam ve çağrının yerini bildiren bilgi.
- Varsayılan çözücü kaynaktan aynı adlı alanı okur; çözücü yalnız veri şekli ile şema şekli
  ayrıştığında yazılır.
- Yürütme derinlik değil düzey sırasıyla ilerler: bir düzeydeki bütün kardeş alanlar
  birlikte başlatılır, hepsi bitmeden alt düzeye geçilmez.
- Sıra iki kısıttan doğar: alt düzey üstün döndürdüğü değere muhtaçtır ve kardeş alanlar
  birbirinin sonucuna dayanamaz.
- Çözücü çağrısı sayısı sorgunun biçiminden, veri kaynağı turu sayısı yazılmış çözücülerden
  gelir; ikisi ayrı ölçülerdir.
- Aynı üye ve aynı kitap birden çok kez getirilir; bu sayı liste uzunluğuyla birlikte büyür.

#### Sonraki Adım

Son paragraftaki sayı bir sorunun adıdır. Üç ödünç kaydı için üç üye çağrısı yapıldı, oysa
iki ayrı üye vardı; dört kitap çağrısı yapıldı, oysa üç ayrı kitap vardı. Liste yüz kayda
çıktığında yüz bir tur olacak: bir liste sorgusu artı her kayıt için bir tane. Sonraki ders
bu sorunu adıyla koyar, aynı düzeyde yapılan çağrıların birlikte başlatıldığı gerçeğine
dayanan bir toplu getirme katmanı yazar ve tur sayısının kaça düştüğünü ölçer.

### Parçalar ve Değişkenler

Source: https://academia.sh/tr/kurslar/api-tasarimi/parcalar-ve-degiskenler

Parçanın yinelenen seçim kümesini tek yere toplaması, çakışan parçalardaki alanların birleştirilmesiyle düşen çözücü çağrısı ve değişkenlerin ayrı belge sayısını sabitlemesi.

Çalıştırıcı parçaları ve değişkenleri baştan beri destekliyordu ama ikisinin ne işe
yaradığı gösterilmedi. Bu ders iki soruyu ölçerek yanıtlar: parça yazmanın karşılığı nedir
ve değeri sorgu metnine gömmek yerine değişken kullanmak neyi değiştirir?

İkisinin de ortak bir yanı vardır: yanıtı değiştirmezler. Parçalı sorgu ile parçasız sorgu
aynı veriyi döndürür, değişkenli sorgu ile değeri gömülü sorgu aynı veriyi döndürür.
Kazanç başka yerdedir.

Bu dersteki ayrıştırıcı, şema, veri kaynağı ve çalıştırıcı önceki üç derste yazıldıkları
hâliyle kullanılır; yalnız çalıştırıcının seçim kümesini düzleyen bölümü değişecek.

#### Parçanın Yaptığı İş

**Sorgu parçası** (fragment), bir tipe bağlanmış adlandırılmış seçim kümesidir; bu kurs
içinde kısaca parça denir. Aynı alanları birden çok yerde okuyan bir sorgu, o kümeyi bir
kez yazıp adıyla çağırır.

```js
// kosu.mjs — parcali ve parcasiz sorgulari karsilastirir
import { ayristir } from "./ayristirici.mjs";
import { calistir } from "./calistirici.mjs";
import { SEMA } from "./sema.mjs";
import { COZUCULER, sifirla } from "./veri.mjs";

// Ayni secim kumesi uc yerde tekrarlanmis hali
const PARCASIZ = `{
  acik:   odunc(id: "O-1") { id durum uye { ad } kalemler { sube kitap { baslik } } }
  kapali: odunc(id: "O-3") { id durum uye { ad } kalemler { sube kitap { baslik } } }
  digeri: odunc(id: "O-2") { id durum uye { ad } kalemler { sube kitap { baslik } } }
}`;

// Ayni sorgu, secim kumesi tek yerde
const PARCALI = `{
  acik:   odunc(id: "O-1") { ...OduncOzeti }
  kapali: odunc(id: "O-3") { ...OduncOzeti }
  digeri: odunc(id: "O-2") { ...OduncOzeti }
}
fragment OduncOzeti on Odunc { id durum uye { ad } kalemler { sube kitap { baslik } } }`;

// "durum" gibi bir alan degistirilecek olsa kac satira dokunulurdu?
const duzenlemeYeri = (metin, alan) => metin.split("\n").filter((s) => s.includes(alan)).length;

async function calistirVeSay(metin, degiskenler = {}) {
  const iz = [];
  sifirla();
  const s = await calistir(SEMA, COZUCULER, ayristir(metin), { degiskenler, izleyici: (b) => iz.push(b) });
  return { veri: JSON.stringify(s.data), cagri: iz.length };
}

const a = await calistirVeSay(PARCASIZ);
const b = await calistirVeSay(PARCALI);

console.log("belge      satır  'durum' geçen satır  çözücü çağrısı");
console.log(`parçasız   ${String(PARCASIZ.split("\n").length).padStart(5)}  ${String(duzenlemeYeri(PARCASIZ, "durum")).padStart(19)}  ${String(a.cagri).padStart(14)}`);
console.log(`parçalı    ${String(PARCALI.split("\n").length).padStart(5)}  ${String(duzenlemeYeri(PARCALI, "durum")).padStart(19)}  ${String(b.cagri).padStart(14)}`);
console.log(`\niki belgenin sonucu birebir aynı mı: ${a.veri === b.veri}`);
console.log(`sonuç: ${a.veri.slice(0, 96)}...`);
```

```
belge      satır  'durum' geçen satır  çözücü çağrısı
parçasız       5                    3              30
parçalı        6                    1              30

iki belgenin sonucu birebir aynı mı: true
sonuç: {"acik":{"id":"O-1","durum":"ACIK","uye":{"ad":"Ayse Yilmaz"},"kalemler":[{"sube":"merkez","kita...
```

Parçalı belge bir satır daha uzun ama seçim kümesi tek yerde duruyor. Alan eklemek ya da
çıkarmak gerektiğinde parçasız belgede üç satıra, parçalı belgede bir satıra dokunulur.
Fark üçe birdir ve kök alan sayısıyla birlikte büyür.

Sonuçların birebir aynı olması ve çözücü çağrısı sayısının değişmemesi, parçanın **çalışma
anında yok olduğunu** gösterir. Çalıştırıcı seçim kümesini düzlerken parçaları açar; o
noktadan sonra parçalı belgeyle parçasız belge arasında hiçbir fark kalmaz. Parça bir
yürütme kavramı değil, bir kaynak metin kavramıdır.

Bunun karşılığı, parçanın istemci tarafında bileşenle eşleşebilmesidir. Bileşen Tabanlı
Arayüz Geliştirme kursundaki bileşen ağacında her bileşen kendi veri gereksinimini bir
parça olarak bildirebilir ve üst bileşen bu parçaları birleştirerek tek bir sorgu kurar;
alt bileşen bir alan eklediğinde üst sorgu kendiliğinden büyür.

#### Çakışan Alanların Birleştirilmesi

Parçalar birleştirilirken aynı alan birden çok kez seçilebilir. İki parça da `id` istiyorsa
ne olur?

Çalıştırıcının seçim kümesini düzleyen bölümü ayrı bir modüle alınır ve bu soruyu yanıtlar:
aynı yanıt anahtarına düşen alanlar tek çağrıya indirilir, alt seçimleri birleşir.

```js
// duzle.mjs — secim kumesini duzler; ayni yanit anahtarina dusen alanlari birlestirir
import { olasiTipler } from "./sema.mjs";

// Parcalari acar, somut tipe uymayan dallari atar, kalanlari sirayla dizer.
function ac(sema, belge, tipAdi, secim, birikim = []) {
  for (const s of secim) {
    if (s.tur === "alan") { birikim.push(s); continue; }
    const parca = s.tur === "parcaYayimi" ? belge.parcalar[s.ad] : s;
    const kosul = s.tur === "parcaYayimi" ? parca.tip : s.tip;
    if (kosul === tipAdi || olasiTipler(sema, kosul).includes(tipAdi)) ac(sema, belge, tipAdi, parca.secim, birikim);
  }
  return birikim;
}

// Ayni yanit anahtarina dusen alanlar tek cagriya indirilir, alt secimleri birlesir.
export function duzle(sema, belge, tipAdi, secim, birlestir = true) {
  const acik = ac(sema, belge, tipAdi, secim);
  if (!birlestir) return acik;
  const kume = new Map();
  for (const a of acik) {
    const onceki = kume.get(a.takma);
    if (!onceki) { kume.set(a.takma, { ...a, secim: a.secim ? [...a.secim] : null }); continue; }
    if (a.secim) onceki.secim = [...(onceki.secim ?? []), ...a.secim];
  }
  return [...kume.values()];
}
```

Çalıştırıcı bu modülün üzerine yeniden kurulur. Çözücü imzası, izleyici ve düzey sıralı
yürütme Çözücüler dersindeki gibidir; değişen iki şey vardır: düzleme işi dışarı alınmıştır
ve birleştirme kapatılabilir. Ölçüm bu anahtara dayanacak.

```js
// calistirici.mjs — duzleyicisi ayri modulden gelen calistirici
// Cozucu imzasi ve izleyici Cozuculer dersindeki gibidir; secim kumesini duzleyen bolum
// artik duzle.mjs'den gelir ve "birlestir" secenegiyle kapatilabilir.
import { tipCoz, alanTanimi } from "./sema.mjs";
import { duzle } from "./duzle.mjs";

const YAPRAK = new Set(["skaler", "sirali"]);

function degiskenleriCoz(islem, gelen) {
  const d = {};
  for (const b of islem.degiskenler) {
    const v = gelen[b.ad] ?? b.varsayilan;
    if (v === undefined && b.tip.endsWith("!")) throw new Error(`$${b.ad} değişkeni zorunlu`);
    d[b.ad] = v;
  }
  return d;
}

const argCoz = (argumanlar, dv) =>
  Object.fromEntries(Object.entries(argumanlar).map(([a, d]) => [a, d.tur === "degisken" ? dv[d.ad] : d.deger]));

async function secimCalistir(o, tipAdi, secim, kaynak, yol, sirali = false) {
  const alanlar = duzle(o.sema, o.belge, tipAdi, secim, o.birlestir);
  const sonuc = {};
  for (const a of alanlar) sonuc[a.takma] = null;      // anahtar sirasi sorgudan gelir
  const isle = async (a) => { sonuc[a.takma] = await alanCalistir(o, tipAdi, a, kaynak, `${yol}.${a.takma}`); };
  if (sirali) for (const a of alanlar) await isle(a);
  else await Promise.all(alanlar.map(isle));
  return sonuc;
}

async function alanCalistir(o, tipAdi, alan, kaynak, yol) {
  const tanim = alanTanimi(o.sema.tipler[tipAdi], alan.ad);
  const ozel = o.cozucular[tipAdi]?.[alan.ad];
  const cozucu = ozel ?? ((k) => k?.[alan.ad]);       // varsayilan cozucu: ayni adli alani okur
  const bilgi = { alan: alan.ad, ustTip: tipAdi, yol, varsayilan: !ozel };
  o.izleyici?.(bilgi);
  const deger = await cozucu(kaynak, argCoz(alan.argumanlar, o.dv), o.baglam, bilgi);
  return tamamla(o, tanim.tip, deger, alan.secim, yol);
}

async function tamamla(o, tipDizgi, deger, secim, yol) {
  if (deger === null || deger === undefined) return null;
  const t = tipCoz(tipDizgi);
  if (t.liste) return Promise.all(deger.map((d, i) => tamamla(o, t.ad, d, secim, `${yol}[${i}]`)));
  const tip = o.sema.tipler[t.ad];
  if (YAPRAK.has(tip.tur)) return deger;
  const somut = tip.tur === "nesne" ? t.ad : o.cozucular.__tip[t.ad](deger);
  return secimCalistir(o, somut, secim, deger, yol);
}

export async function calistir(sema, cozucular, belge,
    { islemAdi, degiskenler = {}, baglam = {}, izleyici, birlestir = true } = {}) {
  const islem = islemAdi ? belge.islemler.find((i) => i.ad === islemAdi) : belge.islemler[0];
  const o = { sema, cozucular, belge, dv: degiskenleriCoz(islem, degiskenler), baglam, izleyici, birlestir };
  const kok = islem.tur === "mutation" ? sema.mutasyon : sema.sorgu;
  return { data: await secimCalistir(o, kok, islem.secim, null, "", islem.tur === "mutation") };
}
```

```js
// cakisma.mjs — cakisan parcalarin ayni alani iki kez cozdurmesini olcer
import { ayristir } from "./ayristirici.mjs";
import { calistir } from "./calistirici.mjs";
import { SEMA } from "./sema.mjs";
import { COZUCULER } from "./veri.mjs";

const SORGU = `{
  odunc(id: "O-1") { id ...Kim ...Ne }
}
fragment Kim on Odunc { id durum uye { ad } }
fragment Ne  on Odunc { id durum kalemler { sube } }`;

for (const birlestir of [false, true]) {
  const iz = [];
  const s = await calistir(SEMA, COZUCULER, ayristir(SORGU), { izleyici: (b) => iz.push(b), birlestir });
  const kez = {};
  for (const b of iz) kez[`${b.ustTip}.${b.alan}`] = (kez[`${b.ustTip}.${b.alan}`] ?? 0) + 1;
  console.log(`birleştirme ${birlestir ? "açık " : "kapalı"}  çağrı=${String(iz.length).padStart(2)}  ` +
    Object.entries(kez).map(([a, n]) => `${a}×${n}`).join(" "));
  console.log(`  sonuç: ${JSON.stringify(s.data)}`);
}
```

```
birleştirme kapalı  çağrı=11  Sorgu.odunc×1 Odunc.id×3 Odunc.durum×2 Odunc.uye×1 Odunc.kalemler×1 Uye.ad×1 Kalem.sube×2
  sonuç: {"odunc":{"id":"O-1","durum":"ACIK","uye":{"ad":"Ayse Yilmaz"},"kalemler":[{"sube":"merkez"},{"sube":"sahil"}]}}
birleştirme açık   çağrı= 8  Sorgu.odunc×1 Odunc.id×1 Odunc.durum×1 Odunc.uye×1 Odunc.kalemler×1 Uye.ad×1 Kalem.sube×2
  sonuç: {"odunc":{"id":"O-1","durum":"ACIK","uye":{"ad":"Ayse Yilmaz"},"kalemler":[{"sube":"merkez"},{"sube":"sahil"}]}}
```

İki sonuç aynı, çağrı sayısı on birden sekize düştü. Birleştirme olmasaydı `id` alanı üç
kez, `durum` alanı iki kez çözülürdü; yanıtta yine tek kez görünürlerdi, çünkü aynı anahtara
yazılıyorlar. Yani birleştirmesiz yürütme yanlış sonuç vermez, **gereksiz iş** yapar.

Bu, parçaların bileşen başına yazılmasını güvenli kılan kuraldır. Beş bileşen kendi
parçasında `id` alanını isteyebilir; birleştirme sayesinde alan bir kez çözülür. Kural
olmasaydı parça yazmanın maliyeti kullanım sayısıyla çarpılırdı.

Birleştirmenin bir koşulu vardır: aynı anahtara düşen alanların çakışabilir olması gerekir.
Aynı ada iki farklı argümanla iki kez sorulan alan birleştirilemez; bu durumda takma ad
zorunludur ve iki ayrı anahtar oluşur.

#### Değişkenler

Değişken, sorgu metnini değerden ayırır. Ayrımın sonucu, aynı ekranın her açılışında
sunucuya **aynı belgenin** gitmesidir.

```js
// degiskenler.mjs — degeri metne gomen sorgu ile degisken kullanan sorguyu karsilastirir
import { ayristir } from "./ayristirici.mjs";
import { calistir } from "./calistirici.mjs";
import { SEMA } from "./sema.mjs";
import { COZUCULER } from "./veri.mjs";

const KUCUK = ["O-1", "O-2", "O-3", "O-1", "O-2"];
// Buyuk yuk: 200 istek, 50 ayri kimlige dagilmis
const BUYUK = Array.from({ length: 200 }, (_, i) => `O-${(i % 50) + 1}`);

const GOMULU  = { metin: (id) => `{ odunc(id: "${id}") { id durum uye { ad } } }`, degisken: () => ({}) };
const DEGISKENLI = { metin: () => `query($id: ID!) { odunc(id: $id) { id durum uye { ad } } }`, degisken: (id) => ({ id }) };

async function kosu(baslik, kimlikler, bicim, calistirilsin) {
  const onbellek = new Map();      // sunucu ayristirilmis belgeyi metnine gore saklar
  let iska = 0, bayt = 0;
  for (const id of kimlikler) {
    const metin = bicim.metin(id);
    if (!onbellek.has(metin)) { iska++; onbellek.set(metin, ayristir(metin)); }
    bayt += Buffer.byteLength(metin) + Buffer.byteLength(JSON.stringify(bicim.degisken(id)));
    if (calistirilsin) await calistir(SEMA, COZUCULER, onbellek.get(metin), { degiskenler: bicim.degisken(id) });
  }
  console.log(`${baslik.padEnd(30)} istek=${String(kimlikler.length).padStart(3)}  ayrı belge=${String(onbellek.size).padStart(3)}  ` +
    `ayrıştırma=${String(iska).padStart(3)}  gönderilen bayt=${bayt}`);
}

await kosu("5 istek, değer metne gömülü", KUCUK, GOMULU, true);
await kosu("5 istek, değişkenli", KUCUK, DEGISKENLI, true);
await kosu("200 istek, değer metne gömülü", BUYUK, GOMULU, false);
await kosu("200 istek, değişkenli", BUYUK, DEGISKENLI, false);
```

```
5 istek, değer metne gömülü    istek=  5  ayrı belge=  3  ayrıştırma=  3  gönderilen bayt=230
5 istek, değişkenli            istek=  5  ayrı belge=  1  ayrıştırma=  1  gönderilen bayt=350
200 istek, değer metne gömülü  istek=200  ayrı belge= 50  ayrıştırma= 50  gönderilen bayt=9364
200 istek, değişkenli          istek=200  ayrı belge=  1  ayrıştırma=  1  gönderilen bayt=14164
```

Bayt sütunu değişkenli biçemin aleyhinedir ve öyle kalır: değişken bildirimi sorgu metnini
uzatır, değişkenler ayrıca gönderilir. Kazanç bayt sayısında değil, **ayrı belge sayısında**
görülür.

Beş istekte fark üçe birdir, iki yüz istekte elliye bir. Değeri metne gömen biçemde ayrı
belge sayısı ayrı değer sayısıyla birlikte büyür; değişkenli biçemde bir tanedir ve öyle
kalır. Bu sayı üç yerde işe yarar: sunucu ayrıştırma ve denetleme sonucunu belge başına
saklayabilir, kütüklerde hangi sorgunun ne sıklıkla çalıştığı gruplanabilir ve belgeler
önceden kaydedilip istemci yalnız bir kimlik gönderebilir.

Bir de doğruluk gerekçesi vardır. Değeri metne gömmek, değeri sorgu diline **enjekte
etmek** demektir; tırnak içeren bir değerin kaçırılmadan gömülmesi sorgunun yapısını
değiştirebilir. Değişken kullanıldığında değer hiçbir zaman ayrıştırılmaz, ayrı bir JSON
gövdesinde taşınır ve tipi şemaya karşı denetlenir. SQL'de hazır deyimlerin çözdüğü sorunun
aynısı, aynı biçimde çözülür.

#### Özet

- Parça, bir tipe bağlanmış adlandırılmış seçim kümesidir; yinelenen alanları tek yere
  toplayarak düzenleme yeri sayısını üçten bire indirir.
- Parça çalışma anında yok olur: parçalı ve parçasız belge aynı sonucu ve aynı çözücü çağrı
  sayısını üretir; parça bir kaynak metin kavramıdır.
- Aynı yanıt anahtarına düşen alanlar birleştirilir; birleştirmesiz yürütme yanlış sonuç
  vermez ama aynı alanı birden çok kez çözer.
- Birleştirme, parçaların bileşen başına yazılmasını güvenli kılar; olmasaydı parça yazmanın
  maliyeti kullanım sayısıyla çarpılırdı.
- Değişken kullanmak gönderilen baytı azaltmaz, ayrı belge sayısını sabitler: iki yüz
  istekte elli ayrı belge yerine bir.
- Sabit belge sayısı ayrıştırma sonucunun saklanmasını, kütüklerin gruplanmasını ve
  belgelerin önceden kaydedilmesini mümkün kılar; ayrıca değer sorgu diline hiç girmediği
  için yapı bozulamaz.

#### Sonraki Adım

Çözücüler dersi bir sayıyı açıkta bırakmıştı: üç ödünç kaydı için üç üye çağrısı yapılmıştı,
oysa iki ayrı üye vardı. Parçalar ve değişkenler bu sayıya dokunmadı; birleştirme yalnız
**aynı yanıt anahtarındaki** çakışmaları giderir, farklı kayıtların aynı üyeyi istemesini
gidermez. Liste yüz kayda çıktığında yüz üye çağrısı yapılacak. Sonraki ders bu sorunu
adıyla koyar, aynı düzeydeki çağrıların birlikte başlatıldığı gerçeğine dayanan bir toplu
getirme katmanı yazar ve tur sayısının kaça düştüğünü ölçer.

### N+1 Sorunu ve Toplu Yükleme

Source: https://academia.sh/tr/kurslar/api-tasarimi/n-arti-1-sorunu-ve-toplu-yukleme

Liste uzunluğuyla birlikte büyüyen veri kaynağı turu sayısı, aynı iş parçacığındaki çağrıları tek turda birleştiren yükleyici katmanı ve 101 turdan 3 tura düşen ölçüm.

Çözücüler dersi bir sayıyı açıkta bırakmıştı: üç ödünç kaydı için üç üye çağrısı yapılmıştı,
oysa iki ayrı üye vardı. Parçalar ve değişkenler bu sayıya dokunmadı; alan birleştirme
yalnız aynı yanıt anahtarındaki çakışmaları giderir, farklı kayıtların aynı üyeyi
istemesini gidermez.

Bu ders sorunu ölçekte gösterir ve çözer. Veri kaynağı elli ödünç kaydı, sekiz üye ve on
iki kitap içerir.

```js
// veri.mjs — olcek icin uretilmis odunc verisi; her veri kaynagi turu sayilir
export const tur = { kitap: 0, uye: 0, odunc: 0 };
export const getirilenKayit = { kitap: 0, uye: 0 };
export const sifirla = () => { tur.kitap = tur.uye = tur.odunc = 0; getirilenKayit.kitap = getirilenKayit.uye = 0; };

const YAZAR = ["Cormen", "Knuth", "Kernighan", "Dijkstra", "Hoare", "Lamport"];
export const KITAPLAR = new Map(Array.from({ length: 12 }, (_, i) => {
  const isbn = `978-000000${String(i).padStart(2, "0")}`;
  return [isbn, { isbn, baslik: `Kitap ${i}`, yazar: YAZAR[i % YAZAR.length], kopya: 1 + (i % 3) }];
}));
export const UYELER = new Map(Array.from({ length: 8 }, (_, i) => {
  const id = `U-${1001 + i}`;
  return [id, { id, olusturma: "2025-01-01", ad: `Üye ${i}` }];
}));
export const ODUNCLER = new Map(Array.from({ length: 50 }, (_, i) => {
  const id = `O-${i + 1}`;
  return [id, { id, olusturma: "2026-01-01", uyeId: `U-${1001 + (i % 8)}`,
    durum: i % 3 === 0 ? "KAPALI" : "ACIK", iadeTarihi: "2026-04-01",
    kalemler: [{ isbn: `978-000000${String(i % 12).padStart(2, "0")}`, sube: "merkez" }] }];
}));

// Tekil erisimler: her cagri bir tur
export const kitapGetir = (isbn) => { tur.kitap++; getirilenKayit.kitap++; return KITAPLAR.get(isbn); };
export const uyeGetir = (id) => { tur.uye++; getirilenKayit.uye++; return UYELER.get(id); };
export const oduncListesi = () => { tur.odunc++; return [...ODUNCLER.values()]; };

// Toplu erisimler: tek turda birden cok anahtar
export const kitaplariGetir = (isbnler) => { tur.kitap++; getirilenKayit.kitap += isbnler.length; return isbnler.map((i) => KITAPLAR.get(i)); };
export const uyeleriGetir = (idler) => { tur.uye++; getirilenKayit.uye += idler.length; return idler.map((i) => UYELER.get(i)); };
```

Ayrıştırıcı, şema, düzleyici ve çalıştırıcı önceki derslerde yazıldıkları hâliyle
kullanılır; yeni olan veri kaynağı ile yükleyici katmanıdır.

#### Sorunun Adı

Elli ödünç kaydını üyeleriyle birlikte isteyen bir sorgu, veri kaynağına elli bir kez
gider: bir kez listeyi almak için, elli kez her kaydın üyesini almak için. Kitap alanı
eklendiğinde elli tur daha. Buna **N+1 sorunu** denir: bir liste sorgusu artı liste
uzunluğu kadar tekil sorgu.

Sorunun kaynağı çözücü modelidir. `Odunc.uye` çözücüsü tek bir ödünç kaydı görür ve o kaydın
üyesini getirir; kendisinin kırk dokuz kardeşi olduğunu bilmez. Her çözücünün yalnız kendi
kaynağını görmesi, çözücü modelinin gücüdür — ve bu sorunun da nedenidir.

Çözüm, çözücüleri değiştirmek değil, aralarına bir katman koymaktır. **Yükleyici**, kendisine
gelen anahtarları hemen getirmez; aynı iş parçacığında istenen anahtarları biriktirir ve
tek bir toplu çağrıya indirir. İkinci görevi tekilleştirmedir: aynı anahtar iki kez
istenirse bir kez getirilir.

```js
// yukleyici.mjs — ayni is parcaciginda istenen anahtarlari tek toplu cagriya indirir
// Ikinci gorevi tekillestirme: ayni anahtar iki kez istenirse bir kez getirilir.
export function yukleyiciKur(topluGetir) {
  let bekleyen = new Map();          // anahtar -> [coz, ...]
  let planlandi = false;

  const bosalt = async () => {
    const kume = bekleyen; bekleyen = new Map(); planlandi = false;
    const anahtarlar = [...kume.keys()];
    const degerler = await topluGetir(anahtarlar);
    anahtarlar.forEach((a, i) => kume.get(a).forEach((coz) => coz(degerler[i])));
  };

  return (anahtar) => new Promise((coz) => {
    if (!bekleyen.has(anahtar)) bekleyen.set(anahtar, []);
    bekleyen.get(anahtar).push(coz);
    if (!planlandi) { planlandi = true; queueMicrotask(bosalt); }
  });
}
```

Toplama penceresi `queueMicrotask` çağrısıyla belirlenir: o an çalışan iş parçacığı bitene
kadar gelen bütün anahtarlar aynı kümeye düşer. Çözücüler dersinde ölçülen düzey sıralı
yürütme burada karşılığını buluyor — aynı düzeydeki elli `Odunc.uye` çağrısı zaten birlikte
başlatılıyordu, dolayısıyla hepsi aynı pencereye girer.

#### Ölçüm

```js
// kosu.mjs — tekil ve toplu cozuculerle ayni sorguyu calistirip tur sayisini karsilastirir
import { ayristir } from "./ayristirici.mjs";
import { calistir } from "./calistirici.mjs";
import { SEMA } from "./sema.mjs";
import { yukleyiciKur } from "./yukleyici.mjs";
import { oduncListesi, kitapGetir, uyeGetir, kitaplariGetir, uyeleriGetir, tur, getirilenKayit, sifirla } from "./veri.mjs";

const SORGU = `{ oduncler { id durum uye { ad } kalemler { sube kitap { baslik yazar } } } }`;

const TEKIL = {
  Sorgu: { oduncler: () => oduncListesi() },
  Odunc: { uye: (o) => uyeGetir(o.uyeId) },
  Kalem: { kitap: (k) => kitapGetir(k.isbn) },
};

// Toplu cozuculer, yukleyicileri istek basina baglamdan alir.
const TOPLU = {
  Sorgu: { oduncler: () => oduncListesi() },
  Odunc: { uye: (o, _, b) => b.uyeYukle(o.uyeId) },
  Kalem: { kitap: (k, _, b) => b.kitapYukle(k.isbn) },
};

async function olc(baslik, cozucular, baglamUret) {
  sifirla();
  const iz = [];
  const s = await calistir(SEMA, cozucular, ayristir(SORGU), { baglam: baglamUret(), izleyici: (b) => iz.push(b) });
  const toplam = tur.kitap + tur.uye + tur.odunc;
  console.log(`${baslik.padEnd(14)} çözücü çağrısı=${String(iz.length).padStart(3)}  ` +
    `veri kaynağı turu: ödünç=${tur.odunc} üye=${String(tur.uye).padStart(2)} kitap=${String(tur.kitap).padStart(2)} toplam=${String(toplam).padStart(3)}  ` +
    `getirilen kayıt: üye=${getirilenKayit.uye} kitap=${getirilenKayit.kitap}`);
  return s.data.oduncler.length;
}

const n = await olc("tekil", TEKIL, () => ({}));
await olc("toplu", TOPLU, () => ({
  uyeYukle: yukleyiciKur(uyeleriGetir),      // istek basina yeni yukleyici
  kitapYukle: yukleyiciKur(kitaplariGetir),
}));
console.log(`\nyanıttaki ödünç kaydı: ${n}   ayrı üye: 8   ayrı kitap: 12`);
```

```
tekil          çözücü çağrısı=451  veri kaynağı turu: ödünç=1 üye=50 kitap=50 toplam=101  getirilen kayıt: üye=50 kitap=50
toplu          çözücü çağrısı=451  veri kaynağı turu: ödünç=1 üye= 1 kitap= 1 toplam=  3  getirilen kayıt: üye=8 kitap=12

yanıttaki ödünç kaydı: 50   ayrı üye: 8   ayrı kitap: 12
```

Üç sayı okunmalıdır.

**Tur sayısı 101'den 3'e indi.** Sorgu değişmedi, çözücü sayısı değişmedi, yanıt değişmedi;
değişen yalnız çözücülerin veri kaynağına nasıl gittiğidir.

**Çözücü çağrısı sayısı 451'de sabit kaldı.** Yükleyici çözücüleri azaltmaz; her ödünç
kaydının `uye` alanı yine ayrı ayrı çözülür. Azalttığı şey, o çözücülerin veri kaynağına
yaptığı turdur. Bu ayrım önemlidir: yürütmenin biçimi sorgudan gelir ve değiştirilemez,
veri erişiminin biçimi çözücüden gelir ve değiştirilebilir.

**Getirilen kayıt sayısı 50'den 8'e indi.** Tekilleştirmenin payı budur. Elli ödünç kaydı
sekiz üyeye dağıldığı için aynı üye ortalama altı kez isteniyordu; yükleyici her birini bir
kez getirdi.

#### Toplama Penceresi Yürütme Sırasına Bağlıdır

Yükleyicinin kazancı, anahtarların aynı pencereye düşmesine bağlıdır. Pencere bir iş
parçacığı kadardır ve arada bir bekleme varsa kapanır.

```js
// pencere.mjs — yukleyicinin toplama penceresi bir is parcacigi kadardir
import { yukleyiciKur } from "./yukleyici.mjs";

const izle = (ad) => (anahtarlar) => {
  console.log(`  ${ad}: bir turda ${anahtarlar.length} anahtar -> ${anahtarlar.join(", ")}`);
  return anahtarlar.map((a) => ({ id: a }));
};

console.log("aynı iş parçacığında istenen anahtarlar:");
const y1 = yukleyiciKur(izle("birlikte"));
await Promise.all(["U-1001", "U-1002", "U-1003", "U-1001"].map(y1));

console.log("sırayla beklenerek istenen anahtarlar:");
const y2 = yukleyiciKur(izle("sırayla "));
for (const a of ["U-1001", "U-1002", "U-1003"]) await y2(a);
```

```
aynı iş parçacığında istenen anahtarlar:
  birlikte: bir turda 3 anahtar -> U-1001, U-1002, U-1003
sırayla beklenerek istenen anahtarlar:
  sırayla : bir turda 1 anahtar -> U-1001
  sırayla : bir turda 1 anahtar -> U-1002
  sırayla : bir turda 1 anahtar -> U-1003
```

Dört anahtar istendi, üçü getirildi: `U-1001` iki kez istenmiş ve bir kez getirilmiştir.
Aynı anahtarlar sırayla beklenerek istendiğinde üç ayrı tur oluştu; toplama hiç işlemedi.

Buradan iki kural çıkar. Çözücü içinde gereksiz `await` kullanmak toplamayı bozar: bir
çözücü önce başka bir şeyi bekleyip sonra yükleyiciyi çağırırsa, kardeşleriyle aynı
pencereye giremez. Ve mutasyonlarda kök alanlar sırayla yürütüldüğü için kök düzeyde toplama
olmaz; toplama alt düzeylerde yine işler.

#### Yükleyicinin Ömrü

Ölçüm betiğinde yükleyiciler her çalıştırmada yeniden kuruluyor. Bu bir ayrıntı değil,
zorunluluktur: **yükleyici istek başına yaratılır.**

Nedeni tekilleştirmenin aynı zamanda bir önbellek olmasıdır. Yükleyici bir isteğin ömrü
boyunca aynı anahtar için aynı değeri döndürür; bu, tek bir yanıtın kendi içinde tutarlı
olmasını sağlar. İstekler arasında paylaşılan bir yükleyici ise eski veriyi süresiz
saklayan bir önbelleğe dönüşür ve iki ayrı kullanıcının isteği aynı kayıtları paylaşmaya
başlar. İstek başına yaratılan yükleyici, ömrü bittiğinde birlikte gider.

Bu, bağlamın neden var olduğunu da açıklar. Yükleyiciler bağlama konur, çünkü bağlam istek
başına yaratılan ve bütün çözücülerin eriştiği tek yapıdır.

#### Özet

- N+1 sorunu bir liste sorgusu artı liste uzunluğu kadar tekil sorgudur; kaynağı, her
  çözücünün yalnız kendi kaynağını görmesidir.
- Yükleyici, aynı iş parçacığında istenen anahtarları biriktirip tek toplu çağrıya indirir
  ve aynı anahtarı bir kez getirir.
- Elli kayıtlık sorguda veri kaynağı turu 101'den 3'e, getirilen kayıt sayısı 50'den 8'e
  iner; çözücü çağrısı sayısı 451'de sabit kalır.
- Yürütmenin biçimi sorgudan gelir ve değiştirilemez; veri erişiminin biçimi çözücüden gelir
  ve değiştirilebilir.
- Toplama penceresi bir iş parçacığı kadardır; sırayla beklenen çağrılar ayrı turlar oluşturur
  ve çözücü içindeki gereksiz beklemeler toplamayı bozar.
- Yükleyici istek başına yaratılır; paylaşılan bir yükleyici süresiz önbelleğe dönüşür ve
  ayrı isteklerin verisini karıştırır.

#### Sonraki Adım

Elli ödünç kaydının tamamı tek yanıtta döndü. Kütüphanenin gerçek ödünç sayısı elli değil,
yüz binlerdir ve hiçbir istemci hepsini istemez. Kaynak tasarımı konusunda sayfalama üç
biçimde ele alınmıştı — ofset, imleç ve anahtar tabanlı — ama oradaki tasarım adres ve sorgu
parametreleri üzerine kuruluydu. Sorgu temelli yaklaşımda sayfalama bir uç noktanın değil
bir **alanın** özelliğidir ve iç içe her listede yeniden ortaya çıkar. Sonraki ders imleç
temelli bağlantı modelini kurar, sayfa bilgisinin nereye yazılacağını belirler ve iç içe
listelerde sayfalamanın nasıl davrandığını gösterir.

### Sayfalama Kalıpları

Source: https://academia.sh/tr/kurslar/api-tasarimi/sayfalama-kaliplari

İmleç temelli bağlantı modelinin kurulması, kenar ile düğümün ayrılması, liste değişirken ofset ve imleç sayfalamanın ölçülen davranış farkı ve iç içe listelerde sayfalamanın çoğalması.

Önceki dersin sorgusu elli ödünç kaydının tamamını tek yanıtta döndürdü. Kütüphanenin gerçek
ödünç sayısı yüz binlerdir ve hiçbir istemci hepsini istemez.

Kaynak tasarımı konusunda sayfalama üç biçimde ele alınmıştı ama oradaki tasarım adres ve
sorgu parametreleri üzerine kuruluydu: sayfalanacak şey bir uç noktaydı. Sorgu temelli
yaklaşımda sayfalanacak şey bir **alandır** ve iç içe her listede yeniden ortaya çıkar. Bu
ders alan düzeyinde sayfalamanın standartlaşmış biçimini kurar.

Ayrıştırıcı, düzleyici ve çalıştırıcı önceki derslerde yazıldıkları hâliyle kullanılır;
şemaya bu derste bağlantı tipleri eklenir.

#### Bağlantı Modeli

Sayfalanan bir alan liste değil, bir **bağlantı** döndürür. Bağlantının üç parçası vardır.

- **Kenarlar**, sayfadaki her öğe için bir kayıt tutar ve iki şey taşır: öğenin kendisi
  (**düğüm**) ve o öğenin listedeki konumunu gösteren **imleç**.
- **Sayfa bilgisi**, gezinme durumunu bildirir: ileride ve geride sayfa olup olmadığı,
  sayfanın baş ve son imleci.
- **Toplam sayı** gibi bağlantıya özgü alanlar isteğe bağlıdır.

Kenar ile düğümün ayrılması ilk bakışta gereksiz bir katman gibi görünür. Gerekçesi şudur:
imleç öğenin değil, **öğenin bu listedeki konumunun** özelliğidir. Aynı ödünç kaydı iki ayrı
listede farklı imleçler alır; imleç düğümün içine konsaydı bu olanaksızlaşırdı. Aynı yer
ilişkiye özgü başka verileri de taşıyabilir — kaydın listeye eklenme anı, sıralamadaki
puanı — ve bunların hiçbiri ödünç kaydının kendi alanı değildir.

```js
// sayfalama.mjs — imlec temelli sayfalama; imlec siralama anahtarini tasir
// Kayitlar (olusturma, id) ikilisine gore siralidir; imlec bu ikilinin kodlanmis halidir.
export const imlecYaz = (k) => Buffer.from(`${k.olusturma}|${k.id}`).toString("base64url");
export const imlecOku = (i) => { const [olusturma, id] = Buffer.from(i, "base64url").toString().split("|"); return { olusturma, id }; };

const sirala = (a, b) => (a.olusturma === b.olusturma ? a.id.localeCompare(b.id) : a.olusturma.localeCompare(b.olusturma));

// Imlec temelli: "sonra" imlecinden buyuk ilk N kayit
export function imlecSayfasi(kayitlar, { ilk = 10, sonra = null }) {
  const sirali = [...kayitlar].sort(sirala);
  const baslangic = sonra ? sirali.findIndex((k) => sirala(k, imlecOku(sonra)) > 0) : 0;
  const dilim = baslangic < 0 ? [] : sirali.slice(baslangic, baslangic + ilk);
  return {
    kenarlar: dilim.map((k) => ({ imlec: imlecYaz(k), dugum: k })),
    sayfaBilgisi: {
      sonrakiSayfaVar: baslangic >= 0 && baslangic + ilk < sirali.length,
      oncekiSayfaVar: baslangic > 0,
      basImlec: dilim.length ? imlecYaz(dilim[0]) : null,
      sonImlec: dilim.length ? imlecYaz(dilim.at(-1)) : null,
    },
    toplamSayi: sirali.length,
  };
}

// Ofset temelli: siralamadaki N. kayittan basla
export function ofsetSayfasi(kayitlar, { ilk = 10, atla = 0 }) {
  const sirali = [...kayitlar].sort(sirala);
  return { kayitlar: sirali.slice(atla, atla + ilk), toplamSayi: sirali.length };
}
```

İmlecin kodlanmış olması bir gizleme değil, bir **sözleşme kararıdır**. İstemci imleci
çözmemeli, yalnız geri göndermelidir; kodlama, içeriğin sözleşmenin parçası olmadığını
görünür kılar. Sıralama anahtarı değiştiğinde imlecin yapısı da değişir ve bu, imleci
ayrıştırmaya kalkmamış istemcileri etkilemez.

#### Liste Değişirken

Ofset ile imleç arasındaki fark, liste durağanken görünmez. Sayfalar arasında kayıt
eklenip silindiğinde ortaya çıkar.

```js
// kayma.mjs — sayfalar arasinda kayit eklenir/silinirse ofset ile imlecin davranisi
import { imlecSayfasi, ofsetSayfasi } from "./sayfalama.mjs";

const KAYITLAR = Array.from({ length: 9 }, (_, i) =>
  ({ id: `O-${String(i + 1).padStart(2, "0")}`, olusturma: `2026-01-${String(i + 1).padStart(2, "0")}` }));
const SAYFA = 3;

const DEGISIKLIKLER = {
  "başa ekleme": (v) => v.push({ id: "O-00", olusturma: "2026-01-00" }),
  "baştan silme": (v) => v.splice(v.findIndex((k) => k.id === "O-01"), 1),
};

const BICEMLER = {
  ofset: { baslangic: { atla: 0 }, al: (v, d) => ofsetSayfasi(v, { ilk: SAYFA, atla: d.atla }),
           idler: (s) => s.kayitlar.map((k) => k.id), sonraki: (s, d) => ({ atla: d.atla + SAYFA }) },
  imleç: { baslangic: { sonra: null }, al: (v, d) => imlecSayfasi(v, { ilk: SAYFA, sonra: d.sonra }),
           idler: (s) => s.kenarlar.map((k) => k.dugum.id), sonraki: (s) => ({ sonra: s.sayfaBilgisi.sonImlec }) },
};

for (const [degisiklikAdi, degistir] of Object.entries(DEGISIKLIKLER)) {
  console.log(`ilk sayfa okunduktan sonra: ${degisiklikAdi} (sayfa boyu ${SAYFA}, başlangıçta 9 kayıt)`);
  for (const [bicemAdi, b] of Object.entries(BICEMLER)) {
    const veri = [...KAYITLAR];
    const gorulen = [];
    let durum = b.baslangic;
    for (let sayfa = 0; sayfa < 4; sayfa++) {
      const s = b.al(veri, durum);
      const idler = b.idler(s);
      if (!idler.length) break;
      gorulen.push(...idler);
      if (sayfa === 0) degistir(veri);
      durum = b.sonraki(s, durum);
    }
    const tekil = new Set(gorulen);
    const atlanan = KAYITLAR.map((k) => k.id).filter((id) => !tekil.has(id) && veri.some((k) => k.id === id));
    console.log(`  ${bicemAdi}: ${gorulen.join(" ")}`);
    console.log(`  ${bicemAdi}: görülen=${gorulen.length} tekil=${tekil.size} yinelenen=${gorulen.length - tekil.size} atlanan=${atlanan.join(",") || "yok"}`);
  }
  console.log();
}
```

```
ilk sayfa okunduktan sonra: başa ekleme (sayfa boyu 3, başlangıçta 9 kayıt)
  ofset: O-01 O-02 O-03 O-03 O-04 O-05 O-06 O-07 O-08 O-09
  ofset: görülen=10 tekil=9 yinelenen=1 atlanan=yok
  imleç: O-01 O-02 O-03 O-04 O-05 O-06 O-07 O-08 O-09
  imleç: görülen=9 tekil=9 yinelenen=0 atlanan=yok

ilk sayfa okunduktan sonra: baştan silme (sayfa boyu 3, başlangıçta 9 kayıt)
  ofset: O-01 O-02 O-03 O-05 O-06 O-07 O-08 O-09
  ofset: görülen=8 tekil=8 yinelenen=0 atlanan=O-04
  imleç: O-01 O-02 O-03 O-04 O-05 O-06 O-07 O-08 O-09
  imleç: görülen=9 tekil=9 yinelenen=0 atlanan=yok
```

Ofset iki ayrı biçimde yanılıyor. Listeye kayıt eklendiğinde her şey bir konum kayar ve
ikinci sayfa birinci sayfanın son kaydını yeniden gösterir; `O-03` iki kez görülmüştür.
Kayıt silindiğinde ters yönde kayma olur ve bir kayıt hiç görülmez; `O-04` atlanmıştır.

İmleç iki durumda da doğru gezindi. Nedeni, sorulan sorunun farklı olmasıdır: ofset "baştan
altıncı kayıttan itibaren ver" diye sorar ve baş hareket ettiğinde yanlış yeri gösterir;
imleç "şu kayıttan sonrasını ver" diye sorar ve o kayıt yerinde durduğu sürece doğru yanıtı
alır.

Atlanan kaydın maliyeti yinelenen kayıttan yüksektir. Yinelenme kullanıcıya görünür ve
düzeltilebilir; atlama sessizdir. Bir dışa aktarma işi ofsetle sayfalanıyorsa, eksik
kayıtla biten bir dosya hiçbir hata üretmez.

#### Bağlantının Çalıştırılması

Bağlantı ancak şemada tanımlıysa sorulabilir. Kenar, sayfa bilgisi ve bağlantının kendisi
sıradan nesne tipleridir; ayrıcalıklı bir yanları yoktur. Şema ve Tip Sistemi dersindeki tip
kaydı olduğu gibi kalır, üzerine bu üç tipi ve ikisi de bağlantı döndüren iki alanı ekleyen
bir uzantı konur.

```js
// sema-baglanti.mjs — 01. dersteki semaya baglanti tipleri eklenmis hali
import { SEMA } from "./sema.mjs";

const BAGLANTI_ALANI = { tip: "OduncBaglantisi!", argumanlar: { ilk: "Int", sonra: "String" } };

export const SEMA_B = {
  ...SEMA,
  tipler: {
    ...SEMA.tipler,
    Boolean:        { tur: "skaler" },
    SayfaBilgisi:   { tur: "nesne", alanlar: { sonrakiSayfaVar: "Boolean!", oncekiSayfaVar: "Boolean!",
                                               basImlec: "String", sonImlec: "String" } },
    OduncKenari:    { tur: "nesne", alanlar: { imlec: "String!", dugum: "Odunc!" } },
    OduncBaglantisi:{ tur: "nesne", alanlar: { kenarlar: "[OduncKenari!]!",
                                               sayfaBilgisi: "SayfaBilgisi!", toplamSayi: "Int!" } },

    Uye:   { ...SEMA.tipler.Uye,
             alanlar: { ...SEMA.tipler.Uye.alanlar, oduncBaglantisi: BAGLANTI_ALANI } },
    Sorgu: { ...SEMA.tipler.Sorgu,
             alanlar: { ...SEMA.tipler.Sorgu.alanlar, oduncBaglantisi: BAGLANTI_ALANI } },
  },
};
```

Veri kaynağı dokuz ödünç kaydı ve üç üye içerir. Bağlantı çözücüleri kendileri sayfalama
yapmaz; kayıtları `sayfalama.mjs` içindeki imleç sayfasına verir ve dönen yapıyı olduğu gibi
döndürürler. Kenar, sayfa bilgisi ve toplam sayı alanları için çözücü yazılmamıştır, çünkü
dönen yapı bu adları zaten taşır.

```js
// veri.mjs — dokuz odunc kaydi, uc uye ve baglanti cozuculeri
import { imlecSayfasi } from "./sayfalama.mjs";

export const UYELER = new Map(Array.from({ length: 3 }, (_, i) =>
  [`U-${1001 + i}`, { id: `U-${1001 + i}`, olusturma: "2025-01-01", ad: `Üye ${i}` }]));

export const ODUNCLER = Array.from({ length: 9 }, (_, i) => ({
  id: `O-${String(i + 1).padStart(2, "0")}`, olusturma: `2026-01-${String(i + 1).padStart(2, "0")}`,
  uyeId: `U-${1001 + (i % 3)}`, durum: i % 3 === 0 ? "KAPALI" : "ACIK", iadeTarihi: "2026-04-01",
  kalemler: [{ isbn: "978-0262033848", sube: "merkez" }],
}));

const sayfala = (kayitlar, a) => imlecSayfasi(kayitlar, { ilk: a.ilk ?? 10, sonra: a.sonra ?? null });

export const COZUCULER = {
  __tip: { Kayit: (d) => (d.kalemler ? "Odunc" : "Uye") },
  Sorgu: {
    uye: (_, a) => UYELER.get(a.kimlik) ?? null,
    oduncBaglantisi: (_, a) => sayfala(ODUNCLER, a),
  },
  Uye:   { oduncBaglantisi: (u, a) => sayfala(ODUNCLER.filter((o) => o.uyeId === u.id), a) },
  Odunc: { uye: (o) => UYELER.get(o.uyeId) },
};
```

```js
// baglanti.mjs — baglanti modelini calistirir; ic ice sayfalamayi olcer
import { ayristir } from "./ayristirici.mjs";
import { calistir } from "./calistirici.mjs";
import { SEMA_B as SEMA } from "./sema-baglanti.mjs";
import { COZUCULER } from "./veri.mjs";

const SAYFA = `query($ilk: Int!, $sonra: String) {
  oduncBaglantisi(ilk: $ilk, sonra: $sonra) {
    toplamSayi
    sayfaBilgisi { sonrakiSayfaVar oncekiSayfaVar sonImlec }
    kenarlar { imlec dugum { id durum } }
  }
}`;

// Imleci izleyerek butun sayfalari gez
let sonra = null, sayfa = 0, toplamKenar = 0;
while (true) {
  const s = await calistir(SEMA, COZUCULER, ayristir(SAYFA), { degiskenler: { ilk: 4, sonra } });
  const b = s.data.oduncBaglantisi;
  toplamKenar += b.kenarlar.length;
  console.log(`sayfa ${++sayfa}: ${b.kenarlar.map((k) => k.dugum.id).join(" ")}   sonraki=${b.sayfaBilgisi.sonrakiSayfaVar}  önceki=${b.sayfaBilgisi.oncekiSayfaVar}`);
  if (!b.sayfaBilgisi.sonrakiSayfaVar) { console.log(`toplam kayıt=${b.toplamSayi}  gezilen kenar=${toplamKenar}`); break; }
  sonra = b.sayfaBilgisi.sonImlec;
}

console.log(`\nilk sayfanın ilk kenarı: ${JSON.stringify((await calistir(SEMA, COZUCULER, ayristir(SAYFA), { degiskenler: { ilk: 1, sonra: null } })).data.oduncBaglantisi.kenarlar[0])}`);

// Ic ice sayfalama: her uyenin kendi odunc baglantisi ayri sayfalanir
const ICICE = `{
  u1: uye(kimlik: "U-1001") { ad oduncBaglantisi(ilk: 2) { toplamSayi kenarlar { dugum { id } } } }
  u2: uye(kimlik: "U-1002") { ad oduncBaglantisi(ilk: 2) { toplamSayi kenarlar { dugum { id } } } }
}`;
const ic = (await calistir(SEMA, COZUCULER, ayristir(ICICE))).data;
for (const [anahtar, u] of Object.entries(ic)) {
  console.log(`${anahtar} ${u.ad}: ${u.oduncBaglantisi.kenarlar.map((k) => k.dugum.id).join(" ")}  (toplam ${u.oduncBaglantisi.toplamSayi})`);
}
```

```
sayfa 1: O-01 O-02 O-03 O-04   sonraki=true  önceki=false
sayfa 2: O-05 O-06 O-07 O-08   sonraki=true  önceki=true
sayfa 3: O-09   sonraki=false  önceki=true
toplam kayıt=9  gezilen kenar=9

ilk sayfanın ilk kenarı: {"imlec":"MjAyNi0wMS0wMXxPLTAx","dugum":{"id":"O-01","durum":"KAPALI"}}
u1 Üye 0: O-01 O-04  (toplam 3)
u2 Üye 1: O-02 O-05  (toplam 3)
```

Üç sayfada dokuz kenar gezildi; yinelenme yok, atlama yok. İmleç değeri kodlanmış hâliyle
görünüyor ve içeriğinin okunmasının istenmediği buradan anlaşılıyor.

Son iki satır sorgu temelli sayfalamanın kaynak temelli sayfalamadan ayrıldığı yeri
gösteriyor. İki üyenin ödünç bağlantısı **ayrı ayrı** sayfalandı; her birinin kendi
`toplamSayi` değeri, kendi kenarları var. Bir uç nokta tasarımında bu, iki ayrı istek
demektir; burada tek sorgunun iki dalıdır.

Bunun sonucu, sayfalamanın artık tek bir sınır olmamasıdır. Kök alandan `ilk: 20` üye
istenip her üyenin `ilk: 20` ödüncü istenirse yanıt dört yüz ödünç kaydı taşır. Her alan
kendi sınırına uyar ve hepsi uyduğu hâlde toplam beklenenden büyük çıkar. Sayfalama tek
başına yanıtın boyutunu sınırlamaz.

#### Özet

- Sayfalanan alan liste değil bağlantı döndürür; bağlantı kenarlardan, sayfa bilgisinden ve
  bağlantıya özgü alanlardan oluşur.
- İmleç öğenin değil öğenin o listedeki konumunun özelliğidir; kenar katmanı bu yüzden
  vardır ve ilişkiye özgü başka verileri de taşıyabilir.
- İmleç kodlanmış tutulur; bu bir gizleme değil, içeriğinin sözleşmeye dahil olmadığını
  bildiren bir karardır.
- Liste değişirken ofset sayfalama kayıt eklenmesinde yinelenme, silinmesinde atlama üretir;
  imleç sayfalama iki durumda da doğru gezinir.
- Atlanan kaydın maliyeti yinelenenden yüksektir, çünkü atlama sessizdir ve hiçbir hata
  üretmez.
- Sayfalama alan başına uygulanır; iç içe listelerde her alan kendi sınırına uyar ama
  toplam yanıt boyutu sınırlardan bağımsız olarak büyüyebilir.

#### Sonraki Adım

Bağlantı modeli yanıtın hangi kayıtları taşıyacağını düzenledi ama bir varsayımı korudu:
sorulan her alan okunabilir ve her alan bir değer döndürür. Kütüphanede bu doğru değildir.
Bir üyenin ceza bilgisini yalnız o üye ve görevliler görebilir; ödünç kaydının notu yalnız
kaydı açan şubede okunabilir. Bir alanın yetkisiz olması, bütün sorgunun reddedilmesini
gerektirmez — yanıtın geri kalanı hâlâ geçerlidir. Sonraki ders alan düzeyinde yetkilendirmeyi
kurar, tek bir alanın çökmesinin yanıtın nesine dokunduğunu gösterir ve kısmi veriyle
hatanın aynı yanıtta nasıl birlikte döndüğünü ölçer.

### Hata ve Yetkilendirme

Source: https://academia.sh/tr/kurslar/api-tasarimi/hata-ve-yetkilendirme

Alan düzeyinde hata toplama, kısmi veriyle hatanın aynı yanıtta dönmesi, çözücüleri sarmalayan yetki katmanı ve zorunlu alanlarda boşluğun üst düzeylere yayılmasının ölçülmesi.

Bağlantı modeli yanıtın hangi kayıtları taşıyacağını düzenledi ama bir varsayımı korudu:
sorulan her alan okunabilir ve her alan bir değer döndürür.

Kütüphanede bu doğru değildir. Bir üyenin ceza bilgisini yalnız o üye ve görevliler
görebilir; ödünç kaydının notu yalnız kaydın açıldığı şubede okunabilir. Ayrıca alanların
bir bölümü başka servislerden gelir ve o servisler düşebilir. Bu ders iki soruyu birlikte
yanıtlar: bir alan çöktüğünde yanıtın geri kalanına ne olur ve yetki denetimi nereye
yazılır?

Ayrıştırıcı, düzleyici ve şema önceki derslerde yazıldıkları hâliyle kullanılır;
çalıştırıcı bu derste hata toplayacak biçimde yeniden yazılır.

#### Alan Düzeyinde Hata

Kaynak temelli bir uç noktada hata bütün yanıtı kaplar: durum kodu ya başarıdır ya
değildir. Sorgu temelli yaklaşımda tek bir yanıt yüzlerce alan taşır ve bunlardan birinin
çökmesi diğerlerini geçersiz kılmaz. Yanıt bu yüzden iki bölümlüdür: `data` çözülebilen
alanları, `errors` çözülemeyenleri taşır. İkisi aynı yanıtta bulunur; buna **kısmi başarı**
denir.

Çalıştırıcıya iki şey eklenir. Her alan çağrısı bir `try` bloğuna alınır ve çöken alan
hata listesine, yanıt içindeki yoluyla birlikte yazılır. İkincisi daha incedir: şemada `!` ile işaretlenmiş
bir alana `null` yazılamaz, dolayısıyla o alan çöktüğünde boşluk **yukarı taşınır**.

```js
// calistirici.mjs — alan duzeyinde hata toplayan calistirici
// Bir alan cokerse hata listeye yazilir, o alan null olur. Alan "!" ile zorunluysa
// null yazilamaz; bosluk en yakin bos deger alabilen ust alana kadar yukselir.
import { tipCoz, alanTanimi } from "./sema.mjs";
import { duzle } from "./duzle.mjs";

const YAPRAK = new Set(["skaler", "sirali"]);
class BosDegil extends Error {}                       // zorunlu alana null yazma girisimi

function degiskenleriCoz(islem, gelen) {
  const d = {};
  for (const b of islem.degiskenler) {
    const v = gelen[b.ad] ?? b.varsayilan;
    if (v === undefined && b.tip.endsWith("!")) throw new Error(`$${b.ad} değişkeni zorunlu`);
    d[b.ad] = v;
  }
  return d;
}

const argCoz = (argumanlar, dv) =>
  Object.fromEntries(Object.entries(argumanlar).map(([a, d]) => [a, d.tur === "degisken" ? dv[d.ad] : d.deger]));

async function secimCalistir(o, tipAdi, secim, kaynak, yol, sirali = false) {
  const alanlar = duzle(o.sema, o.belge, tipAdi, secim);
  const sonuc = {};
  for (const a of alanlar) sonuc[a.takma] = null;
  const isle = async (a) => { sonuc[a.takma] = await alanCalistir(o, tipAdi, a, kaynak, `${yol}.${a.takma}`); };
  if (sirali) for (const a of alanlar) await isle(a);
  else await Promise.all(alanlar.map(isle));
  return sonuc;
}

async function alanCalistir(o, tipAdi, alan, kaynak, yol) {
  const tanim = alanTanimi(o.sema.tipler[tipAdi], alan.ad);
  const zorunlu = tipCoz(tanim.tip).zorunlu;
  const cozucu = o.cozucular[tipAdi]?.[alan.ad] ?? ((k) => k?.[alan.ad]);
  const bilgi = { alan: alan.ad, ustTip: tipAdi, yol };
  try {
    const deger = await cozucu(kaynak, argCoz(alan.argumanlar, o.dv), o.baglam, bilgi);
    if ((deger === null || deger === undefined) && zorunlu) throw new BosDegil();
    return await tamamla(o, tanim.tip, deger, alan.secim, yol);
  } catch (h) {
    // Yukseltilen bosluk yeniden bildirilmez; asil hata zaten listede.
    if (!(h instanceof BosDegil)) o.hatalar.push({ ileti: h.ileti ?? h.message, kod: h.kod ?? "ic_hata", yol });
    if (zorunlu) throw new BosDegil();
    return null;
  }
}

async function tamamla(o, tipDizgi, deger, secim, yol) {
  if (deger === null || deger === undefined) return null;
  const t = tipCoz(tipDizgi);
  if (t.liste) {
    const ogeZorunlu = /^\[[^\]]+!\]/.test(tipDizgi);      // [Odunc!] mi [Odunc] mu
    return Promise.all(deger.map(async (d, i) => {
      try { return await tamamla(o, t.ad, d, secim, `${yol}[${i}]`); }
      catch (h) { if (h instanceof BosDegil && !ogeZorunlu) return null; throw h; }
    }));
  }
  const tip = o.sema.tipler[t.ad];
  if (YAPRAK.has(tip.tur)) return deger;
  const somut = tip.tur === "nesne" ? t.ad : o.cozucular.__tip[t.ad](deger);
  return secimCalistir(o, somut, secim, deger, yol);
}

export async function calistir(sema, cozucular, belge, { islemAdi, degiskenler = {}, baglam = {} } = {}) {
  const islem = islemAdi ? belge.islemler.find((i) => i.ad === islemAdi) : belge.islemler[0];
  const o = { sema, cozucular, belge, dv: degiskenleriCoz(islem, degiskenler), baglam, hatalar: [] };
  const kok = islem.tur === "mutation" ? sema.mutasyon : sema.sorgu;
  let data = null;
  try { data = await secimCalistir(o, kok, islem.secim, null, "", islem.tur === "mutation"); }
  catch (h) { if (!(h instanceof BosDegil)) throw h; }      // kokte bosluk: data null olur
  return o.hatalar.length ? { data, errors: o.hatalar } : { data };
}
```

Hata kaydının `yol` alanı, önceki konudaki alan düzeyi doğrulama hatalarıyla aynı işi
görür: istemcinin hatayı yanıttaki yerine bağlamasını sağlar. Fark, yolun burada istek
gövdesini değil **yanıt ağacını** göstermesidir.

#### Yetkilendirme Nereye Yazılır

Yetki denetimi çözücünün içine yazılabilir ama o zaman her çözücüde tekrarlanır ve yeni bir
çözücü yazan kişinin unutması sessizce bir sızıntı üretir. Denetim, çözücüleri sarmalayan
ayrı bir katmanda toplanır.

```js
// yetki.mjs — alan duzeyinde erisim denetimi; cozuculeri sarmalar
export class YetkiHatasi extends Error {
  constructor(alan) { super(`"${alan}" alanını görme yetkiniz yok.`); this.kod = "yetkisiz"; this.ileti = this.message; }
}

// kurallar: { Tip: { alan: (kaynak, baglam) => boolean } }
export function yetkiUygula(cozucular, kurallar) {
  const yeni = { ...cozucular };
  for (const [tip, alanlar] of Object.entries(kurallar)) {
    yeni[tip] = { ...(cozucular[tip] ?? {}) };
    for (const [alan, izinli] of Object.entries(alanlar)) {
      const asil = cozucular[tip]?.[alan] ?? ((k) => k?.[alan]);
      yeni[tip][alan] = (kaynak, arg, baglam, bilgi) => {
        if (!izinli(kaynak, baglam)) throw new YetkiHatasi(`${tip}.${alan}`);
        return asil(kaynak, arg, baglam, bilgi);
      };
    }
  }
  return yeni;
}
```

Kuralın iki girdisi vardır ve ikisi de gereklidir. **Bağlam** isteği yapanı taşır; **kaynak**
ise üzerinde karar verilen kaydı. "Üye kendi cezasını görebilir" kuralı ikisini
karşılaştırmadan yazılamaz. Bu, yetki denetiminin neden alan düzeyinde durduğunu da
açıklar: karar, kaydın kendisine bakmadan verilemez ve kayıt ancak o alanın çözücüsü
çağrılırken elde olur.

Korunacak iki alan şemaya eklenir. İkisi de boş değer alabilir yazılmıştır ve bu bir
zorunluluktur: yetkisiz bir alan `null` döner, dolayısıyla yetkiye bağlı hiçbir alan `!`
ile işaretlenemez. Bu kısıtın gerekçesi dersin sonunda ölçülecek.

```js
// sema-yetki.mjs — 01. dersteki semaya korunacak iki alan eklenmis hali
import { SEMA } from "./sema.mjs";

export const SEMA_Y = {
  ...SEMA,
  tipler: {
    ...SEMA.tipler,
    Uye:   { ...SEMA.tipler.Uye,   alanlar: { ...SEMA.tipler.Uye.alanlar,   ceza: "Int" } },
    Odunc: { ...SEMA.tipler.Odunc, alanlar: { ...SEMA.tipler.Odunc.alanlar, not: "String" } },
  },
};
```

```js
// veri.mjs — odunc verisi ve cozuculer; ceza ve not alanlari korumaya alinacak
export const UYELER = new Map([
  ["U-1001", { id: "U-1001", olusturma: "2024-02-11", ad: "Ayse Yilmaz", ceza: 0 }],
  ["U-1002", { id: "U-1002", olusturma: "2025-06-03", ad: "Deniz Kaya", ceza: 12 }],
]);
export const ODUNCLER = new Map([
  ["O-1", { id: "O-1", olusturma: "2026-01-04", uyeId: "U-1001", durum: "ACIK", iadeTarihi: "2026-03-20",
            sube: "merkez", not: "Kapak zedeli teslim alındı.", kalemler: [{ isbn: "978-0262033848", sube: "merkez" }] }],
  ["O-2", { id: "O-2", olusturma: "2026-01-19", uyeId: "U-1002", durum: "ACIK", iadeTarihi: "2026-04-02",
            sube: "sahil", not: "Ikinci uzatma verildi.", kalemler: [{ isbn: "978-0201896831", sube: "sahil" }] }],
]);

export const COZUCULER = {
  __tip: { Kayit: (d) => (d.kalemler ? "Odunc" : "Uye") },
  Sorgu: {
    odunc: (_, a) => ODUNCLER.get(a.id) ?? null,
    oduncler: () => [...ODUNCLER.values()],
    uye: (_, a) => UYELER.get(a.kimlik) ?? null,
  },
  Odunc: { uye: (o) => UYELER.get(o.uyeId) },
};
```

```js
// kosu.mjs — kismi hata ve alan duzeyi yetkilendirme
import { ayristir } from "./ayristirici.mjs";
import { calistir } from "./calistirici.mjs";
import { SEMA_Y as SEMA } from "./sema-yetki.mjs";
import { COZUCULER } from "./veri.mjs";
import { yetkiUygula } from "./yetki.mjs";

// Kurallar: ceza yalniz kendi kaydinda, not yalniz kaydin acildigi subede gorulur.
const KURALLAR = {
  Uye: { ceza: (uye, b) => b.kullanici === uye.id || b.rol === "gorevli" },
  Odunc: { not: (odunc, b) => b.sube === odunc.sube },
};
const KORUMALI = yetkiUygula(COZUCULER, KURALLAR);

const SORGU = `{
  oduncler { id durum not uye { ad ceza } }
}`;

const BAGLAMLAR = {
  "üye U-1001, merkez şubede": { kullanici: "U-1001", rol: "uye", sube: "merkez" },
  "görevli, sahil şubede":     { kullanici: "P-7", rol: "gorevli", sube: "sahil" },
};

for (const [ad, baglam] of Object.entries(BAGLAMLAR)) {
  const s = await calistir(SEMA, KORUMALI, ayristir(SORGU), { baglam });
  const alanSayisi = JSON.stringify(s.data).match(/:/g).length;
  console.log(`\n=== ${ad} ===`);
  console.log(`data: ${JSON.stringify(s.data)}`);
  console.log(`hata sayısı: ${(s.errors ?? []).length}   dolu alan sayısı: ${alanSayisi}`);
  for (const h of s.errors ?? []) console.log(`  ${h.kod.padEnd(12)} ${h.yol.padEnd(24)} ${h.ileti}`);
}
```

```
=== üye U-1001, merkez şubede ===
data: {"oduncler":[{"id":"O-1","durum":"ACIK","not":"Kapak zedeli teslim alındı.","uye":{"ad":"Ayse Yilmaz","ceza":0}},{"id":"O-2","durum":"ACIK","not":null,"uye":{"ad":"Deniz Kaya","ceza":null}}]}
hata sayısı: 2   dolu alan sayısı: 13
  yetkisiz     .oduncler[1].not         "Odunc.not" alanını görme yetkiniz yok.
  yetkisiz     .oduncler[1].uye.ceza    "Uye.ceza" alanını görme yetkiniz yok.

=== görevli, sahil şubede ===
data: {"oduncler":[{"id":"O-1","durum":"ACIK","not":null,"uye":{"ad":"Ayse Yilmaz","ceza":0}},{"id":"O-2","durum":"ACIK","not":"Ikinci uzatma verildi.","uye":{"ad":"Deniz Kaya","ceza":12}}]}
hata sayısı: 1   dolu alan sayısı: 13
  yetkisiz     .oduncler[0].not         "Odunc.not" alanını görme yetkiniz yok.
```

Aynı sorgu, iki farklı bağlamda iki farklı yanıt üretti. Her iki durumda da ödünç kayıtları
döndü, kimlikleri ve durumları okundu; yalnız korunan alanlar boş kaldı ve gerekçeleri
`errors` içinde bildirildi.

Görevlinin yanıtında `not` alanının biri boş: görevli olmak ceza görmeye yeter ama not
görmeye yetmez, çünkü not kuralı role değil şubeye bakar. İki kuralın farklı ölçütlere
dayanabilmesi, yetkilendirmenin alan düzeyinde tanımlanmasının kazandırdığı şeydir.

Hata iletisinin ne söylediğine de dikkat edilmelidir: alanın **var olduğunu** söyler ama
değerini vermez. Bir alanın varlığını gizlemek gerekiyorsa yetkilendirme yeterli değildir;
o alan şemadan çıkarılmalı ya da sorgu denetimi sırasında reddedilmelidir.

#### Boşluğun Yayılması

Bir alanın çökmesi her zaman o alanla sınırlı kalmaz. Şemadaki `!` işaretleri, boşluğun
nereye kadar yayılacağını belirler.

Ölçüm için aynı ödünç listesini iki ayrı işaretlemeyle sunan bir alan gerekir. `oduncler`
alanı `[Odunc!]!` yazılmıştır; şemaya aynı kayıtları `[Odunc]` olarak döndüren ikinci bir
kök alan eklenir. İkisi aynı veriyi verir, yalnız işaretlemeleri ayrılır.

```js
// sema-esnek.mjs — ayni odunc listesini bos deger alabilen isaretlemeyle sunan kok alan
import { SEMA_Y } from "./sema-yetki.mjs";

export const SEMA_E = {
  ...SEMA_Y,
  tipler: {
    ...SEMA_Y.tipler,
    Sorgu: { ...SEMA_Y.tipler.Sorgu,
             alanlar: { ...SEMA_Y.tipler.Sorgu.alanlar, esnekOduncler: "[Odunc]" } },
  },
};
```

```js
// bosluk.mjs — bos deger alabilen ve alamayan alanlarda cokmenin yayilmasi
import { ayristir } from "./ayristirici.mjs";
import { calistir } from "./calistirici.mjs";
import { SEMA_E as SEMA } from "./sema-esnek.mjs";
import { ODUNCLER, UYELER } from "./veri.mjs";

const coker = (alan) => (o) => {
  if (o.id === "O-2") throw Object.assign(new Error("Gecikme servisi yanıt vermedi."), { kod: "yukari_akis" });
  return o[alan];
};

const TABAN = {
  __tip: { Kayit: (d) => (d.kalemler ? "Odunc" : "Uye") },
  Sorgu: { oduncler: () => [...ODUNCLER.values()], esnekOduncler: () => [...ODUNCLER.values()] },
  Odunc: { uye: (o) => UYELER.get(o.uyeId) },
};

const DURUMLAR = [
  ["iadeTarihi çöküyor (Tarih — boş olabilir)", "oduncler", { ...TABAN, Odunc: { ...TABAN.Odunc, iadeTarihi: coker("iadeTarihi") } }],
  ["durum çöküyor ([Odunc!]! içinde, Durum!)", "oduncler", { ...TABAN, Odunc: { ...TABAN.Odunc, durum: coker("durum") } }],
  ["durum çöküyor ([Odunc] içinde, Durum!)", "esnekOduncler", { ...TABAN, Odunc: { ...TABAN.Odunc, durum: coker("durum") } }],
];

for (const [ad, kok, cozucular] of DURUMLAR) {
  const s = await calistir(SEMA, cozucular, ayristir(`{ ${kok} { id durum iadeTarihi uye { ad } } }`));
  const metin = JSON.stringify(s.data);
  console.log(`\n${ad}`);
  console.log(`  data: ${metin}`);
  console.log(`  kaybolan: ${metin === "null" ? "tüm yanıt" : (metin.match(/null/g) ?? []).length + " alan"}   hata: ${(s.errors ?? []).map((h) => `${h.yol} (${h.kod})`).join(", ") || "yok"}`);
}
```

```
iadeTarihi çöküyor (Tarih — boş olabilir)
  data: {"oduncler":[{"id":"O-1","durum":"ACIK","iadeTarihi":"2026-03-20","uye":{"ad":"Ayse Yilmaz"}},{"id":"O-2","durum":"ACIK","iadeTarihi":null,"uye":{"ad":"Deniz Kaya"}}]}
  kaybolan: 1 alan   hata: .oduncler[1].iadeTarihi (yukari_akis)

durum çöküyor ([Odunc!]! içinde, Durum!)
  data: null
  kaybolan: tüm yanıt   hata: .oduncler[1].durum (yukari_akis)

durum çöküyor ([Odunc] içinde, Durum!)
  data: {"esnekOduncler":[{"id":"O-1","durum":"ACIK","iadeTarihi":"2026-03-20","uye":{"ad":"Ayse Yilmaz"}},null]}
  kaybolan: 1 alan   hata: .esnekOduncler[1].durum (yukari_akis)
```

Üç satır aynı çökmeyi, üç ayrı şema işaretlemesiyle gösteriyor.

Boş değer alabilen bir alan çöktüğünde yalnız o alan boşalır; ikinci kayıt yerinde kalır.
Zorunlu bir alan çöktüğünde boşluk yukarı taşınır ve **en yakın boş değer alabilen düzeyde**
durur. `[Odunc!]!` işaretlemesinde ne öğe ne de liste boş olabilir; boşluk kök alana kadar
çıkar ve `data` tümüyle null olur. Aynı çökme `[Odunc]` işaretlemesinde ikinci öğede durur
ve yanıtın kalanı kurtulur.

Bu, `!` işaretinin bir tasarım kararı olduğunu gösteriyor. Zorunluluk istemcinin işini
kolaylaştırır — boş denetimi yazmaz — ama bunun bedeli, tek bir alanın çökmesinin bütün
yanıtı götürebilmesidir. Ölçüt şudur: **bir alan gerçekten hiçbir koşulda boş olamıyorsa**
zorunlu yazılır. Başka bir servisten gelen, hesaplanan ya da yetkiye bağlı alanlar bu ölçütü
karşılamaz.

#### Özet

- Sorgu temelli yanıt iki bölümlüdür: `data` çözülebilen alanları, `errors` çözülemeyenleri
  taşır ve ikisi aynı yanıtta bulunur.
- Hata kaydındaki yol, yanıt ağacındaki yeri gösterir ve istemcinin hatayı doğru alana
  bağlamasını sağlar.
- Yetki denetimi çözücülerin içine değil, onları sarmalayan bir katmana yazılır; kural hem
  isteği yapanı hem üzerinde karar verilen kaydı görmek zorundadır.
- Aynı sorgu farklı bağlamlarda farklı alanları boş bırakır; iki kural farklı ölçütlere
  dayanabilir, bu alan düzeyi tanımın kazandırdığı şeydir.
- Yetki hatası alanın var olduğunu söyler ama değerini vermez; alanın varlığını gizlemek
  gerekiyorsa şemadan çıkarılmalıdır.
- Zorunlu alanda çökme, boşluğu en yakın boş değer alabilen düzeye kadar taşır; `[Odunc!]!`
  altında tek bir alan bütün yanıtı, `[Odunc]` altında yalnız bir öğeyi götürür.

#### Sonraki Adım

Yetkilendirme sorgunun **neyi görebileceğini** sınırladı ama **ne kadar isteyebileceğini**
sınırlamadı. Sayfalama dersi bunu açıkta bırakmıştı: her alan kendi sınırına uysa da iç içe
listelerin çarpımı yanıtı büyütebiliyordu. Üstelik tip sistemi çevrimler içerir — ödünç
kaydından üyeye, üyeden ödünçlerine, oradan yeniden üyeye gidilebilir — ve bu, sorgu
derinliğine bir sınır konmadığında yürütmeyi durduracak bir yol bırakmaz. Sonraki ders sorgu
derinliğini ve maliyetini çalıştırmadan önce hesaplar, sınırı aşan sorguyu reddeder ve
sınırın nereden seçileceğini ölçer.

### Sorgu Maliyeti ve Derinlik Sınırı

Source: https://academia.sh/tr/kurslar/api-tasarimi/sorgu-maliyeti-ve-derinlik-siniri

Çevrimli tip sisteminde sorgu derinliğinin sınırsız büyümesi, çalıştırmadan önce hesaplanan maliyet üst sınırı ve derinlik sınırının tek başına yetmediğini gösteren ölçüm.

Yetkilendirme sorgunun neyi görebileceğini sınırladı, ne kadar isteyebileceğini
sınırlamadı. Sayfalama dersi bunu açıkta bırakmıştı: her alan kendi sınırına uysa da iç içe
listelerin çarpımı yanıtı büyütebiliyordu.

Asıl sorun tip sisteminin **çevrimli** olmasıdır. Ödünç kaydından üyeye, üyeden ödünçlerine,
oradan yeniden üyeye gidilebilir. Şemada bu çevrim tek bir satır alan çift yönlü bir
ilişkidir; sorguda ise istenen derinlikte açılabilir. Bu ders sorguyu çalıştırmadan önce
ölçer ve sınırı aşanı reddeder.

Bu dersteki ayrıştırıcı, düzleyici ve çalıştırıcı önceki derslerde yazıldıkları hâliyle
kullanılır; şemaya sayfa boyu argümanı eklenir ve yeni olan ölçüm katmanıdır. Veri kaynağı
üç üye ve altı ödünç kaydı içerir.

```js
// veri.mjs — 3 uye, 6 odunc; tip sistemi cevrimli oldugu icin derinlik sinirsiz gezilebilir
export const UYELER = new Map(Array.from({ length: 3 }, (_, i) =>
  [`U-${1001 + i}`, { id: `U-${1001 + i}`, olusturma: "2025-01-01", ad: `Üye ${i}`, ceza: i * 5 }]));

export const ODUNCLER = Array.from({ length: 6 }, (_, i) => ({
  id: `O-${i + 1}`, olusturma: `2026-01-0${i + 1}`, uyeId: `U-${1001 + (i % 3)}`,
  durum: "ACIK", iadeTarihi: "2026-04-01", sube: "merkez", not: null,
  kalemler: [{ isbn: "978-0262033848", sube: "merkez" }],
}));

export const COZUCULER = {
  __tip: { Kayit: (d) => (d.kalemler ? "Odunc" : "Uye") },
  Sorgu: {
    odunc: (_, a) => ODUNCLER.find((o) => o.id === a.id) ?? null,
    oduncler: (_, a) => ODUNCLER.slice(0, a.ilk ?? ODUNCLER.length),
    uye: (_, a) => UYELER.get(a.kimlik) ?? null,
  },
  Odunc: { uye: (o) => UYELER.get(o.uyeId) },
  Uye: { oduncler: (u, a) => ODUNCLER.filter((o) => o.uyeId === u.id).slice(0, a.ilk ?? ODUNCLER.length) },
};
```

Ölçüm, bir liste alanının kaç kayıt döndüreceğini `ilk` argümanından okur. Argüman şemada
bildirilmemişse onu kullanan sorgu denetimden geçemez; bu yüzden iki liste alanı sayfa boyu
argümanıyla genişletilir. Argümanın **isteğe bağlı** olması dersin sonunda tartışılacak
soruyu şimdiden kuruyor: verilmediğinde ölçüm neye dayanacak?

```js
// sema-liste.mjs — liste alanlarina sayfa boyu argumani eklenmis sema
import { SEMA } from "./sema.mjs";

const LISTE_ALANI = { tip: "[Odunc!]!", argumanlar: { durum: "Durum", ilk: "Int" } };

export const SEMA_L = {
  ...SEMA,
  tipler: {
    ...SEMA.tipler,
    Uye:   { ...SEMA.tipler.Uye,   alanlar: { ...SEMA.tipler.Uye.alanlar,   oduncler: LISTE_ALANI } },
    Sorgu: { ...SEMA.tipler.Sorgu, alanlar: { ...SEMA.tipler.Sorgu.alanlar, oduncler: LISTE_ALANI } },
  },
};
```

#### Maliyetin Hesaplanması

Sorgunun maliyeti, çalıştırılmadan önce yalnız belge ve şemadan hesaplanabilir. Model
basittir: bir alanın üreteceği düğüm sayısı, üst düzeyden gelen çarpanla kendi çarpanının
çarpımıdır. Liste alanlarının çarpanı `ilk` argümanından okunur; argüman verilmemişse
şemanın öngördüğü bir varsayılan kullanılır.

```js
// maliyet.mjs — sorguyu calistirmadan derinligini ve maliyetini hesaplar
// Maliyet modeli: bir alanin uretecegi dugum sayisi = ust carpan × kendi carpani.
// Liste alanlarinin carpani "ilk" argumanindan, yoksa varsayilandan gelir.
import { tipCoz, alanTanimi } from "./sema.mjs";
import { duzle } from "./duzle.mjs";

const YAPRAK = new Set(["skaler", "sirali"]);

export function olc(sema, belge, islem, { varsayilanCarpan = 10, degiskenler = {} } = {}) {
  const kok = islem.tur === "mutation" ? sema.mutasyon : sema.sorgu;
  const carpanOku = (a) => {
    const d = a.argumanlar.ilk;
    if (!d) return null;
    return d.tur === "degisken" ? degiskenler[d.ad] : d.deger;
  };

  let enDerin = 0, maliyet = 0;
  const gez = (tipAdi, secim, derinlik, ustCarpan) => {
    enDerin = Math.max(enDerin, derinlik);
    for (const a of duzle(sema, belge, tipAdi, secim)) {
      const tanim = alanTanimi(sema.tipler[tipAdi], a.ad);
      if (!tanim) continue;
      const t = tipCoz(tanim.tip);
      const carpan = ustCarpan * (t.liste ? (carpanOku(a) ?? varsayilanCarpan) : 1);
      maliyet += carpan;
      if (!YAPRAK.has(sema.tipler[t.ad].tur)) gez(t.ad, a.secim, derinlik + 1, carpan);
    }
  };
  gez(kok, islem.secim, 1, 1);
  return { derinlik: enDerin, maliyet };
}

// Sinirlari asan sorgu calistirilmadan reddedilir.
export function sinirDenetle(olcum, { enCokDerinlik, enCokMaliyet }) {
  const hatalar = [];
  if (olcum.derinlik > enCokDerinlik) hatalar.push(`derinlik ${olcum.derinlik}, sınır ${enCokDerinlik}`);
  if (olcum.maliyet > enCokMaliyet) hatalar.push(`maliyet ${olcum.maliyet}, sınır ${enCokMaliyet}`);
  return hatalar;
}
```

Hesabın parçaları düzleyiciden geçtiğine dikkat edilmeli: parça içine gizlenmiş bir derinlik
ölçüme girer. Ölçüm yalnız yazılı seçim kümesine bakan bir sayaç olsaydı, sorgu parçalara
bölünerek sınır aşılabilirdi.

#### Ölçüm

```js
// kosu.mjs — sorgularin derinligini ve maliyetini olcer, sinirlari uygular, tahmini sinar
import { ayristir } from "./ayristirici.mjs";
import { calistir } from "./calistirici.mjs";
import { olc, sinirDenetle } from "./maliyet.mjs";
import { SEMA_L as SEMA } from "./sema-liste.mjs";
import { COZUCULER } from "./veri.mjs";

const SINIR = { enCokDerinlik: 6, enCokMaliyet: 1000 };

// Cevrimli tip sistemi: odunc -> uye -> oduncler -> uye -> ...
const cevrim = (kat) => {
  let ic = "id";
  for (let i = 0; i < kat; i++) ic = `id uye { ad oduncler { ${ic} } }`;
  return `{ oduncler { ${ic} } }`;
};

const SORGULAR = {
  "olağan sorgu": `{ oduncler { id durum uye { ad } } }`,
  "sayfalı sorgu": `{ oduncler { id uye { ad oduncler { id } } } }`,
  "çevrim ×2": cevrim(2),
  "çevrim ×5": cevrim(5),
  "sığ ama geniş": `{ a: oduncler(ilk: 500) { id uye { ad } } b: oduncler(ilk: 500) { id uye { ad } }
                     c: oduncler(ilk: 500) { id uye { ad } } d: oduncler(ilk: 500) { id uye { ad } } }`,
  "sınırlı sorgu": `{ oduncler(ilk: 6) { id durum uye { ad } } }`,
};

// Yanittaki yaprak deger sayisi: tahminin sinandigi gercek sayi
const yaprakSay = (d) =>
  d === null || typeof d !== "object" ? 1 : Array.isArray(d) ? d.reduce((t, x) => t + yaprakSay(x), 0)
    : Object.values(d).reduce((t, x) => t + yaprakSay(x), 0);

console.log("sorgu             derinlik  maliyet  karar      yanıttaki yaprak");
for (const [ad, metin] of Object.entries(SORGULAR)) {
  const belge = ayristir(metin);
  const olcum = olc(SEMA, belge, belge.islemler[0], { varsayilanCarpan: 6 });
  const hatalar = sinirDenetle(olcum, SINIR);
  let yaprak = "—";
  if (!hatalar.length) yaprak = String(yaprakSay((await calistir(SEMA, COZUCULER, belge)).data));
  console.log(`${ad.padEnd(17)} ${String(olcum.derinlik).padStart(8)}  ${String(olcum.maliyet).padStart(7)}  ` +
    `${(hatalar.length ? "RED" : "kabul").padEnd(9)}  ${yaprak.padStart(15)}${hatalar.length ? "   (" + hatalar.join("; ") + ")" : ""}`);
}
```

```
sorgu             derinlik  maliyet  karar      yanıttaki yaprak
olağan sorgu             3       30  kabul                   18
sayfalı sorgu            4       96  kabul                   24
çevrim ×2                6      600  kabul                   60
çevrim ×5               12   130632  RED                      —   (derinlik 12, sınır 6; maliyet 130632, sınır 1000)
sığ ama geniş            3     8000  RED                      —   (maliyet 8000, sınır 1000)
sınırlı sorgu            3       30  kabul                   18
```

#### Üç Satırın Söyledikleri

**Çevrim ×5 satırı** sorunun büyüklüğünü gösteriyor. Beş kat açılmış bir çevrim, on iki
derinliğe ve yüz otuz bini aşan bir maliyete ulaşıyor. Bu sorgu yirmi satırdan kısadır ve
yazması bir dakika sürer; çalıştırılırsa altı ödünç kaydının bulunduğu bir veri kaynağında
bile işlemciyi uzun süre meşgul eder. Gerçek bir kütüphane verisinde sonuç bellek
tükenmesidir.

**Sığ ama geniş satırı** derinlik sınırının neden yetmediğini gösteriyor. Bu sorgunun
derinliği üçtür ve altı olan sınırın çok altındadır; ama dört kök alanın her biri beş yüz
kayıt istediği için maliyeti sekiz bine çıkar. Yalnız derinliğe bakan bir koruma bu sorguyu
geçirirdi. **Derinlik bir kalkan değil, bir tavan ölçüsüdür; maliyet asıl sınırı çizer.**

**Sınırlı sorgu satırı** tahminin niteliğini gösteriyor. `ilk: 6` verildiğinde maliyet otuz
hesaplanıyor, yanıtta on sekiz yaprak dönüyor. Tahmin gerçeğin üstündedir ve öyle olmalıdır:
maliyet bileşik alanları da sayar, gerçek yaprak sayısı yalnız uçları. Bir kabul kararı
tahmine dayanacaksa tahminin **üst sınır** olması gerekir; altında kalan bir tahmin, geçmesi
gerekmeyen sorguları geçirir.

#### Sınırın Nereden Seçileceği

İki sayı da keyfî görünür. Seçimin dayanağı ölçümdür.

Maliyet sınırı, sunucunun karşılayabileceği yükten türetilir: bir isteğin ürettiği düğüm
sayısıyla harcadığı süre arasındaki ilişki ölçülür, kabul edilebilir en uzun yanıt süresine
karşılık gelen düğüm sayısı sınır olur. Derinlik sınırı ise gerçek istemcilerin
kullandığı en derin sorgudan türetilir; kütükteki sorgu derinliklerinin dağılımına bakılıp
gerçek kullanımın üstünde bir değer seçilir.

Varsayılan çarpan üçüncü ve en kritik sayıdır. `ilk` argümanı verilmemiş bir liste alanının
kaç kayıt döndüreceği bilinemez, dolayısıyla tahmin bir varsayıma dayanır. İki çözüm
vardır: liste alanlarında `ilk` argümanını **zorunlu** kılmak, ya da şemada alan başına bir
üst sınır bildirmek. İkisi de yapılmazsa varsayılan çarpan bir tahmin olarak kalır ve
gerçek yanıt tahminin üstüne çıkabilir; bu da maliyet korumasının temel varsayımını bozar.

Reddin nasıl bildirileceği ayrı bir karardır. Sorgu çalıştırılmadığı için kısmi veri yoktur;
yanıt yalnız hata taşır. Hatanın **ölçülen değerleri ve sınırları** bildirmesi gerekir,
çünkü istemcinin yapması gereken şey sorgusunu küçültmektir ve neyi küçülteceğini
bilmelidir. Yalnız "sorgu çok karmaşık" diyen bir hata, geliştiriciyi deneme yanılmaya
bırakır.

#### Özet

- Çevrimli tip sistemi sorgu derinliğine kendiliğinden bir sınır koymaz; kısa bir sorgu
  keyfî derinliğe açılabilir.
- Maliyet, yalnız belge ve şemadan çalıştırmadan önce hesaplanır: bir alanın üreteceği düğüm
  sayısı üst çarpanla kendi çarpanının çarpımıdır.
- Ölçüm parçalar açıldıktan sonra yapılır; yoksa sorgu parçalara bölünerek sınır aşılabilir.
- Derinlik sınırı tek başına yetmez: derinliği üç olan bir sorgu, kök alanları çoğaltarak
  maliyet sınırını sekiz katına çıkarabilir.
- Maliyet tahmini gerçek yaprak sayısının üstünde olmalıdır; altında kalan bir tahmin
  geçmemesi gereken sorguları geçirir.
- Varsayılan çarpanın güvenilir olması için liste alanlarında `ilk` zorunlu kılınmalı ya da
  şemada alan başına üst sınır bildirilmelidir; ret bildirimi ölçülen değeri ve sınırı
  içermelidir.

#### Sonraki Adım

Buraya kadar yazılan her şey tek bir süreç içinde çalıştı: sorgu bir dizgiydi, sonuç bir
nesne. Gerçek bir servis sorguyu ağ üzerinden alır ve yanıtı ağ üzerinden döndürür. Bu,
yalnız bir sarmalama işi değildir. Tek uç nokta ve tek yöntem kullanan bir aktarım,
önceki konuda kurulan her şeyi — adres tabanlı önbellekleme, durum kodu eşlemesi, aracıların
gördüğü bilgi — yeniden düşünmeyi gerektirir. Sonraki ders GraphQL'i HTTP üzerine oturtur ve
bu aktarım kararının önbelleklemeye, hata bildirimine ve ölçüme ne yaptığını ölçer.

### HTTP Üzerinden GraphQL

Source: https://academia.sh/tr/kurslar/api-tasarimi/http-uzerinden-graphql

Tek uç nokta üzerinden aktarım, POST ile GET arasındaki önbellekleme farkı, önceden kaydedilmiş belgeler ve tek adresin aracılara ve kütüklere bıraktığı bilginin ölçülmesi.

Buraya kadar yazılan her şey tek bir süreç içinde çalıştı: sorgu bir dizgiydi, sonuç bir
nesne. Gerçek bir servis sorguyu ağ üzerinden alır ve yanıtı ağ üzerinden döndürür.

Bu yalnız bir sarmalama işi değildir. Tek uç nokta ve çoğunlukla tek yöntem kullanan bir
aktarım, önceki konuda kurulan her şeyi yeniden düşünmeyi gerektirir: adres tabanlı
önbellekleme, durum kodu eşlemesi, aracıların ve kütüklerin gördüğü bilgi. Bu ders
çalıştırıcıyı HTTP'ye oturtur ve bu kararın ne götürüp ne getirdiğini ölçer.

#### Sunucu

Sunucu üç aktarım biçimini destekler. `POST` gövdesinde sorguyu, değişkenleri ve işlem
adını taşır. `GET` aynı üçlüyü sorgu parametrelerinde taşır. Üçüncüsü, sorgu metni yerine
**önceden kaydedilmiş belgenin kimliğini** taşır.

Önceki derslerde yazılan ayrıştırıcı, denetleyici, şema, maliyet ölçeri ve çalıştırıcı
olduğu gibi kullanılır; sunucu bunları sırayla çağıran ince bir katmandır.

```js
// sunucu.mjs — GraphQL'i HTTP uzerine oturtur
//   POST /graphql            govdede { query, variables, operationName }
//   GET  /graphql?query=...  sorgu adreste; onbelleklenebilir
//   GET  /graphql?kimlik=... onceden kaydedilmis belge; kisa ve izin listesiyle sinirli
import { createServer } from "node:http";
import { createHash } from "node:crypto";
import { ayristir } from "./ayristirici.mjs";
import { denetle } from "./denetle.mjs";
import { calistir } from "./calistirici.mjs";
import { olc, sinirDenetle } from "./maliyet.mjs";
import { SEMA_L as SEMA } from "./sema-liste.mjs";
import { COZUCULER } from "./veri.mjs";

const SINIR = { enCokDerinlik: 6, enCokMaliyet: 1000 };
export const kimlikYaz = (metin) => createHash("sha256").update(metin).digest("hex").slice(0, 12);

// Onceden kaydedilmis belgeler: istemci yalniz kimligini gonderir.
export const KAYITLI = new Map([
  `query OduncListesi { oduncler(ilk: 6) { id durum } }`,
  `query OduncAyrinti($id: ID!) { odunc(id: $id) { id durum uye { ad } } }`,
].map((m) => [kimlikYaz(m), m]));

const govdeOku = (istek) =>
  new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); });

const yaz = (yanit, kod, nesne, ekBaslik = {}) => {
  yanit.writeHead(kod, { "content-type": "application/graphql-response+json; charset=utf-8", ...ekBaslik });
  yanit.end(JSON.stringify(nesne));
};

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");
  if (adres.pathname !== "/graphql") return yaz(yanit, 404, { errors: [{ ileti: "yol yok", kod: "yol_yok" }] });

  let metin, degiskenler = {}, islemAdi;
  if (istek.method === "POST") {
    const g = JSON.parse((await govdeOku(istek)) || "{}");
    ({ query: metin, variables: degiskenler = {}, operationName: islemAdi } = g);
  } else {
    const kimlik = adres.searchParams.get("kimlik");
    metin = kimlik ? KAYITLI.get(kimlik) : adres.searchParams.get("query");
    degiskenler = JSON.parse(adres.searchParams.get("variables") ?? "{}");
    islemAdi = adres.searchParams.get("operationName") ?? undefined;
    if (kimlik && !metin) return yaz(yanit, 404, { errors: [{ ileti: "kayıtlı belge yok", kod: "belge_yok" }] });
  }

  let belge;
  try { belge = ayristir(metin ?? ""); }
  catch (h) { return yaz(yanit, 400, { errors: [{ ileti: h.message, kod: "ayristirma" }] }); }

  const denetimHatalari = denetle(SEMA, belge);
  if (denetimHatalari.length) return yaz(yanit, 400, { errors: denetimHatalari.map((i) => ({ ileti: i, kod: "denetim" })) });

  const islem = islemAdi ? belge.islemler.find((i) => i.ad === islemAdi) : belge.islemler[0];
  const olcum = olc(SEMA, belge, islem, { varsayilanCarpan: 6, degiskenler });
  const asim = sinirDenetle(olcum, SINIR);
  if (asim.length) return yaz(yanit, 400, { errors: asim.map((i) => ({ ileti: i, kod: "sinir" })) });

  const sonuc = await calistir(SEMA, COZUCULER, belge, { islemAdi, degiskenler });
  // Alan duzeyi hata kismi basaridir: istek yerine getirilmistir, 200 doner.
  const baslik = istek.method === "GET" ? { "cache-control": "public, max-age=30" } : { "cache-control": "no-store" };
  yaz(yanit, 200, sonuc, { ...baslik, "graphql-islem": islem.ad ?? "adsiz" });
}).listen(8441, "127.0.0.1", () => {
  console.log("graphql sunucusu 127.0.0.1:8441");
  for (const [k, m] of KAYITLI) console.log(`kayitli ${k} ${/query (\w+)/.exec(m)[1]}`);
});
```

```bash
#!/usr/bin/env bash
# Uc aktarim bicimi ve iki hata durumu
node sunucu.mjs > /tmp/baslangic & s=$!
sleep 0.6
cat /tmp/baslangic
KIMLIK=$(grep 'OduncAyrinti' /tmp/baslangic | cut -d' ' -f2)

goster() { printf -- '--- %s ---\n' "$1"; shift; curl -sS -D - -o /tmp/g "$@" | grep -iE '^HTTP|^cache-control|^graphql-islem' | tr -d '\r'; cat /tmp/g; echo; }

goster "POST (gövdede sorgu)" -X POST -H 'content-type: application/json' \
  -d '{"query":"query OduncAyrinti($id: ID!) { odunc(id: $id) { id durum uye { ad } } }","variables":{"id":"O-2"}}' \
  http://127.0.0.1:8441/graphql

goster "GET (adreste sorgu)" --get --data-urlencode 'query=query OduncListesi { oduncler(ilk: 2) { id durum } }' \
  http://127.0.0.1:8441/graphql

goster "GET (kayıtlı belge)" "http://127.0.0.1:8441/graphql?kimlik=$KIMLIK&variables=%7B%22id%22%3A%22O-1%22%7D"

goster "şemada olmayan alan" -X POST -H 'content-type: application/json' \
  -d '{"query":"{ odunc(id: \"O-1\") { gecikmeCezasi } }"}' http://127.0.0.1:8441/graphql

goster "sınır aşımı" -X POST -H 'content-type: application/json' \
  -d '{"query":"{ oduncler(ilk: 500) { id uye { ad oduncler(ilk: 500) { id } } } }"}' http://127.0.0.1:8441/graphql

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

```
graphql sunucusu 127.0.0.1:8441
kayitli e51e709a4e07 OduncListesi
kayitli dcb9c7bb85fa OduncAyrinti
--- POST (gövdede sorgu) ---
HTTP/1.1 200 OK
cache-control: no-store
graphql-islem: OduncAyrinti
{"data":{"odunc":{"id":"O-2","durum":"ACIK","uye":{"ad":"Üye 1"}}}}
--- GET (adreste sorgu) ---
HTTP/1.1 200 OK
cache-control: public, max-age=30
graphql-islem: OduncListesi
{"data":{"oduncler":[{"id":"O-1","durum":"ACIK"},{"id":"O-2","durum":"ACIK"}]}}
--- GET (kayıtlı belge) ---
HTTP/1.1 200 OK
cache-control: public, max-age=30
graphql-islem: OduncAyrinti
{"data":{"odunc":{"id":"O-1","durum":"ACIK","uye":{"ad":"Üye 0"}}}}
--- şemada olmayan alan ---
HTTP/1.1 400 Bad Request
{"errors":[{"ileti":"Sorgu.odunc: Odunc tipinde \"gecikmeCezasi\" alanı yok","kod":"denetim"}]}
--- sınır aşımı ---
HTTP/1.1 400 Bad Request
{"errors":[{"ileti":"maliyet 502000, sınır 1000","kod":"sinir"}]}
```

Belge kimlikleri karma işlevinden türetildiği için makineden makineye değişmez, ama belge
metni değiştiğinde kimlik de değişir.

#### Durum Kodunun Anlamı

Ölçümdeki sunucunun yanıtları iki gruba ayrılıyor ve ayrım keyfî değildir.

**400 dönen iki durum**, sorgunun hiç çalıştırılmadığı durumlardır: belge ayrıştırılamamış,
şemaya uymuyor ya da sınırı aşıyor. İstek yerine getirilmemiştir; istemcinin **gönderdiği
şey** hatalıdır ve yeniden denemek işe yaramaz.

**200 dönen durumlar**, sorgunun çalıştırıldığı durumlardır — alan düzeyinde hata bulunsa
bile. Önceki derste kurulan kısmi başarı budur: bir alanın çökmesi isteğin yerine
getirilmediği anlamına gelmez, çünkü yanıtın geri kalanı geçerlidir. Yanıtta hem `data` hem
`errors` bulunabilir ve durum kodu bunu bildiremez.

Ayrım, önceki konudaki 400 ile 422 ayrımının aynısıdır: anlaşılamayan istek ile anlaşılıp
kısmen karşılanan istek farklı şeylerdir. Buradaki fark, ikinci grubun başarısız
sayılmamasıdır.

**GraphQL over HTTP** belirtimi aynı ayrımı daha ince yapar ve üç koda dağıtır:
ayrıştırılamayan belge `400`, doğrulamadan geçemeyen belge `422`, hem `data` hem `errors`
taşıyan yanıt ise `294` ile bildirilir. Yukarıdaki sunucu doğrulama ve sınır hatalarını da
`400` altında topluyor ve kısmi başarıda `200` döndürüyor; ölçüm bu sadeleştirmeyle
okunmalıdır. Ayrımın kendisi — çalıştırılmayan istek ile çalıştırılıp kısmen karşılanan
istek — iki eşlemede de aynıdır.

#### Aracıların ve Kütüklerin Gördüğü

Tek uç nokta kararının asıl bedeli, isteğin ayırt edici bilgisinin adresten gövdeye
taşınmasıdır. Yol üzerindeki bir önbellek ya da erişim kütüğü isteği yalnız yöntem ve
adresten tanır.

```js
// aracinin-gordugu.mjs — yol uzerindeki bir araci istegi yalnizca yontem+adresten tanir.
// Ayni is yuku dort aktarim bicimiyle yollanirsa aracinin gordugu ne kadar degisir?
import { createHash } from "node:crypto";
const kimlikYaz = (m) => createHash("sha256").update(m).digest("hex").slice(0, 12);

// Yarim gunluk is yuku: dort ayri islem, tekrarli
const ISLEMLER = [
  { ad: "OduncListesi", yol: "/oduncler", sorgu: `query OduncListesi { oduncler(ilk: 6) { id durum } }`, degisken: {} },
  { ad: "OduncAyrinti", yol: "/odunc/O-1", sorgu: `query OduncAyrinti($id: ID!) { odunc(id: $id) { id durum uye { ad } } }`, degisken: { id: "O-1" } },
  { ad: "OduncAyrinti", yol: "/odunc/O-2", sorgu: `query OduncAyrinti($id: ID!) { odunc(id: $id) { id durum uye { ad } } }`, degisken: { id: "O-2" } },
  { ad: "UyeOzeti", yol: "/uyeler/U-1001", sorgu: `query UyeOzeti($k: ID!) { uye(kimlik: $k) { ad ceza } }`, degisken: { k: "U-1001" } },
];
const YUK = Array.from({ length: 12 }, (_, i) => ISLEMLER[i % ISLEMLER.length]);

const enc = encodeURIComponent;
const BICEMLER = {
  "kaynak temelli (GET)":   (o) => ["GET", o.yol],
  "GraphQL POST":           () => ["POST", "/graphql"],
  "GraphQL GET (sorgu)":    (o) => ["GET", `/graphql?query=${enc(o.sorgu)}&variables=${enc(JSON.stringify(o.degisken))}`],
  "GraphQL GET (kayıtlı)":  (o) => ["GET", `/graphql?kimlik=${kimlikYaz(o.sorgu)}&variables=${enc(JSON.stringify(o.degisken))}`],
};

console.log("aktarım                 istek  önbellek anahtarı  önbellekten  ayırt edilen işlem  ort. adres");
for (const [ad, uret] of Object.entries(BICEMLER)) {
  const anahtarlar = YUK.map((o) => uret(o).join(" "));
  const tekil = new Set(anahtarlar);
  // Araci yalniz GET isteklerini onbellekleyebilir.
  const onbelleklenebilir = anahtarlar[0].startsWith("GET") ? YUK.length - tekil.size : 0;
  const islemAyrimi = new Set(anahtarlar.map((a) => a.split("?")[0] + (a.includes("query=") || a.includes("kimlik=") ? a.split("&")[0].split("?")[1] : "")));
  const ortUzunluk = Math.round(anahtarlar.reduce((t, a) => t + a.length, 0) / anahtarlar.length);
  console.log(`${ad.padEnd(23)} ${String(YUK.length).padStart(5)}  ${String(tekil.size).padStart(17)}  ${String(onbelleklenebilir).padStart(11)}  ${String(islemAyrimi.size).padStart(18)}  ${String(ortUzunluk).padStart(10)}`);
}
```

```
aktarım                 istek  önbellek anahtarı  önbellekten  ayırt edilen işlem  ort. adres
kaynak temelli (GET)       12                  4            8                   4          15
GraphQL POST               12                  1            0                   1          13
GraphQL GET (sorgu)        12                  4            8                   3         154
GraphQL GET (kayıtlı)      12                  4            8                   3          65
```

`POST` satırı tek uç noktanın bedelini gösteriyor: on iki isteğin tamamı tek bir önbellek
anahtarına düşüyor, hiçbiri önbellekten karşılanamıyor ve erişim kütüğü on iki isteği tek
bir satır türü olarak görüyor. Bir aracı açısından bütün istekler aynıdır.

`GET` satırları bunu geri kazanıyor. Sorgu adrese taşındığında önbellek anahtarı sayısı
kaynak temelli aktarımla eşitleniyor ve on iki isteğin sekizi önbellekten karşılanabilir
hâle geliyor. Bedeli adres uzunluğudur: ortalama 154 karakter ve gerçek sorgular bundan
çok daha uzundur. Kayıtlı belgeler bu bedeli 65 karaktere indiriyor.

Ayırt edilen işlem sütunundaki 4'e karşı 3 farkı bir eksiklik değil, farklı bir gruplamadır.
Kaynak temelli kütük istekleri **kaynağa** göre ayırır; GraphQL kütüğü **işleme** göre.
`OduncAyrinti` iki farklı kimlikle çağrıldığında ikinci gruplamada tek satırdır. Hangisinin
işe yaradığı sorulan soruya bağlıdır: "hangi kaynak çok okunuyor" sorusu birinciyi, "hangi
ekran yavaş" sorusu ikinciyi ister.

Kayıtlı belgelerin ikinci bir sonucu daha vardır. Sunucu yalnız kaydettiği belgeleri
çalıştırdığı için, istemcinin gönderdiği keyfî bir sorgu hiç ayrıştırılmaz. Maliyet
sınırının koruduğu şeyin bir bölümü böylece kendiliğinden korunur: bilinmeyen bir belge
çalıştırılmadan reddedilir.

#### Özet

- GraphQL isteği üç biçimde taşınabilir: gövdede sorgu ile POST, adreste sorgu ile GET ve
  önceden kaydedilmiş belge kimliği ile GET.
- Ölçümdeki sunucuda çalıştırılmayan istekler (ayrıştırma, denetim, sınır) 400, çalıştırılan
  istekler alan düzeyinde hata içerse bile 200 döner; belirtim aynı ayrımı `400` · `422` ·
  `294` olarak üç koda dağıtır. Kısmi başarı iki eşlemede de başarısızlık sayılmaz.
- Tek uç noktaya yapılan POST istekleri tek bir önbellek anahtarına düşer; on iki isteğin
  hiçbiri önbellekten karşılanamaz ve kütükte hepsi tek satır türü olarak görünür.
- Sorgunun adrese taşınması önbellek anahtarı sayısını kaynak temelli aktarımla eşitler;
  bedeli 154 karaktere çıkan adres uzunluğudur.
- Kayıtlı belgeler adresi 65 karaktere indirir ve sunucunun yalnız izin verilen belgeleri
  çalıştırmasını sağlar.
- Kaynak temelli kütük kaynağa, GraphQL kütüğü işleme göre gruplar; ikisi farklı sorulara
  yanıt verir.

#### Kurs Kapanışı

Bu kurs bir arayüzün tasarlanmasını dört aşamada ele aldı ve her aşamada aynı ölçütü
kullandı: bir kararın karşılığı ölçülebiliyor mu?

**API biçemleri** kaynak temelli, uzak yordam çağrısı temelli ve sorgu temelli yaklaşımları
karşılaştırdı. Aralarındaki fark bir üstünlük sıralaması değil, hangi tarafın ne kadar karar
verdiğidir: kaynak temelli tasarımda yanıtın biçimine sunucu, sorgu temelli tasarımda
istemci karar verir.

**Kaynak ve sözleşme tasarımı** kaynak temelli yaklaşımı sonuna kadar götürdü. Kaynak
modeli, adres düzeni, yöntem seçimi, durum kodu eşlemesi, gövde adlandırması, sayfalama,
filtreleme, kısmi yanıt, etkisizlik anahtarları ve bağlantı odaklı yanıtlar — hepsi tek bir
soruya hizmet etti: istemci sunucunun ne yapacağını tahmin etmek zorunda kalmasın.

**Hata, sürüm ve belge** sözleşmenin zaman içindeki hâlini kurdu. Hata gövdesi tek biçime
bağlandı, doğrulama hataları alan düzeyinde bildirildi, sürümleme biçemlerinin gerçek
farkının aracılarda olduğu ölçüldü, kırıcılık istek ve yanıt yönünde ters kurallarla
tanımlandı ve sürüm numarası şema farkından türetildi. Sonra sözleşme makine okunur hâle
geldi: aynı tanımdan doğrulayıcı, belge, sözleşme sınaması ve sahte sunucu üretildi. Bu
konunun kalıcı sonucu tek cümleye sığar — **sözleşme, yazıldığı yerde denetlenebiliyorsa
sözleşmedir.**

**GraphQL ayrıntısı** sorgu temelli yaklaşımı ölçerek anlattı ve bunu kütüphaneye
başvurmadan, kendi ayrıştırıcısını ve çalıştırıcısını yazarak yaptı. Tip sistemi sorguyu
çalıştırmadan denetledi; üç işlem türünün yürütme kuralı tek kopyalı kitap üzerinde
ölçüldü; çözücü çağrısı ile veri kaynağı turu ayrıldı ve N+1 sorunu 101 turdan 3 tura
indirildi; parçaların çalışma anında yok olduğu, değişkenlerin ayrı belge sayısını
sabitlediği gösterildi; imleç sayfalamanın ofsete üstünlüğü liste değişirken ölçüldü; alan
düzeyi yetkilendirme ve kısmi hata kuruldu; zorunluluk işaretinin bir alanın çökmesini bütün
yanıta yayabildiği gösterildi; ve maliyet sınırının derinlik sınırından neden daha temel
olduğu sayıyla konuldu.

İki yaklaşımın ortak dersi de burada görünür. Kaynak temelli tasarımda tüketicinin ne
istediğini sunucu tahmin eder ve fazla ya da eksik veri gönderir; sorgu temelli tasarımda
tüketici tam olarak istediğini alır ama sunucu ne kadar iş yapacağını önceden bilemez. İlki
tasarım zamanında, ikincisi çalışma zamanında ödenen bir bedeldir. Sözleşme testleri
birinciyi, maliyet sınırı ikinciyi ölçülebilir kılar.

Bu kurs boyunca bir soru sürekli ertelendi. Ödünç kaydının notunu yalnız kaydın açıldığı
şubenin görmesi gerektiğini yazdık ama isteği yapanın gerçekten o şubenin görevlisi olduğunu
nereden bildiğimizi hiç sormadık; yetki kuralları bağlamdaki `kullanici` ve `rol`
alanlarını okudu ve o alanların oraya nasıl geldiği açıkta kaldı. Kullanımdan kaldırma
telemetrisinde tüketici kimliğinin bir başlıkla gelmesi de aynı boşluğun başka bir
görüntüsüydü — istemcinin kendi kimliğini bildirmesi, kimlik doğrulama değildir.

**Kimlik Doğrulama ve Yetkilendirme** kursu bu boşluğu doldurur. Kimliğin nasıl kanıtlandığı
ile o kimliğe neyin izin verildiğini birbirinden ayırır; parola ve belirteç saklamanın
güvenli gerçekleştirimini kurar; ve yetki modelini rol, öznitelik ya da ilişki temelinde
tanımlamanın ölçülebilir sonuçlarını karşılaştırır. Bu kursta yazdığımız yetki kuralları
oraya taşındığında, bağlamın nasıl doldurulduğu artık bir varsayım olmayacak.

