---
title: 'Sunucu Tarafı Temelleri'
source: 'https://academia.sh/tr/kurslar/sunucu-temelleri'
language: tr
updated: '2026-08-17T04:38:54+00:00'
license: 'CC BY-SA 4.0'
lessons: 16
outcomes:
    - 'Bir isteğin sunucu tarafındaki işlenme zincirini bileşenlerine ayırır'
    - 'Web sunucusu, uygulama sunucusu ve çalışma zamanı rollerini ayırır'
    - 'Yapılandırmayı ortamdan okuyan bir uygulama iskeleti kurar'
---

# Sunucu Tarafı Temelleri

Bir isteğin sunucu tarafında geçtiği yol: web sunucusu, çalışma zamanı ve uygulama arasındaki iş bölümü, istek yaşam döngüsü, ara katman zinciri ve ortamdan yapılandırılan taşınabilir bir uygulama iskeleti.

## Backend'in Görevi

Sunucu tarafının üstlendiği sorumluluklar; istemciyle sorumluluk sınırı, web sunucusunun ve çalışma zamanının rolleri, bir isteğin kabulden yanıta uzanan yaşam döngüsü ve kesişen ilgileri sıraya koyan ara katman zinciri.

### Backend Ne Yapar

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/backend-ne-yapar

Sunucu tarafının üstlendiği üç sorumluluk: durumun tek yerde tutulması, iş kurallarının uygulanma noktası ve istemcinin erişemediği sistemlerle tümleştirme.

Frontend müfredatındaki Uygulama Mimarisi kursu, tarayıcıda çalışan uygulamanın veriyi bir
istek katmanı (request layer) üzerinden aldığını kurmuştu: uygulama bir adrese istek yollar,
gelen yanıtı çözer, durum kodunu hata sözleşmesine eşler ve ekranı günceller. O kursta
isteğin gittiği taraf sabit bir varsayımdı. İnternet Nasıl Çalışır kursu ise isteğin
biçimini tanımladı — istek satırı, başlık, gövde — ama karşı tarafta çalışan programın ne
yaptığını değil.

Bu ders o boşluğu doldurur ve tek bir soruyu yanıtlar: istemcinin karşısında duran taraf
neyi üstlenir ve bu iş neden istemciye bırakılamaz? Yanıt üç sorumluluğa ayrılır —
**kalıcılık** (persistence), **iş kuralları** (business rules) ve **tümleştirme**
(integration). Üçü de aynı gerekçeye dayanır ve o gerekçe bu derste ölçülerek gösterilir.
Müfredat boyunca tek bir uygulama sürecek: bir kütüphane ödünç servisi. Kitap, üye, ödünç
işlemi ve şube kavramları bütün derslerde aynı anlamı taşıyacak.

#### Durum Tek İstemcinin Belleğinde Kalamaz

Ödünç servisinin çekirdek verisi bir liste: hangi üyenin hangi kitabı elinde tuttuğu. Bu
liste istemcide tutulursa ne olur? Aşağıdaki program tek bir istemciyi temsil eder; katalog
sabit, ödünç kaydı ise sürecin kendi belleğindedir.

```js
// istemcide-durum.mjs — odunc kaydini yalnizca kendi belleginde tutan istemci
const KATALOG = new Map([
  ["978-0201896831", { ad: "Programlama Sanati", kopya: 2 }],
  ["978-0262033848", { ad: "Algoritmalara Giris", kopya: 1 }],
]);

const oduncler = []; // bu surece ozgu: baska bir calistirma bunu gormez

const oduncAl = (isbn, uye) => {
  const kitap = KATALOG.get(isbn);
  const elde = oduncler.filter((o) => o.isbn === isbn).length;
  if (elde >= kitap.kopya) return "reddedildi: kopya yok";
  oduncler.push({ isbn, uye });
  return "verildi";
};

const [uye, isbn] = process.argv.slice(2);
console.log(`${uye} -> ${oduncAl(isbn, uye)}`);
console.log(`${uye} gorunumunde toplam odunc: ${oduncler.length}`);
```

İki ayrı üye, tek kopyası olan aynı kitabı ister. Her üye kendi istemcisini çalıştırır:

```bash
node istemcide-durum.mjs U-1001 978-0262033848
node istemcide-durum.mjs U-1002 978-0262033848
```

```
U-1001 -> verildi
U-1001 gorunumunde toplam odunc: 1
U-1002 -> verildi
U-1002 gorunumunde toplam odunc: 1
```

Kod hatalı değildir: kopya denetimi vardır ve doğru yazılmıştır. Kusur, denetimin
denetleyecek veriye sahip olmamasıdır. Her çalıştırma kendi boş listesiyle başlar, kendi
listesini doğru sayar ve doğru sonuca varır. Ama sistemde tek kopya vardır ve iki kişiye
verilmiştir. Bu, eşzamanlılıktan değil **yalıtımdan** doğan bir çakışmadır.

Sunucu tarafının ilk görevi bu yalıtımı kırmaktır: ödünç kaydı bütün istemcilerin
paylaştığı tek bir yerde tutulur. Aşağıdaki sunucu aynı kuralı aynı biçimde uygular, tek
farkla — liste süreçlerin dışında, isteklerin ortaklaştığı yerdedir.

```js
// odunc-sunucusu.mjs — odunc kaydini ve kurallari tek yerde tutan sunucu
import { createServer } from "node:http";

const KATALOG = new Map([
  ["978-0201896831", { ad: "Programlama Sanati", kopya: 2 }],
  ["978-0262033848", { ad: "Algoritmalara Giris", kopya: 1 }],
]);
const UYE_SINIRI = 2;
const oduncler = []; // butun istemcilerin paylastigi tek kayit

const govdeOku = (istek) =>
  new Promise((coz, red) => {
    let veri = "";
    istek.on("data", (parca) => (veri += parca));
    istek.on("end", () => { try { coz(veri ? JSON.parse(veri) : {}); } catch (h) { red(h); } });
  });

const yanitla = (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;
  if (istek.method === "GET" && istek.url === "/odunc") return yanitla(yanit, 200, oduncler);
  if (istek.method !== "POST" || istek.url !== "/odunc") return yanitla(yanit, 404, { hata: "yol_yok" });

  const govde = await govdeOku(istek);          // govde.uygun alani okunmaz
  const kitap = KATALOG.get(govde.isbn);
  if (!kitap) return yanitla(yanit, 404, { hata: "kitap_yok" });

  const eldeki = oduncler.filter((o) => o.isbn === govde.isbn).length;
  if (eldeki >= kitap.kopya) return yanitla(yanit, 409, { hata: "kopya_yok", kalan: 0 });

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

  oduncler.push({ uye: govde.uye, isbn: govde.isbn });
  yanitla(yanit, 201, { durum: "verildi", kalan: kitap.kopya - eldeki - 1 });
}).listen(8421, "127.0.0.1", () => console.log("dinleniyor 127.0.0.1:8421"));
```

Sunucu uzun ömürlü bir süreçtir; komut satırından çalıştırıldığında durdurulana kadar
dinlemeye devam eder. Bu yüzden örnek, sunucuyu başlatan ve sonunda durduran bir betikle
birlikte verilir. Kullanılan bağlantı noktası 8421 keyfîdir ve boş olmalıdır; doluysa iki
dosyada da değiştirilir.

```bash
#!/usr/bin/env bash
# odunc-sunucusu.mjs'yi baslatir, bes istegi sirayla yollar, kaydi okur, durdurur.
node odunc-sunucusu.mjs & sunucu=$!
sleep 0.5

gonder() {
  printf '%-54s -> ' "$1"
  curl -sS -w ' [%{http_code}]\n' -X POST -H 'content-type: application/json' \
    -d "$1" http://127.0.0.1:8421/odunc
}

gonder '{"uye":"U-1001","isbn":"978-0262033848"}'
gonder '{"uye":"U-1002","isbn":"978-0262033848"}'
gonder '{"uye":"U-1002","isbn":"978-0262033848","uygun":true}'
gonder '{"uye":"U-1001","isbn":"978-0201896831"}'
gonder '{"uye":"U-1001","isbn":"978-0201896831"}'

echo "--- sunucudaki kayit ---"
curl -sS http://127.0.0.1:8421/odunc; echo
kill "$sunucu"
```

```
dinleniyor 127.0.0.1:8421
{"uye":"U-1001","isbn":"978-0262033848"}               -> {"durum":"verildi","kalan":0} [201]
{"uye":"U-1002","isbn":"978-0262033848"}               -> {"hata":"kopya_yok","kalan":0} [409]
{"uye":"U-1002","isbn":"978-0262033848","uygun":true}  -> {"hata":"kopya_yok","kalan":0} [409]
{"uye":"U-1001","isbn":"978-0201896831"}               -> {"durum":"verildi","kalan":1} [201]
{"uye":"U-1001","isbn":"978-0201896831"}               -> {"hata":"uye_siniri","sinir":2,"acik":2} [409]
--- sunucudaki kayit ---
[{"uye":"U-1001","isbn":"978-0262033848"},{"uye":"U-1001","isbn":"978-0201896831"}]
```

İkinci istek reddedilmiştir. Aynı kural, aynı kod, farklı sonuç: değişen tek şey, kuralın
baktığı listenin iki isteğin de yazdığı liste olmasıdır. Sunucudaki kayıt iki satırdır ve
tek kopyalı kitap bir kez verilmiştir.

Buna **kalıcılık** denir. Sözcük burada dar anlamda "diske yazma" değildir; ilerideki
derslerde kayıt bir veritabanına taşınacak. Buradaki anlamı, durumun **tek bir istemcinin
ömrünü ve görüş alanını aşması**dır. Ödünç kaydı, onu yaratan istekten sonra da vardır;
başka bir istemci gelip okuyabilir. Bir sistemin hangi verisinin sunucu tarafında durması
gerektiği sorusu, bu ölçüte indirgenir: bu veriyi tek bir istemcinin görmesi yeterli mi?

#### Kuralın Uygulandığı Yer ile Gösterildiği Yer

Yukarıdaki çıktının üçüncü satırı ayrıca okunmalıdır. O istek, gövdesine `"uygun": true`
alanını eklemiştir — istemcinin kendi hesabına göre "bu ödünç uygundur" demesidir. Sunucu
aynı yanıtı vermiştir: `kopya_yok`. Çünkü sunucu o alanı hiç okumaz, kararı elindeki
listeden yeniden hesaplar.

Bu ayrım, iş kuralı sorumluluğunun özüdür. Kütüphanenin "bir üye aynı anda en fazla iki
kitap tutabilir" kuralı iki yerde birden görünebilir:

- **Gösterim yeri.** İstemci, sınıra ulaşmış bir üyeye ödünç düğmesini pasif gösterir,
  gerekçeyi yazar, gereksiz bir ağ turu yapmaz. Bu bir kullanıcı deneyimi kararıdır.
- **Uygulama yeri.** Sunucu, gelen her istekte kuralı yeniden değerlendirir ve
  karşılanmıyorsa isteği reddeder. Bu bir doğruluk kararıdır.

İkisi aynı kuralın iki gerçekleştirimi olduğu için birbirinin yerine geçtiği sanılır.
Geçmez. İstemcideki denetim isteği **azaltır**, sunucudaki denetim isteği **bağlar**.
Sunucuya ulaşan bir isteğin gerçekten hangi istemciden geldiği, o istemcinin hangi kodu
çalıştırdığı ve denetimin çalışıp çalışmadığı sunucunun bilgisi dışındadır — yukarıdaki
`curl` çağrıları bunun somut hâlidir. Kuralın tek bağlayıcı gerçekleştirimi, isteğin
ulaştığı taraftakidir.

Buradan bir tasarım ölçütü çıkar: **bir kural yanlış uygulandığında veri tutarsız kalıyorsa
o kural sunucu tarafındadır.** Sınır aşımı ödünç kaydını bozar, bu yüzden sunucudadır. Bir
tarihin ekranda hangi biçimde yazılacağı ise veriyi bozmaz, bu yüzden istemcide kalabilir.

#### İstemcinin Erişemediği Sistemler

Üçüncü sorumluluk, ilk ikisinden farklı bir gerekçeye dayanır. Kütüphanenin ceza kayıtları
başka bir ekibin işlettiği ayrı bir serviste durur ve bu servise erişim bir kimlik bilgisiyle
(credential) korunur.

```js
// ceza-servisi.mjs — ayri bir ekibin islettigi ceza kaydi servisi
import { createServer } from "node:http";

const ANAHTAR = process.env.CEZA_ANAHTARI ?? "";
const CEZALAR = new Map([["U-1001", 0], ["U-1002", 12.5]]);

createServer((istek, yanit) => {
  yanit.sendDate = false;
  yanit.setHeader("content-type", "application/json; charset=utf-8");
  if (istek.headers.authorization !== `Bearer ${ANAHTAR}`) {
    return yanit.writeHead(401).end(JSON.stringify({ hata: "yetkisiz" }));
  }
  const uye = new URL(istek.url, "http://yerel").searchParams.get("uye");
  yanit.writeHead(200).end(JSON.stringify({ uye, ceza: CEZALAR.get(uye) ?? 0 }));
}).listen(8422, "127.0.0.1", () => console.log("ceza servisi 127.0.0.1:8422"));
```

Ödünç uygulaması, üyenin durumunu üretirken kendi ödünç kaydını ceza servisinin yanıtıyla
birleştirir. Kimlik bilgisini taşıyan taraf uygulamadır.

```js
// uygulama.mjs — istemciye bakan uygulama; ceza servisine kimlik bilgisiyle kendisi gider
import { createServer } from "node:http";

const ANAHTAR = process.env.CEZA_ANAHTARI ?? "";
const ODUNCLER = [{ uye: "U-1001", isbn: "978-0262033848" }, { uye: "U-1002", isbn: "978-0201896831" }];

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  yanit.setHeader("content-type", "application/json; charset=utf-8");
  const uye = new URL(istek.url, "http://yerel").searchParams.get("uye");

  const cevap = await fetch(`http://127.0.0.1:8422/ceza?uye=${uye}`, {
    headers: { authorization: `Bearer ${ANAHTAR}` },
  });
  if (!cevap.ok) return yanit.writeHead(502).end(JSON.stringify({ hata: "ceza_servisi_erisilemedi" }));

  const { ceza } = await cevap.json();
  const acik = ODUNCLER.filter((o) => o.uye === uye).length;
  yanit.writeHead(200).end(JSON.stringify({ uye, acikOdunc: acik, ceza, oduncAlabilir: ceza === 0 }));
}).listen(8423, "127.0.0.1", () => console.log("uygulama 127.0.0.1:8423"));
```

```bash
#!/usr/bin/env bash
# Iki sunucuyu ayni gizli anahtarla baslatir, istemcinin gorebildigi yuzeyi olcer.
export CEZA_ANAHTARI="k-9f3a-ornek"
node ceza-servisi.mjs & ceza=$!
sleep 0.5
node uygulama.mjs & uyg=$!
sleep 0.5

echo "--- istemci ceza servisine dogrudan gidiyor ---"
curl -sS -w ' [%{http_code}]\n' 'http://127.0.0.1:8422/ceza?uye=U-1002'

echo "--- istemci uygulamaya gidiyor ---"
curl -sS -w ' [%{http_code}]\n' 'http://127.0.0.1:8423/uye?uye=U-1002'
curl -sS -w ' [%{http_code}]\n' 'http://127.0.0.1:8423/uye?uye=U-1001'

yanit=$(curl -sS -D - 'http://127.0.0.1:8423/uye?uye=U-1001')
echo "--- uygulama yanitinda (baslik + govde) anahtarin gectigi satir sayisi ---"
printf '%s' "$yanit" | grep -c 'k-9f3a-ornek'

kill "$ceza" "$uyg"
```

```
ceza servisi 127.0.0.1:8422
uygulama 127.0.0.1:8423
--- istemci ceza servisine dogrudan gidiyor ---
{"hata":"yetkisiz"} [401]
--- istemci uygulamaya gidiyor ---
{"uye":"U-1002","acikOdunc":1,"ceza":12.5,"oduncAlabilir":false} [200]
{"uye":"U-1001","acikOdunc":1,"ceza":0,"oduncAlabilir":true} [200]
--- uygulama yanitinda (baslik + govde) anahtarin gectigi satir sayisi ---
0
```

Üç şey ölçülmüştür. Ceza servisine kimlik bilgisi olmadan gidilemez. Uygulama üzerinden
gidildiğinde iki kaynaktan gelen veri tek yanıtta birleşir. Ve uygulamanın yanıtında —
başlıklar dâhil — anahtarın geçtiği satır sayısı sıfırdır.

Son ölçüm belirleyicidir. İstemciye giden her bayt, o istemciyi çalıştıran kişinin
okuyabildiği bir bayttır; tarayıcıda çalışan kod, indirdiği betiği ve aldığı yanıtı
gizleyemez. Dolayısıyla ceza servisinin anahtarı istemciye verilseydi, anahtar herkese
verilmiş olurdu. **Tümleştirme sorumluluğu sunucudadır, çünkü tümleştirmenin gerektirdiği
kimlik bilgisi istemciye taşınamaz.**

Tümleştirme yalnız gizlilikle de sınırlı değildir. Ceza servisi yavaşladığında ne olacağı,
yanıt vermediğinde hangi durum kodunun üretileceği, birden çok kaynağın nasıl birleştirileceği
sunucu tarafının kararlarıdır. Yukarıdaki uygulama bu kararların en yalınını verir: yukarı
akış servisi başarısızsa `502` üretir ve kendi hata sözleşmesini bozmaz.

#### Üç Sorumluluğun Ortak Adı

Kalıcılık, iş kuralları ve tümleştirme birbirinden bağımsız üç iş gibi görünür ama tek bir
kavrama dayanır: **doğruluk kaynağı** (source of truth). Bir sistemde her veri parçası için
"buna hangi bileşen karar verir" sorusunun tek bir yanıtı olmalıdır.

Ödünç kaydında bu yanıt sunucudur, çünkü kayıt paylaşılır. Ödünç kuralında sunucudur, çünkü
kararın bağlayıcı olması gerekir. Ceza bilgisinde sunucudur, çünkü kaynağa erişim orada
korunur. İstemci bu üç durumda da bir **görünüm** üretir: sunucudan aldığı veriyi biçimler,
kuralı önceden gösterir, kullanıcıya bekleme yaşatmadan tepki verir. Görünüm üretmek küçük
bir iş değildir — Frontend müfredatı bunun tamamına ayrılmıştır — ama doğruluk kaynağı olmak
başka bir iştir.

Bu ayrım kurulduğunda bir sonraki soru kendiliğinden gelir: her hesap sunucuya mı taşınmalı?
Hayır; sunucuya taşınan her hesap bir ağ turu, bir yanıt gecikmesi ve bir ölçekleme maliyeti
demektir. Sınırın nereden geçeceği ayrı bir karar konusudur.

#### Özet

- Durum tek bir istemcinin belleğinde tutulduğunda her istemci kendi görüşüne göre doğru,
  sistem bütününde yanlış karar verir; çalıştırılan örnek tek kopyalı kitabın iki üyeye
  verilmesiyle bunu gösterir.
- Kalıcılık, verinin tek bir istemcinin ömrünü ve görüş alanını aşması demektir; ölçüt,
  veriyi başka istemcilerin de görmesi gerekip gerekmediğidir.
- Bir iş kuralı istemcide **gösterilir**, sunucuda **uygulanır**; istemcinin gövdeye
  eklediği hazır karar alanı sunucu tarafından okunmaz, karar elde bulunan veriden yeniden
  hesaplanır.
- Yanlış uygulandığında veriyi tutarsız bırakan her kural sunucu tarafındadır; yalnız sunumu
  etkileyen kararlar istemcide kalabilir.
- Tümleştirme sunucudadır, çünkü yukarı akış servislerinin kimlik bilgisi istemciye
  taşınamaz; ölçülen yanıtta anahtarın geçtiği satır sayısı sıfırdır.
- Üç sorumluluk tek kavrama dayanır: her veri parçası için doğruluk kaynağının hangi bileşen
  olduğu tek biçimde belirlenmiş olmalıdır.

#### Sonraki Adım

Doğruluk kaynağının sunucu olması, her hesabın sunucuda yapılacağı anlamına gelmez. Bir
kitap listesinin sıralanması, bir gecikme cezasının tutarı, bir arama kutusunun sonuçları —
bunların her biri iki tarafta da hesaplanabilir ve seçim ölçülebilir sonuçlar doğurur:
taşınan bayt, ağ turu sayısı, yanıt süresi ve kararın bağlayıcılığı. Sonraki ders bu sınırı
çizer: aynı hesabı iki tarafta da yapıp taşınan veriyi ve süreyi ölçer, sonra ölçümün
karar veremediği bir kalem gösterir — bağlayıcı olması gereken hesabın hangi tarafta
durması gerektiği, başarımdan değil güvenden gelir.

### İstemci ve Sunucu Sorumluluk Sınırı

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/istemci-ve-sunucu-sorumluluk-siniri

Aynı hesabın iki tarafta da yapılabildiği durumlarda seçimi belirleyen ölçütler; taşınan bayt, ağ turu sayısı ve kararın bağlayıcılığı ölçülerek karşılaştırılır.

Önceki ders doğruluk kaynağının sunucu olduğunu üç sorumluluk üzerinden kurdu: kalıcılık,
iş kuralları ve tümleştirme. Bu, her hesabın sunucuda yapılacağı anlamına gelmez. Kütüphane
uygulamasında pek çok işlem — bir listenin süzülmesi, bir tutarın hesaplanması, bir arama
teriminin düzeltilmesi — iki tarafta da yazılabilir ve ikisi de doğru çalışır.

Bu ders sınırın nereden geçtiğini üç ölçüyle inceler: taşınan bayt, ağ turu sayısı ve
kararın bağlayıcılığı. İlk ikisi ölçülür, üçüncüsü ölçülemez ama en belirleyicisidir.

#### Aynı Sonuç, İki Yol: Taşınan Veri

Kütüphanenin kataloğu beş bin kitap içeriyor. Kullanıcı "ağ" konusundaki kitapların ilk
sayfasını görmek istiyor. İki yol vardır: sunucu kataloğun tamamını gönderir ve istemci
süzer; ya da sunucu süzer, sayfalar ve yalnız görüntülenecek dilimi gönderir. Aşağıdaki
sunucu ikisini de sunar.

```js
// katalog-sunucusu.mjs — ayni katalogu iki bicimde sunar: tamami ve suzulmus sayfa
import { createServer } from "node:http";

const KONULAR = ["algoritma", "veritabani", "ag", "isletim-sistemi", "derleyici"];
const KATALOG = Array.from({ length: 5000 }, (_, i) => ({
  isbn: `978-0${(1000000 + i).toString()}`,
  ad: `Kitap ${i + 1}: ${KONULAR[i % 5]} uzerine inceleme`,
  konu: KONULAR[i % 5],
  yil: 1970 + (i % 55),
  raf: `R-${Math.floor(i / 50) + 1}`,
}));

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");
  yanit.setHeader("content-type", "application/json; charset=utf-8");

  if (adres.pathname === "/kitaplar/tumu") {
    return yanit.writeHead(200).end(JSON.stringify(KATALOG));
  }
  if (adres.pathname === "/kitaplar") {
    const konu = adres.searchParams.get("konu");
    const sayfa = Number(adres.searchParams.get("sayfa") ?? 1);
    const boy = 20;
    const suzulmus = konu ? KATALOG.filter((k) => k.konu === konu) : KATALOG;
    const dilim = suzulmus.slice((sayfa - 1) * boy, sayfa * boy);
    return yanit.writeHead(200).end(JSON.stringify({ toplam: suzulmus.length, sayfa, kitaplar: dilim }));
  }
  yanit.writeHead(404).end(JSON.stringify({ hata: "yol_yok" }));
}).listen(8424, "127.0.0.1", () => console.log("katalog 127.0.0.1:8424"));
```

```bash
#!/usr/bin/env bash
# Ayni sonucu ureten iki yaklasimin tasidigi baytlari ve sureyi olcer.
node katalog-sunucusu.mjs & sunucu=$!
sleep 0.5
curl -sS -o /dev/null 'http://127.0.0.1:8424/kitaplar?konu=ag'   # isinma

BICIM='  govde %{size_download} B   ilk bayt %{time_starttransfer} s\n'
echo "--- tum katalog istemciye tasiniyor (suzme istemcide) ---"
curl -sS -o /dev/null -w "$BICIM" 'http://127.0.0.1:8424/kitaplar/tumu'
echo "--- suzme ve sayfalama sunucuda ---"
curl -sS -o /dev/null -w "$BICIM" 'http://127.0.0.1:8424/kitaplar?konu=ag&sayfa=1'

echo "--- iki yanitin ilk satirlari ---"
curl -sS 'http://127.0.0.1:8424/kitaplar?konu=ag&sayfa=1' | head -c 220; echo
kill "$sunucu"
```

```
katalog 127.0.0.1:8424
--- tum katalog istemciye tasiniyor (suzme istemcide) ---
  govde 563494 B   ilk bayt 0.001898 s
--- suzme ve sayfalama sunucuda ---
  govde 1956 B   ilk bayt 0.000569 s
--- iki yanitin ilk satirlari ---
{"toplam":1000,"sayfa":1,"kitaplar":[{"isbn":"978-01000002","ad":"Kitap 3: ag uzerine inceleme","konu":"ag","yil":1972,"raf":"R-1"},{"isbn":"978-01000007","ad":"Kitap 8: ag uzerine inceleme","konu":"ag","yil":1977,"raf":
```

Süre alanları makineye ve o andaki yüke bağlıdır; her çalıştırmada değişir. Sabit olan
bayt oranıdır: 563 494 bayta karşılık 1 956 bayt, yaklaşık 288 kat. Kullanıcı ekranda
yirmi satır görecektir; ilk yolda bu yirmi satır için beş bin kaydın tamamı taşınmıştır.

Fark yalnız bant genişliği değildir. Kataloğun tamamı istemciye indiğinde bellekte de
tutulur, ayrıştırılır ve her süzme işleminde yeniden gezilir; katalog büyüdükçe bu maliyet
istemcinin en zayıf aygıtında ödenir. Buna karşılık ikinci yolda her süzme değişikliği yeni
bir istek demektir: kullanıcı konu değiştirdiğinde ağa çıkılır, sonuç beklenir.

Ölçüt bu iki maliyet arasındaki orandır. Veri kümesi ekrana sığmayacak kadar büyükse
süzme sunucuya aittir. Veri kümesi küçük ve zaten indirilmişse — örneğin bir üyenin altı
açık ödüncü — aynı listeyi tekrar tekrar sunucudan istemek gereksiz turlar üretir.

#### Ölçümün Karar Veremediği Kalem: Bağlayıcılık

Yukarıdaki karar başarım üzerinden verilebilir; her iki seçenek de doğru sonucu üretir.
Bazı hesaplarda ise seçenek yoktur ve gerekçe ölçüme dayanmaz.

Gecikme cezası böyle bir hesaptır. Ödünç süresi on dört gündür, gecikilen her gün için
birim ücret alınır. Hesap iki çarpma ve bir çıkarmadır; istemci de yapabilir. Aşağıdaki
sunucu istemcinin gönderdiği tutarı ve istemcinin bildirdiği saati okumaz.

```js
// iade-sunucusu.mjs — gecikme cezasini istemcinin bildirdigine bakmadan kendisi hesaplar
import { createServer } from "node:http";

const GUN = 86_400_000;
const SURE_GUN = 14;        // odunc suresi
const GUNLUK_CEZA = 2.5;    // gecikme basina birim ucret

// Tohum veri: sunucu ayaga kalkarken 20 gun once verilmis bir odunc olusturulur.
const ODUNCLER = new Map([["U-1002:978-0201896831", { verilis: Date.now() - 20 * GUN }]]);

const govdeOku = (istek) =>
  new Promise((coz, red) => {
    let veri = "";
    istek.on("data", (parca) => (veri += parca));
    istek.on("end", () => { try { coz(veri ? JSON.parse(veri) : {}); } catch (h) { red(h); } });
  });

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  yanit.setHeader("content-type", "application/json; charset=utf-8");
  const govde = await govdeOku(istek);                 // govde.tutar ve govde.simdi okunmaz
  const kayit = ODUNCLER.get(`${govde.uye}:${govde.isbn}`);
  if (!kayit) return yanit.writeHead(404).end(JSON.stringify({ hata: "odunc_yok" }));

  const sonGun = kayit.verilis + SURE_GUN * GUN;
  const gecikme = Math.max(0, Math.floor((Date.now() - sonGun) / GUN));
  const tutar = Number((gecikme * GUNLUK_CEZA).toFixed(2));
  yanit.writeHead(200).end(JSON.stringify({
    uye: govde.uye, gecikmeGun: gecikme, tutar,
    istemcininBildirdigi: govde.tutar ?? null,
  }));
}).listen(8425, "127.0.0.1", () => console.log("iade 127.0.0.1:8425"));
```

```bash
#!/usr/bin/env bash
# Istemcinin bildirdigi tutar ve saat degistiginde sunucunun yaniti degisiyor mu?
node iade-sunucusu.mjs & sunucu=$!
sleep 0.5

gonder() {
  printf '%-62s\n  -> ' "$1"
  curl -sS -w '\n' -X POST -H 'content-type: application/json' -d "$1" http://127.0.0.1:8425/iade
}

gonder '{"uye":"U-1002","isbn":"978-0201896831"}'
gonder '{"uye":"U-1002","isbn":"978-0201896831","tutar":0}'
gonder '{"uye":"U-1002","isbn":"978-0201896831","tutar":0,"simdi":"2020-01-01T00:00:00Z"}'
kill "$sunucu"
```

```
iade 127.0.0.1:8425
{"uye":"U-1002","isbn":"978-0201896831"}                      
  -> {"uye":"U-1002","gecikmeGun":6,"tutar":15,"istemcininBildirdigi":null}
{"uye":"U-1002","isbn":"978-0201896831","tutar":0}            
  -> {"uye":"U-1002","gecikmeGun":6,"tutar":15,"istemcininBildirdigi":0}
{"uye":"U-1002","isbn":"978-0201896831","tutar":0,"simdi":"2020-01-01T00:00:00Z"}
  -> {"uye":"U-1002","gecikmeGun":6,"tutar":15,"istemcininBildirdigi":0}
```

Üç istek de aynı sonucu almıştır: altı gün gecikme, on beş birim ücret. İkinci istek sıfır
tutar bildirmiştir, üçüncüsü ayrıca kendi saatini altı yıl geriye almıştır. Sunucunun
yanıtı değişmemiştir, çünkü sunucu iki girdiyi de yalnız kayda geçirmiş, hesabı kendi
verisinden ve kendi saatinden yapmıştır.

Buradaki ilke bir başarım ilkesi değil, bir **güven sınırı** (trust boundary) ilkesidir:
sunucu, kendi denetimindeki verilerden türetemediği hiçbir değeri bağlayıcı saymaz. Bu
listeye istemciden gelen tutarlar, saat bilgisi, kullanıcı kimliği iddiaları, fiyatlar ve
yetki bilgileri girer. İstemci bunların hepsini gönderebilir; sunucu hepsini **girdi**
olarak alır, hiçbirini **karar** olarak almaz.

İstemcinin aynı hesabı yapması yine de yararlıdır. Üye iade ekranını açtığında tutarı
anında görür, sürpriz yaşamaz. Bu hesap bir tahmindir ve sunucunun hesabıyla çeliştiğinde
sunucununki geçerlidir. Aynı kural birinci derste ödünç sınırında görülmüştü: istemci
gösterir, sunucu bağlar.

#### Ters Yön: Sunucuya Taşınmaması Gereken Hesap

Sınır tek yönlü değildir. Bir hesabı gereksiz yere sunucuya taşımak da bir tasarım
hatasıdır ve bedeli tur sayısıyla ölçülür.

Katalog arama kutusunda kullanıcı yazdıkça arama terimi düzeltilir: baştaki ve sondaki
boşluklar atılır, küçük harfe çevrilir, iç boşluklar teke indirilir. Türkçe'de küçük harfe
çevirme yerel ayara duyarlıdır ve bu düzeltmenin sunucuda yapılması için bir gerekçe gibi
görünebilir. Aşağıdaki ölçüm bedeli gösterir.

```js
// tur-maliyeti.mjs — arama kutusundaki her tus icin sunucuya gitmenin bedeli
import { createServer } from "node:http";

const TUSLAR = ["a", "al", "alg", "algo", "algor", "algori", "algorit", "algoritma"];
const duzelt = (metin) => metin.trim().toLocaleLowerCase("tr").replace(/\s+/g, " ");

const sunucu = createServer((istek, yanit) => {
  yanit.sendDate = false;
  const q = new URL(istek.url, "http://yerel").searchParams.get("q") ?? "";
  yanit.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ q: duzelt(q) }));
});

await new Promise((coz) => sunucu.listen(8426, "127.0.0.1", coz));
await fetch("http://127.0.0.1:8426/duzelt?q=isinma");            // isinma turu

const t0 = performance.now();
for (const tus of TUSLAR) await (await fetch(`http://127.0.0.1:8426/duzelt?q=${tus}`)).json();
const sunucuda = performance.now() - t0;

const t1 = performance.now();
for (const tus of TUSLAR) duzelt(tus);
const istemcide = performance.now() - t1;

sunucu.close();

console.log(`tus sayisi: ${TUSLAR.length}`);
console.log(`istemcide duzeltme  : ${istemcide.toFixed(3)} ms`);
console.log(`sunucuda duzeltme   : ${sunucuda.toFixed(3)} ms  (geri dongu arayuzunde)`);
console.log("");
console.log("ag turu eklendiginde toplam bekleme (tus basina bir tur):");
console.log("  " + "tur".padEnd(10) + "sunucuda".padStart(12) + "istemcide".padStart(12));
for (const tur of [0, 20, 60, 150]) {
  const s = sunucuda + TUSLAR.length * tur;
  console.log("  " + `${tur} ms`.padEnd(10) + `${s.toFixed(1)} ms`.padStart(12) +
    `${istemcide.toFixed(1)} ms`.padStart(12));
}
```

```
tus sayisi: 8
istemcide duzeltme  : 0.007 ms
sunucuda duzeltme   : 12.076 ms  (geri dongu arayuzunde)

ag turu eklendiginde toplam bekleme (tus basina bir tur):
  tur           sunucuda   istemcide
  0 ms           12.1 ms      0.0 ms
  20 ms         172.1 ms      0.0 ms
  60 ms         492.1 ms      0.0 ms
  150 ms       1212.1 ms      0.0 ms
```

Süre alanları makineye bağlıdır; oran korunur. Geri döngü arayüzünde bile fark üç
mertebedir, çünkü yerel bir fonksiyon çağrısı ile bir HTTP isteği aynı büyüklükte işler
değildir. Tablo, ağ turu eklendiğinde farkın nasıl büyüdüğünü gösterir: seksen milisaniyelik
bir turda sekiz tuş vuruşu neredeyse bir saniye bekleme üretir.

Bu hesabın sunucuya taşınmasında bir kazanç yoktur, çünkü sonuç bağlayıcı değildir: arama
terimini düzeltmek yanlış yapıldığında veriyi bozmaz, yalnız arama sonucunu değiştirir. Ve
sunucu zaten kendi tarafında aynı düzeltmeyi yapmak zorundadır — gelen terimi olduğu gibi
sorguya vermeyecektir. Yani düzeltme iki tarafta da vardır; istemcideki tepkiyi hızlandırır,
sunucudaki doğruluğu güvence altına alır. **Aynı hesabın iki tarafta da bulunması bir
yineleme değil, iki farklı görevin gereğidir.**

#### Sınırı Çizen Üç Soru

Yukarıdaki üç ölçüm tek bir karar akışına indirgenebilir. Bir hesabın hangi tarafa ait
olduğu şu sırayla sorulur:

**Hesap bağlayıcı mı?** Yanlış sonuç veriye ya da paraya yazılıyorsa hesap sunucudadır ve
tartışma biter. İstemcide aynı hesabın bir kopyası bulunabilir, ama yalnız gösterim için.

**Girdi nerede duruyor?** Hesabın ihtiyaç duyduğu veri sunucudaysa ve büyükse, hesabı veriye
götürmek veriyi hesaba götürmekten ucuzdur — kataloğun süzülmesi budur. Veri zaten
istemciye indirilmişse tersi geçerlidir.

**Kaç kez yapılıyor?** Kullanıcı etkileşimi başına birden çok kez tekrarlanan bir hesap
sunucuya taşındığında maliyet tur sayısıyla çarpılır. Tuş başına bir istek, saniyede birkaç
tur demektir.

Üç soru çelişebilir. Bir arama kutusu hem sık tekrarlanır hem de girdisi sunucudadır: bu
durumda hesabın kendisi sunucuda kalır, ama istek sıklığı gecikmeli tetikleme ile düşürülür.
Çelişki, sorulardan birinin iptal edilmesiyle değil, iki maliyetin birlikte azaltılmasıyla
çözülür.

#### Özet

- Aynı sonucu üreten iki yerleşim arasındaki seçim ölçülebilir: beş bin kayıtlık katalogda
  süzmenin sunucuya alınması taşınan gövdeyi 563 494 bayttan 1 956 bayta indirmiştir.
- Süzmenin yeri veri kümesinin büyüklüğüne bağlıdır; küçük ve zaten indirilmiş bir liste
  için her etkileşimde sunucuya gitmek gereksiz tur üretir.
- Bağlayıcı hesaplar ölçümle seçilmez: sunucu, kendi verisinden ve kendi saatinden
  türetemediği hiçbir değeri karar olarak almaz; istemcinin bildirdiği tutar ve saat
  yanıtı değiştirmemiştir.
- Sunum hesabını sunucuya taşımanın bedeli tur sayısıyla çarpılır; sekiz tuş vuruşu için
  ölçülen fark geri döngüde üç mertebedir, gerçek bir turda saniyelere çıkar.
- Karar üç soruyla verilir: hesap bağlayıcı mı, girdi nerede duruyor, kaç kez tekrarlanıyor.

#### Sonraki Adım

Buraya kadar "sunucu" tek bir program gibi anıldı: bağlantı noktasını dinleyen, isteği
okuyan, kuralı uygulayan ve yanıtı yazan tek bir süreç. Gerçek dağıtımlarda bu iş birden
çok bileşene bölünür. Kütüphane uygulamasının kapak görselleri ile arayüz dosyalarını sunan
bileşenin, ödünç kuralını çalıştıran bileşenle aynı olması gerekmez; hatta bir bileşenin
öndeki tek görevi isteği doğru yere iletmek olabilir. Sonraki ders bu iş bölümünü kurar:
statik dosyaları sunmak, istekleri yola göre ayırmak ve bir isteği başka bir sunucuya
devretmek ayrı ayrı çalıştırılarak gösterilir.

### Web Sunucusu Kavramı

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/web-sunucusu-kavrami

Sunucu tarafının üç ayrı rolü; dosyaları doğru başlıklarla sunmak, istekleri yola göre ayırmak ve isteği arka bir sunucuya devretmek ayrı süreçler olarak kurulup ölçülür.

Önceki iki derste "sunucu" tek bir program gibi ele alındı: bağlantı noktasını dinleyen,
isteği okuyan, kuralı uygulayan ve yanıtı yazan tek süreç. Kütüphane uygulamasının gerçek
trafiği ise türdeş değildir. Aynı bağlantı noktasına hem `/stil.css` isteği hem
`/api/kitaplar` isteği gelir; birincisi diskteki bir dosyanın olduğu gibi verilmesidir,
ikincisi bir hesabın sonucudur.

Bu ders o ayrımı üç role böler ve her rolü ayrı bir süreç olarak çalıştırır: **statik
sunum**, **yönlendirme** ve **vekillik**. Roller ayrı ayrı kurulduğunda hangi işin hangi
bileşene ait olduğu görünür hâle gelir; tek bir programda toplandıklarında da aynı roller
vardır, yalnız sınırları görünmez.

#### Statik Sunum: Dosyayı Bulmak ve Doğru Başlıkla Vermek

Statik sunum, bir isteğin yolunu dosya sistemindeki bir dosyaya eşleyip içeriği olduğu gibi
göndermektir. Görünüşte tek satırlık bir iştir; sorumluluklar sayıldığında dört tanedir.

```js
// statik-sunucu.mjs — varliklar/ dizinindeki dosyalari sunar, disina cikilmasini engeller
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { join, normalize, extname, sep } from "node:path";

const KOK = join(process.cwd(), "varliklar");
const TIPLER = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8",
  ".js": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8" };

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  yanit.setHeader("x-sunan", "statik");
  const yol = decodeURIComponent(new URL(istek.url, "http://yerel").pathname);
  const tam = normalize(join(KOK, yol === "/" ? "/index.html" : yol));

  if (!tam.startsWith(KOK + sep)) {                       // kok disina cikan yol reddedilir
    return yanit.writeHead(403, { "content-type": "text/plain" }).end("kok disi\n");
  }
  try {
    const icerik = await readFile(tam);
    yanit.writeHead(200, {
      "content-type": TIPLER[extname(tam)] ?? "application/octet-stream",
      "content-length": icerik.length,
      "cache-control": "public, max-age=31536000, immutable",
    }).end(icerik);
  } catch {
    yanit.writeHead(404, { "content-type": "text/plain" }).end("bulunamadi\n");
  }
}).listen(8427, "127.0.0.1", () => console.log("statik 127.0.0.1:8427"));
```

Örneği çalıştırmak için sunucu dosyasının yanında bir `varliklar` dizini ve içinde iki dosya
bulunmalıdır:

```bash
mkdir -p varliklar
printf 'body { font-family: system-ui; }\n.kitap { padding: 8px; }\n' > varliklar/stil.css
printf '<!doctype html>\n<html lang="tr"><head><meta charset="utf-8"><title>Kutuphane</title>\n<link rel="stylesheet" href="/stil.css"></head>\n<body><h1>Kutuphane Odunc Servisi</h1></body></html>\n' > varliklar/index.html
```

```bash
#!/usr/bin/env bash
# statik-sunucu.mjs'yi baslatir, dort istegi dener, durdurur. varliklar/ dizini yaninda olmalidir.
node statik-sunucu.mjs & sunucu=$!
sleep 0.5
for yol in / /stil.css /yok.css '/%2e%2e%2f%2e%2e%2fetc%2fhosts'; do
  printf '%-32s -> ' "$yol"
  curl -sS -o /dev/null --path-as-is -w '%{http_code}  tip=%{content_type}  boy=%{size_download} B\n' \
    "http://127.0.0.1:8427$yol"
done
echo "--- yanit basliklari ---"
curl -sS -o /dev/null -D - --path-as-is http://127.0.0.1:8427/stil.css
kill "$sunucu"
```

```
statik 127.0.0.1:8427
/                                -> 200  tip=text/html; charset=utf-8  boy=186 B
/stil.css                        -> 200  tip=text/css; charset=utf-8  boy=58 B
/yok.css                         -> 404  tip=text/plain  boy=11 B
/%2e%2e%2f%2e%2e%2fetc%2fhosts   -> 403  tip=text/plain  boy=9 B
--- yanit basliklari ---
HTTP/1.1 200 OK
x-sunan: statik
content-type: text/css; charset=utf-8
content-length: 58
cache-control: public, max-age=31536000, immutable
Connection: keep-alive
Keep-Alive: timeout=5
```

Dört sorumluluk çıktıda ayrı ayrı görünür. **Yol eşleme**: `/` isteği dizin girişine
karşılık gelir. **İçerik tipi**: aynı sunucu iki farklı tip üretmiştir; tip yanlış olduğunda
tarayıcı dosyayı yorumlayamaz ve stil uygulanmaz. **Bulunamayan kaynak**: eksik dosya bir
çökme değil, `404` durum kodudur. **Kök sınırı**: son istek `403` almıştır.

Son satır ayrıca bir uyarı taşır. `new URL()` yapısı yol üzerindeki `..` bileşenlerini kendi
kendine sadeleştirir; bu yüzden düz bir `/../../etc/hosts` isteği sunucuya ulaşmadan
temizlenir. Kodlanmış biçim (`%2e%2e%2f`) ise sadeleşmeden geçer ve `decodeURIComponent`
sonrası dizin dışına çıkan bir yola dönüşür. Denetimin çözümlenmiş **mutlak yol** üzerinde,
kök dizinle karşılaştırılarak yapılmasının nedeni budur: yolun metin hâline bakan bir
denetim, kodlamanın her biçimini önceden saymak zorunda kalır.

Yanıttaki `cache-control` başlığı, statik sunumun ayırt edici özelliğini taşır: içerik
isteğe göre değişmediği için uzun süreli saklanabilir. Bu başlığın anlamı ve ödünleşimleri
Tarayıcı ve Web Platformu kursunda kurulmuştu; burada önemli olan, aynı başlığın dinamik
bir yanıta konulamayacağıdır.

#### Yönlendirme ve Vekillik: İsteği Doğru Bileşene Götürmek

Statik sunucu `/api/kitaplar` isteğine ne yapacağını bilmez; ödünç uygulaması da CSS
dosyasını okumakla uğraşmamalıdır. Aradaki iş bölümünü kuran bileşen **ters vekildir**
(reverse proxy): istemciden gelen isteği kabul eder, bir kurala göre arka sunuculardan
birine iletir ve gelen yanıtı istemciye geri yazar.

```js
// uygulama-sunucu.mjs — odunc uygulamasi; istegin kendisine hangi baglantidan geldigini bildirir
import { createServer } from "node:http";

const KITAPLAR = [{ isbn: "978-0262033848", ad: "Algoritmalara Giris", raf: "R-12" }];

createServer((istek, yanit) => {
  yanit.sendDate = false;
  yanit.setHeader("x-sunan", "uygulama");
  yanit.setHeader("content-type", "application/json; charset=utf-8");
  yanit.writeHead(200).end(JSON.stringify({
    yol: istek.url,
    kitaplar: KITAPLAR,
    baglantiSahibi: `${istek.socket.remoteAddress}:${istek.socket.remotePort}`,
    forwarded: istek.headers.forwarded ?? null,
  }));
}).listen(8428, "127.0.0.1", () => console.log("uygulama 127.0.0.1:8428"));
```

```js
// vekil.mjs — one duran ters vekil: yola gore iki arka sunucudan birine iletir
import { createServer, request } from "node:http";

const HEDEF = (yol) => (yol.startsWith("/api/") ? 8428 : 8427);

createServer((istekDisi, yanitDisi) => {
  yanitDisi.sendDate = false;
  const kaynak = istekDisi.socket.remoteAddress;
  const basliklar = { ...istekDisi.headers, forwarded: `for="${kaynak}";proto=http` };

  const istekIc = request(
    { host: "127.0.0.1", port: HEDEF(istekDisi.url), path: istekDisi.url,
      method: istekDisi.method, headers: basliklar },
    (yanitIc) => {
      yanitDisi.writeHead(yanitIc.statusCode, { ...yanitIc.headers, "x-vekil": "gecti" });
      yanitIc.pipe(yanitDisi);
    },
  );
  istekIc.on("error", () => yanitDisi.writeHead(502).end("arka sunucuya ulasilamadi\n"));
  istekDisi.pipe(istekIc);
}).listen(8429, "127.0.0.1", () => console.log("vekil 127.0.0.1:8429"));
```

```bash
#!/usr/bin/env bash
# Uc sunucuyu birlikte baslatir: statik (8427), uygulama (8428), vekil (8429).
node statik-sunucu.mjs & s1=$!
sleep 0.3
node uygulama-sunucu.mjs & s2=$!
sleep 0.3
node vekil.mjs & s3=$!
sleep 0.6

echo "--- vekil uzerinden: hangi arka sunucu yanitladi ---"
for yol in /stil.css /api/kitaplar; do
  printf '%-16s -> ' "$yol"
  curl -sS -o /dev/null -D - "http://127.0.0.1:8429$yol" | grep -iE '^(HTTP|x-sunan|x-vekil)' | tr -d '\r' | paste -sd' ' -
done

echo "--- uygulamanin gordugu baglanti: dogrudan istek ve vekil uzerinden istek ---"
printf 'dogrudan : '; curl -sS http://127.0.0.1:8428/api/kitaplar | tr ',' '\n' | grep -E 'baglantiSahibi|forwarded' | paste -sd' ' -
printf 'vekille  : '; curl -sS http://127.0.0.1:8429/api/kitaplar | tr ',' '\n' | grep -E 'baglantiSahibi|forwarded' | paste -sd' ' -

echo "--- ek atlamanin suresi (ayni yanit, iki yol) ---"
curl -sS -o /dev/null -w 'dogrudan : %{time_total} s\n' http://127.0.0.1:8428/api/kitaplar
curl -sS -o /dev/null -w 'vekille  : %{time_total} s\n' http://127.0.0.1:8429/api/kitaplar

kill "$s1" "$s2" "$s3"
```

```
statik 127.0.0.1:8427
uygulama 127.0.0.1:8428
vekil 127.0.0.1:8429
--- vekil uzerinden: hangi arka sunucu yanitladi ---
/stil.css        -> HTTP/1.1 200 OK x-sunan: statik x-vekil: gecti
/api/kitaplar    -> HTTP/1.1 200 OK x-sunan: uygulama x-vekil: gecti
--- uygulamanin gordugu baglanti: dogrudan istek ve vekil uzerinden istek ---
dogrudan : "baglantiSahibi":"127.0.0.1:57388" "forwarded":null}
vekille  : "baglantiSahibi":"127.0.0.1:57387" "forwarded":"for=\"127.0.0.1\";proto=http"}
--- ek atlamanin suresi (ayni yanit, iki yol) ---
dogrudan : 0.000551 s
vekille  : 0.000962 s
```

Üç sunucunun açılış satırlarının çıktıdaki sırası çalıştırmadan çalıştırmaya değişir; geçici
bağlantı noktası numaraları ve süre alanları da her çalıştırmada farklıdır.

İstemci tek bir adrese, 8429'a istek yapmıştır. `x-sunan` başlığı iki isteğin iki ayrı
programdan geldiğini gösterir. İstemci açısından bu ayrım görünmez: tek köken, tek adres,
tek yapılandırma. Yönlendirme kuralı burada tek satırdır — yol `/api/` ile başlıyorsa
uygulamaya, başlamıyorsa statik sunucuya.

#### Vekilin Getirdiği ve Götürdüğü

Vekil ücretsiz değildir. Ölçümdeki iki süre, aynı yanıt için doğrudan bağlantı ile vekil
üzerinden geçen bağlantıyı karşılaştırır: vekilli yol daha uzundur, çünkü ikinci bir TCP
bağlantısı kurulur, istek yeniden yazılır ve yanıt yeniden aktarılır. Geri döngü arayüzünde
bu fark küçüktür; gerçek bir dağıtımda vekil ile arka sunucu arasındaki ağ da hesaba
katılır.

Karşılığında birkaç iş tek noktada toplanır. **Şifreli bağlantı önde sonlandırılabilir**;
arka sunucular düz HTTP konuşur ve sertifika yönetimi tek bileşene iner. **Aynı yol kuralı
birden çok arka örneğe dağıtım yapabilir**: hedef seçimi tek bir bağlantı noktası yerine bir
liste üzerinden yapıldığında vekil bir yük dağıtıcıya dönüşür. **Statik varlıklar uygulama
sürecine hiç uğramaz**, bu yüzden uygulamanın eşzamanlılık bütçesi yalnız hesaplı isteklere
harcanır.

Bedelin bir kısmı ise bilgi kaybıdır ve çıktıda görünür. Uygulama, isteği kimin açtığını
`istek.socket` üzerinden okur; vekil devreye girdiğinde bu bağlantıyı açan taraf **vekildir**,
istemci değil. Ölçüm aynı makinede yapıldığı için iki satırdaki adres de `127.0.0.1`'dir;
ayırt edici olan, bağlantının sahibinin değişmiş olmasıdır. İstemcinin kim olduğu bilgisi
ancak vekilin eklediği `Forwarded` başlığıyla taşınır ve çıktıda yalnız vekilli satırda
doludur.

Bu başlık bir **iddiadır**, ölçüm değil. İstemci de aynı başlığı gönderebilir. Bu yüzden
uygulama, `Forwarded` başlığını yalnız bilinen bir vekilden geldiğini doğruladığı
bağlantılarda dikkate alır; doğrudan erişime açık bir uygulamada başlık yok sayılır. Aynı
akıl yürütme ikinci derste kurulmuştu: istemciden gelen hiçbir değer, sunucunun kendi
denetiminde olmadığı sürece karar sayılmaz.

#### Roller mi, Programlar mı?

Üç dosya üç ayrı süreç başlattı, ama bu bir zorunluluk değil bir gösterim tercihidir. Aynı
üç rol tek bir programda da bulunabilir: uygulama, `/api/` ile başlamayan yolları kendi
dosya okuma dalına yönlendirebilir. Ayrımın kalıcı olan yanı programların sayısı değil,
sorumlulukların içeriğidir.

- **Web sunucusu rolü**: bağlantıyı kabul eder, HTTP iletisini ayrıştırır, statik içeriği
  doğrudan sunar, yolları hedeflere eşler. Bu rolün bilmediği şey iş kurallarıdır.
- **Uygulama sunucusu rolü**: isteği bir işleyiciye taşır, iş kuralını çalıştırır, yanıt
  gövdesini üretir. Bu rolün bilmediği şey dosya sisteminin düzenidir.
- **Vekillik rolü**: iki tarafı birbirine bağlar, kaynak bilgisini taşır, hedefler arasında
  dağıtım yapar, arka sunucu erişilemediğinde `502` üretir.

Rolleri ayırmanın pratik sonucu, her birinin ayrı ölçeklenebilmesidir: statik trafiği artan
bir sistem statik tarafı çoğaltır, hesaplı istekleri artan bir sistem uygulama tarafını.
Rollerin tek bir programda toplandığı bir kurulumda bu iki ihtiyaç birbirine bağlanır ve
ikisi birden çoğaltılır.

#### Özet

- Statik sunum dört sorumluluk taşır: yolun dosyaya eşlenmesi, doğru içerik tipi, eksik
  dosyanın `404` ile karşılanması ve kök dizin sınırının çözümlenmiş mutlak yol üzerinden
  denetlenmesi.
- Adres çözümlemesi `..` bileşenlerini sadeleştirdiği için yol denetimi metin üzerinde değil,
  birleştirilmiş ve normalleştirilmiş yolun kök dizinle karşılaştırılmasıyla yapılır.
- Ters vekil, tek bir adrese gelen istekleri bir kurala göre arka sunuculara dağıtır;
  ölçümde `x-sunan` başlığı aynı adrese yapılan iki isteğin iki ayrı programdan geldiğini
  gösterir.
- Vekil bir atlama ekler ve bağlantının sahibini değiştirir; istemcinin kimliği ancak
  `Forwarded` başlığıyla taşınır ve bu başlık yalnız bilinen bir vekilden geldiğinde
  dikkate alınır.
- Web sunucusu, uygulama sunucusu ve vekillik birer roldür; ayrı süreçlere bölünmeleri
  zorunlu değildir, ama ayrı ölçeklenebilmelerini bu bölünme sağlar.

#### Sonraki Adım

Vekilin arkasındaki uygulama sunucusu bu derste tek bir süreç olarak çalıştı ve istekleri
sırayla karşıladı. Bir istek uzun sürdüğünde diğerlerine ne olur? Aynı uygulamanın iki
kopyası aynı bağlantı noktasını nasıl paylaşır ve istekler aralarında nasıl dağılır?
Ölçekleme kararının birimi süreç müdür, iş parçacığı mıdır, yoksa başka bir şey midir?
Sonraki ders uygulamanın altındaki çalışma zamanına iner: olay döngüsünün tek bir isteği
nasıl beklettiğini ölçer, sonra aynı bağlantı noktasını paylaşan işçi süreçler başlatıp
isteklerin dağılımını sayar.

### Uygulama Çalışma Zamanı

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/uygulama-calisma-zamani

Uygulamanın altındaki süreç modeli: olay döngüsünü bloklayan işin diğer istekleri ne kadar beklettiği ölçülür, iş parçacığı havuzunun sınırı gösterilir ve ölçekleme biriminin neden süreç olduğu aynı bağlantı noktasını paylaşan işçilerle sayılır.

Önceki ders vekilin arkasındaki uygulama sunucusunu tek bir süreç olarak çalıştırdı ve
sorusunu açık bıraktı: bir istek uzun sürdüğünde diğerlerine ne olur, aynı uygulamanın iki
kopyası aynı bağlantı noktasını nasıl paylaşır, ölçekleme kararının birimi süreç müdür yoksa
iş parçacığı mıdır? Bu ders o üç soruyu ölçerek yanıtlar.

Ölçüm için kütüphane ödünç servisinin iki ucu kullanılacak: üye parolasını doğrulayan pahalı
bir uç ve katalogdan tek satır döndüren ucuz bir uç. İkisi aynı süreçte yaşadığında pahalı
işin ucuz isteklere ne yaptığı, aradaki gecikmeyle görünür hâle gelir.

#### Çalışma Zamanı Neyi Sağlar

**Çalışma zamanı** (runtime), uygulama kodunun üstünde durduğu katmandır: kodu yürüten
motoru, işletim sisteminin soket ve dosya arayüzlerine açılan kapıları ve bu ikisinin
arasında işleri sıraya koyan **olay döngüsünü** (event loop) barındırır. Node.js Çalışma
Zamanı kursunda kurulan model burada olduğu gibi geçerlidir; bu derste yeni olan, aynı
modelin bir sunucu uygulamasının ölçekleme kararlarını nasıl belirlediğidir.

Model tek cümleyle şudur: uygulama kodu **tek bir iş parçacığında** (thread) çalışır. Bir
istek işleyicisi çalışırken başka hiçbir işleyici çalışmaz. Olay döngüsü, bir işleyici
bittikten sonra kuyruktaki sıradaki işi alır. Girdi-çıktı beklemeleri — disk okuma, ağ
çağrısı, veritabanı sorgusu — bu kuyruğu meşgul etmez; çalışma zamanı beklemeyi işletim
sistemine devreder ve sonuç geldiğinde geri çağrıyı kuyruğa koyar.

Bu modelin sonucu iki tanedir. Bekleyen iş ucuzdur: binlerce açık bağlantı tek süreçte
tutulabilir, çünkü her biri yalnız bir soket ve bir geri çağrıdır. Buna karşılık **hesaplayan
iş pahalıdır**: işlemciyi meşgul eden bir hesap, süresi boyunca kuyruğu kilitler.

#### Bloklayan İş: Bir İstek Diğerlerini Ne Kadar Bekletir

Aşağıdaki sunucu üye parolası doğrulamasını iki ayrı yoldan yapar. `dogrula-bloklayan`
anahtar türetmeyi doğrudan çağırır ve sonucu beklerken olay döngüsünü elinde tutar.
`dogrula-havuzda` aynı hesabı çalışma zamanının **iş parçacığı havuzuna** (thread pool)
verir ve döngüyü serbest bırakır. Hesabın kendisi ikisinde de aynıdır.

```js
// tek-surec.mjs — odunc servisi tek surecte; ayni is hem bloklayan hem bloklamayan bicimde
import { createServer } from "node:http";
import { scrypt, scryptSync } from "node:crypto";

const RAFLAR = [{ isbn: "978-0262033848", ad: "Algoritmalara Giris", raf: "R-12" }];

// Anahtar turetme maliyeti: yarim saniyenin uzerinde islemci isi uretecek kadar yuksek.
const MALIYET = { N: 1 << 16, r: 8, p: 8, maxmem: 512 * 1024 * 1024 };

createServer((istek, yanit) => {
  yanit.sendDate = false;
  yanit.setHeader("content-type", "application/json; charset=utf-8");
  const yol = new URL(istek.url, "http://yerel").pathname;

  if (yol === "/uye/dogrula-bloklayan") {
    const ozet = scryptSync("parola", "tuz", 32, MALIYET).toString("hex").slice(0, 16);
    return yanit.writeHead(200).end(JSON.stringify({ ozet, yol }));
  }
  if (yol === "/uye/dogrula-havuzda") {
    return scrypt("parola", "tuz", 32, MALIYET, (hata, anahtar) => {
      if (hata) return yanit.writeHead(500).end(JSON.stringify({ hata: "turetme" }));
      yanit.writeHead(200).end(JSON.stringify({ ozet: anahtar.toString("hex").slice(0, 16), yol }));
    });
  }
  if (yol === "/kitaplar") {
    return yanit.writeHead(200).end(JSON.stringify({ kitaplar: RAFLAR }));
  }
  yanit.writeHead(404).end(JSON.stringify({ hata: "yol_yok" }));
}).listen(8430, "127.0.0.1", () => console.log("tek surec 127.0.0.1:8430 pid=" + process.pid));
```

Ölçüm şu düzendedir: önce ağır istek gönderilir, elli milisaniye sonra dört hafif istek
arkasından gelir. Hafif isteklerin kendi işi neredeyse yoktur; süreleri, yalnızca sıralarını
beklemekten gelir.

```bash
#!/usr/bin/env bash
# tek-surec.mjs'yi baslatir; agir istegin yaninda gelen dort hafif istegin suresini olcer.
node tek-surec.mjs & sunucu=$!
sleep 0.6
curl -sS -o /dev/null http://127.0.0.1:8430/kitaplar          # isinma

olc() {  # $1 = agir ucun yolu
  pidler=()
  curl -sS -o /dev/null -w "agir     %{time_total} s\n" "http://127.0.0.1:8430$1" > agir.txt &
  pidler+=($!)
  sleep 0.05
  for i in 1 2 3 4; do
    curl -sS -o /dev/null -w "hafif-$i  %{time_total} s\n" http://127.0.0.1:8430/kitaplar > "hafif-$i.txt" &
    pidler+=($!)
  done
  wait "${pidler[@]}"
  cat agir.txt hafif-1.txt hafif-2.txt hafif-3.txt hafif-4.txt
}

echo "--- agir is olay dongusunde: /uye/dogrula-bloklayan ---"
olc /uye/dogrula-bloklayan
echo "--- agir is is parcacigi havuzunda: /uye/dogrula-havuzda ---"
olc /uye/dogrula-havuzda

kill "$sunucu"
rm -f agir.txt hafif-*.txt
```

```
tek surec 127.0.0.1:8430 pid=14674
--- agir is olay dongusunde: /uye/dogrula-bloklayan ---
agir     0.635000 s
hafif-1  0.581384 s
hafif-2  0.581601 s
hafif-3  0.580992 s
hafif-4  0.581166 s
--- agir is is parcacigi havuzunda: /uye/dogrula-havuzda ---
agir     0.618827 s
hafif-1  0.001071 s
hafif-2  0.000954 s
hafif-3  0.000756 s
hafif-4  0.001232 s
```

Süreç kimliği ve süreler her çalıştırmada değişir; ağır işin mutlak süresi de makinenin
işlemcisine bağlıdır. Değişmeyen, iki bloktaki hafif isteklerin süreleri arasındaki
büyüklük farkıdır.

Birinci ölçümde hafif istekler yaklaşık **580 milisaniye** beklemiştir. Kendi işleri bir
milisaniyeden azdır; bekledikleri şey, ağır isteğin kalan süresidir. Dördü de neredeyse aynı
anda tamamlanır, çünkü hepsi aynı engelin kalkmasını beklemiş ve engel kalkınca art arda
işlenmiştir.

İkinci ölçümde ağır isteğin süresi değişmemiştir — hesap aynı hesaptır — ama hafif istekler
**bir milisaniyenin** altında yanıtlanmıştır. Aradaki tek fark, hesabın nerede yapıldığıdır.
Havuz sürümünde olay döngüsü hesabı bir iş parçacığına verip serbest kalmış, arada gelen
istekleri normal hızında karşılamış, sonuç hazır olduğunda geri çağrıyı çalıştırmıştır.

#### Havuz da Sınırlıdır

İş parçacığı havuzu bloklayan işin çözümü değil, taşındığı yerdir. Havuzun iş parçacığı
sayısı sabittir ve varsayılan değeri küçüktür. Havuza aynı anda kapasitesinden fazla iş
verilirse fazlası kuyruğa girer; bu kez olay döngüsü değil, havuz doyar. Ölçümdeki tek ağır
istek havuzu doldurmadığı için gecikme görünmemiştir.

Bu, ölçeklemenin ilk kuralını verir: **bir sürecin işlemci kapasitesi sabittir.** Hesabı
döngüden havuza taşımak isteklerin birbirini beklemesini azaltır, ama sürecin toplam işlemci
işini artırmaz. Saniyede yapılabilecek anahtar türetme sayısı, işi hangi iş parçacığının
yaptığından bağımsız olarak makinenin çekirdek sayısıyla sınırlıdır.

#### Ölçekleme Birimi: Süreç

Tek bir süreç tek bir çekirdeği doyurabilir. Çok çekirdekli bir makinede kalan çekirdekleri
kullanmanın yolu, uygulamanın birden çok kopyasını çalıştırmaktır. **Küme** (cluster)
düzeninde bir **birincil süreç** (primary process) dinleyen soketi açar ve birkaç **işçi
süreç** (worker process) çatallar; gelen bağlantılar işçiler arasında dağıtılır. İşçiler aynı
bağlantı noktasını paylaşır, çünkü soketi açan taraf birincil süreçtir.

```js
// kume.mjs — ayni baglanti noktasini paylasan uc isci surec; her surecin kendi sayaci var
import cluster from "node:cluster";
import { createServer } from "node:http";

const ISCI_SAYISI = 3;

if (cluster.isPrimary) {
  let kapaniyor = false;
  for (let i = 0; i < ISCI_SAYISI; i++) cluster.fork();

  cluster.on("exit", (isci) => {                        // olen iscinin yerine yenisi
    if (kapaniyor) return;
    console.log(`isci oldu pid=${isci.process.pid}, yerine yenisi catallaniyor`);
    cluster.fork();
  });

  process.on("SIGTERM", () => {                         // isciler birincille birlikte iner
    kapaniyor = true;
    for (const isci of Object.values(cluster.workers)) isci.kill();
    process.exit(0);
  });

  console.log(`birincil pid=${process.pid}, isci sayisi=${ISCI_SAYISI}`);
} else {
  let oduncSayaci = 0;                                  // SURECE OZEL bellek: her iscide ayri
  createServer((istek, yanit) => {
    yanit.sendDate = false;
    yanit.setHeader("content-type", "application/json; charset=utf-8");
    if (new URL(istek.url, "http://yerel").pathname === "/odunc") oduncSayaci++;
    yanit.writeHead(200).end(JSON.stringify({ pid: process.pid, oduncSayaci }));
  }).listen(8431, "127.0.0.1");
}
```

Sunucu her yola yanıt verir ve yanıtta kendi süreç kimliğini bildirir; yalnız `/odunc` yolu
sayacı artırır. Bu ayrım, dağılımı sayaç değerini değiştirmeden yoklamayı sağlar.

```bash
#!/usr/bin/env bash
# kume.mjs'yi baslatir; 12 istegin isciler arasindaki dagilimini, sayaclarin ayrismasini
# ve olen iscinin yerine yenisinin gelmesini gosterir.
node kume.mjs & kume=$!
sleep 0.8

echo "--- 12 odunc istegi: hangi isci kac kez yanitladi ---"
for i in $(seq 12); do curl -sS http://127.0.0.1:8431/odunc; echo; done |
  grep -o '"pid":[0-9]*' | sort | uniq -c

echo "--- ayni uce dort istek: her iscinin sayaci kendi belleginde ---"
for i in 1 2 3 4; do curl -sS http://127.0.0.1:8431/odunc; echo; done

echo "--- bir isci oldurulur, yerine yenisi gelir ---"
kurban=$(curl -sS http://127.0.0.1:8431/durum | sed -n 's/.*"pid":\([0-9]*\).*/\1/p')
echo "oldurulen isci pid=$kurban"
kill -9 "$kurban"
sleep 0.8
for i in $(seq 6); do curl -sS http://127.0.0.1:8431/durum; echo; done |
  grep -o '"pid":[0-9]*' | sort -u

kill "$kume"
```

```
birincil pid=3068, isci sayisi=3
--- 12 odunc istegi: hangi isci kac kez yanitladi ---
      4 "pid":3070
      4 "pid":3071
      4 "pid":3072
--- ayni uce dort istek: her iscinin sayaci kendi belleginde ---
{"pid":3072,"oduncSayaci":5}
{"pid":3070,"oduncSayaci":5}
{"pid":3071,"oduncSayaci":5}
{"pid":3072,"oduncSayaci":6}
--- bir isci oldurulur, yerine yenisi gelir ---
oldurulen isci pid=3070
isci oldu pid=3070, yerine yenisi catallaniyor
"pid":3071
"pid":3072
"pid":3098
```

Süreç kimlikleri her çalıştırmada başkadır. Sabit olan üç gözlemdir.

**Dağılım eşittir.** On iki istek üç işçiye dörder düşmüştür; birincil süreç bağlantıları
sırayla dağıtmıştır. İstemci tarafında hiçbir şey değişmemiştir: aynı adrese, aynı bağlantı
noktasına istek yapılmıştır. Kaç sürecin çalıştığı istemciye görünmez.

**Ölen işçinin yerine yenisi gelir.** Bir işçi zorla sonlandırıldığında birincil süreç bunu
görür ve yeni bir işçi çatallar; son listede eski kimliklerden biri yoktur, yerine yeni bir
kimlik vardır. Bu, sürecin ölçekleme biriminin yanı sıra **yalıtım birimi** olduğunu da
gösterir: bir işçinin çökmesi diğerlerini etkilemez ve hizmet kesilmez.

#### Süreç Belleği Ölçekleme Sınırıdır

Çıktıdaki en öğretici satırlar sayaç satırlarıdır. On iki isteğin ardından her işçinin
sayacı dörttür; sonraki dört istek dağıtıldığında değerler `5`, `5`, `5` ve `6` olarak
okunur. Hiçbir işçi toplam sayıyı bilmez, çünkü sayaç **süreç belleğinde** yaşar ve süreçler
belleklerini paylaşmaz.

Kütüphane uygulamasında bunun karşılığı doğrudandır. Ödünç sayısı, oturum bilgisi, geçici
rezervasyon listesi, hız sınırı sayacı — hepsi süreç belleğinde tutulduğunda ölçekleme
anında bozulur. Üyenin oturum açtığı istek bir işçiye, sonraki isteği başka bir işçiye
düşer ve ikinci işçi oturumu tanımaz.

Bunun sonucu, çalışma zamanı modelinin uygulama tasarımına koyduğu bağlayıcı kuraldır:
**süreç durumsuz olmalıdır.** İstekler arasında yaşaması gereken her değer süreç dışına,
paylaşılan bir depoya taşınır. Süreç yalnız o isteğin süresince yaşayan değerleri kendi
belleğinde tutar. Bu kural sağlandığında süreç sayısını artırmak bir yapılandırma kararına
iner; sağlanmadığında ikinci süreci başlatmak hatalara yol açar.

Aynı kural sürecin ölümlü olmasından da gelir. Ölen işçinin belleği onunla birlikte gider;
yerine gelen işçi boş bellekle başlar. Süreç belleğinde tutulan bir değer, yalnız ölçekleme
sırasında değil, her yeniden başlatmada kaybolur.

#### Özet

- Uygulama kodu tek bir iş parçacığında çalışır; bekleyen girdi-çıktı ucuzdur, işlemciyi
  meşgul eden hesap ise süresi boyunca kuyruğu kilitler.
- Ölçümde olay döngüsünü bloklayan yarım saniyelik bir hesap, yanında gelen dört hafif
  isteği yaklaşık 580 milisaniye bekletmiştir; aynı hesap iş parçacığı havuzuna
  verildiğinde hafif istekler bir milisaniyenin altında yanıtlanmıştır.
- İş parçacığı havuzu işi taşır, kapasiteyi artırmaz; bir sürecin işleyebileceği toplam
  işlemci işi sabittir.
- Ölçekleme birimi süreçtir: birincil süreç dinleyen soketi açar, işçi süreçler aynı
  bağlantı noktasını paylaşır, on iki istek üç işçiye dörder dağılmıştır ve ölen işçinin
  yerine yenisi çatallanmıştır.
- Süreçler bellek paylaşmadığı için süreç belleğindeki sayaç işçiler arasında ayrışır;
  istekler arasında yaşaması gereken durum süreç dışına, paylaşılan bir depoya taşınmalıdır.

#### Sonraki Adım

Bu ders sürecin dışını ölçtü: kaç kopya çalışır, istek hangi kopyaya düşer, kopyalar neyi
paylaşmaz. Sürecin içinde ise tek bir isteğin geçtiği bir yol vardır ve bu yol henüz tek bir
kutu gibi ele alındı. Bağlantı kabul edildiği andan yanıtın son baytı yazıldığı ana kadar
kaç ayrı aşama vardır, her aşama ne kadar sürer ve bir aşamada verilen karar sonrakini nasıl
kısıtlar? Sonraki ders isteğin yaşam döngüsünü aşamalara böler; her aşamaya zaman damgası
koyup sırayı ve süreleri yazdırır.

### İstek Yaşam Döngüsü

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/istek-yasam-dongusu

Bir isteğin bağlantı kabulünden son baytın yazılmasına uzanan yolu yedi aşamaya bölünür; her aşamaya zaman damgası konularak sıra ve süreler ölçülür, bağlantı ile isteğin ayrı şeyler olduğu ve durum satırının ne zaman geri alınamaz hâle geldiği gösterilir.

Önceki ders sürecin dışını ölçtü: kaç kopya çalışır, istek hangi kopyaya düşer, kopyalar
neyi paylaşmaz. Sürecin içinde ise tek bir isteğin geçtiği bir yol vardır ve o yol şimdiye
kadar tek bir kutu gibi ele alındı. Bu ders kutuyu açar.

İstek işleyicisi, dışarıdan bakıldığında "isteği al, yanıtı ver" biçiminde tek adımlı
görünür. Gerçekte arada, her biri kendi zamanında olan ve her biri sonrakini kısıtlayan
aşamalar vardır. Aşamaları görmenin en doğrudan yolu, her birine bir zaman damgası koyup
sırayı yazdırmaktır. Kütüphane ödünç servisinin iki ucu — kitap ödünç verme ve katalog
listeleme — bu ölçüm için kullanılacak.

#### Yedi Aşama

Bir isteğin sunucu tarafındaki yolu şu adımlardan geçer:

1. **Bağlantı kabulü.** İşletim sistemi bir TCP bağlantısını dinleyen sokete koyar, uygulama
   onu kabul eder. Bu noktada henüz hiçbir HTTP verisi okunmamıştır.
2. **Başlık satırlarının çözülmesi.** İstek satırı ve başlıklar okunup ayrıştırılır. Yöntem,
   yol ve başlıklar artık bilinir; gövde bilinmez.
3. **Gövdenin okunması.** Gövde parça parça gelir; son parça geldiğinde istek tamdır.
4. **Yönlendirme.** Yöntem ve yol bir işleyiciye eşlenir. Eşleşme yoksa isteğin karşılığı
   `404` olur.
5. **İşleyicinin çalışması.** İş kuralı uygulanır, veri okunur veya yazılır, yanıt gövdesi
   üretilir.
6. **Yanıt başlığının yazılması.** Durum kodu ve başlıklar sokete yazılır.
7. **Yanıtın tamamlanması.** Gövdenin son baytı yazılır ve bağlantı ya kapanır ya da sonraki
   istek için açık kalır.

Bu sıra bir kural değil, bir gözlemdir; aşağıdaki sunucu her aşamayı gerçekleştiği anda
kaydeder.

#### Aşamaları Ölçmek

```js
// yasam-dongusu.mjs — bir istegin gectigi asamalari zaman damgasiyla kaydeder ve yazdirir
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";

const KATALOG = new Map([["978-0262033848", { ad: "Algoritmalara Giris", adet: 2 }]]);
const t0 = () => process.hrtime.bigint();
const ms = (a, b) => Number(b - a) / 1e6;                    // nanosaniye farki -> milisaniye

let baglantiNo = 0;

const sunucu = createServer();

sunucu.on("connection", (soket) => {                        // 1. TCP baglantisi kabul edildi
  soket.kabulAni = t0();
  soket.no = ++baglantiNo;
  soket.istekSayisi = 0;
});

sunucu.on("request", (istek, yanit) => {
  const soket = istek.socket;
  soket.istekSayisi++;
  const asamalar = [["baglanti kabulu", soket.kabulAni]];
  const kaydet = (ad) => asamalar.push([ad, t0()]);

  kaydet("baslik satirlari cozuldu");                       // 2. istek olayi = basliklar hazir

  const parcalar = [];
  istek.on("data", (parca) => parcalar.push(parca));
  istek.on("end", async () => {
    kaydet("govde okundu");                                 // 3. govdenin son baytini aldik

    const yol = new URL(istek.url, "http://yerel").pathname;
    const isleyici = yol === "/odunc" && istek.method === "POST" ? "oduncVer"
      : yol === "/kitaplar" ? "katalogListele" : null;
    kaydet(`yonlendirme -> ${isleyici ?? "yok"}`);           // 4. yol bir isleyiciye eslendi

    let durum = 404, govde = { hata: "yol_yok" };
    if (isleyici === "oduncVer") {
      const istem = JSON.parse(Buffer.concat(parcalar).toString() || "{}");
      const kitap = KATALOG.get(istem.isbn);
      await readFile(new URL(import.meta.url));              // is: gercek bir girdi-cikti
      durum = kitap && kitap.adet > 0 ? 201 : 409;
      govde = durum === 201 ? { isbn: istem.isbn, uye: istem.uye, kalan: --kitap.adet }
        : { hata: "raf_bos" };
    } else if (isleyici === "katalogListele") {
      durum = 200;
      govde = { kitaplar: [...KATALOG].map(([isbn, k]) => ({ isbn, ...k })) };
    }
    kaydet("isleyici bitti");                                // 5. yanit govdesi hazir

    const metin = JSON.stringify(govde);
    yanit.writeHead(durum, { "content-type": "application/json; charset=utf-8",
      "content-length": Buffer.byteLength(metin) });
    kaydet("baslik yazildi");                                // 6. durum satiri artik kesin

    yanit.end(metin);
    yanit.on("finish", () => {                               // 7. son bayt cekirdege verildi
      kaydet("yanit tamamlandi");
      const baslangic = asamalar[0][1];
      console.log(`\n#${soket.no}.${soket.istekSayisi}  ${istek.method} ${istek.url} -> ${durum}`);
      for (const [ad, an] of asamalar) {
        console.log(`  ${String(ms(baslangic, an).toFixed(3)).padStart(9)} ms  ${ad}`);
      }
    });
  });
});

sunucu.listen(8432, "127.0.0.1", () => console.log("yasam dongusu 127.0.0.1:8432"));
```

Ölçüm üç istek yapar: aynı bağlantı üzerinde bir ödünç verme ve bir katalog listeleme, sonra
ayrı bir bağlantıda bilinmeyen bir yol. `--next` seçeneği aynı `curl` çağrısı içinde ikinci
bir istek tanımlar ve bağlantı yeniden kullanılır.

```bash
#!/usr/bin/env bash
# yasam-dongusu.mjs'yi baslatir; tek baglantida iki istek ve ayri baglantida bir istek yapar.
node yasam-dongusu.mjs & sunucu=$!
sleep 0.6

echo "### tek baglanti uzerinde iki istek (curl baglantiyi yeniden kullanir)"
curl -sS -o /dev/null \
  -X POST -H 'content-type: application/json' \
  -d '{"isbn":"978-0262033848","uye":"U-4711"}' http://127.0.0.1:8432/odunc \
  --next -sS -o /dev/null http://127.0.0.1:8432/kitaplar
sleep 0.3

echo "### ayri baglantida bilinmeyen yol"
curl -sS -o /dev/null http://127.0.0.1:8432/raflar
sleep 0.3

kill "$sunucu"
```

```
yasam dongusu 127.0.0.1:8432
### tek baglanti uzerinde iki istek (curl baglantiyi yeniden kullanir)

#1.1  POST /odunc -> 201
      0.000 ms  baglanti kabulu
      1.378 ms  baslik satirlari cozuldu
      2.192 ms  govde okundu
      2.240 ms  yonlendirme -> oduncVer
      3.534 ms  isleyici bitti
      5.434 ms  baslik yazildi
      6.882 ms  yanit tamamlandi

#1.2  GET /kitaplar -> 200
      0.000 ms  baglanti kabulu
      7.580 ms  baslik satirlari cozuldu
      7.702 ms  govde okundu
      7.728 ms  yonlendirme -> katalogListele
      7.779 ms  isleyici bitti
      7.823 ms  baslik yazildi
      8.088 ms  yanit tamamlandi
### ayri baglantida bilinmeyen yol

#2.1  GET /raflar -> 404
      0.000 ms  baglanti kabulu
      0.180 ms  baslik satirlari cozuldu
      0.243 ms  govde okundu
      0.278 ms  yonlendirme -> yok
      0.280 ms  isleyici bitti
      0.334 ms  baslik yazildi
      0.541 ms  yanit tamamlandi
```

Süreler her çalıştırmada değişir; ilk isteğin ölçümleri modül yüklemesinin ve ilk dosya
okumasının ısınma maliyetini de taşır. Değişmeyen, aşamaların sırası ve aralarındaki
büyüklük ilişkisidir.

Üçüncü ölçüm bir taban çizgisi verir: eşleşmeyen bir yol için toplam yarım milisaniyedir ve
bu sürenin tamamı protokol işidir — bağlantı kabulü, ayrıştırma, yanıt yazımı. Uygulamanın
kendi işi sıfırdır. İlk ölçümde `isleyici bitti` ile `yonlendirme` arasındaki 1,3 milisaniye
ise gerçek iştir: dosya okuması ve iş kuralı. Bir isteğin yavaş olduğu söylendiğinde
sorulacak ilk soru, sürenin bu iki bölümden hangisinde geçtiğidir.

#### Bağlantı ile İstek Ayrı Şeylerdir

İkinci ölçümün ilk satırı `0.000 ms baglanti kabulu` der, ama ikinci satırı `7.580 ms`
gösterir. Arada yeni bir bağlantı kabulü yoktur; ikinci istek, birinci isteğin bittiği
bağlantıdan gelmiştir. `#1.1` ve `#1.2` etiketleri bunu doğrudan söyler: aynı bağlantı,
ikinci istek. Üçüncü istek `#2.1` etiketiyle yeni bir bağlantı numarası almıştır.

Bu ayrım pratik sonuçlar taşır. **Bağlantı başına yapılan iş bir kez yapılır**: TCP el
sıkışması, güvenli bağlantı kurulumu ve buna bağlı gecikme, bağlantı açık kaldığı sürece
sonraki isteklere yayılır. Buna karşılık **bağlantıya bağlanan durum tehlikelidir**: bir
soket üzerinde birden çok üyenin isteği geçebilir, çünkü aradaki vekil bağlantıyı yeniden
kullanabilir. Kimlik bilgisi bağlantıya değil, isteğe aittir.

Ayrım kaynak yönetimini de belirler. Açık bağlantı sayısı, işlenen istek sayısından
bağımsızdır; hiç istek göndermeyen bir istemci de bir bağlantı tutar. Sunucunun bağlantı
sayısına ve boşta bekleme süresine ayrı sınır koymasının nedeni budur.

#### Gövde Bir Değer Değil, Bir Akıştır

Kodda gövde `data` olaylarıyla parça parça toplanır ve `end` olayıyla tamamlanır. Bu, ölçüm
çıktısında `baslik satirlari cozuldu` ile `govde okundu` arasındaki aralık olarak görünür.
Küçük bir JSON gövdesinde bu aralık bir milisaniyenin altındadır; büyük bir yüklemede
saniyeler sürebilir.

Buradan iki karar çıkar. Birincisi, **yönlendirme gövdeden önce yapılabilir.** Bu örnekte
yönlendirme kararı gövde okunduktan sonra kaydedilmiştir, çünkü kod okunaklı olsun diye
işleyici mantığı tek yerde toplanmıştır. Yöntem ve yol ikinci aşamada zaten bilindiği için
yönlendirme oraya alınabilir; alındığında eşleşmeyen bir istek gövdesi hiç okunmadan
reddedilir.

İkincisi, **gövde sınırlanmalıdır.** Sunucu gövdeyi belleğe topladığı sürece, gönderilen
bayt sayısı sunucunun bellek tüketimini belirler. Sınır konmazsa tek bir istemci süreci
belleksiz bırakabilir. Sınırın nasıl konduğu ve aşıldığında hangi durum kodunun döndüğü,
bu kursun Dosya Yükleme ve Depolama dersinde ele alınır.

#### Kararın Geri Alınamadığı An

Altıncı aşama bir eşiktir. Durum satırı ve başlıklar sokete yazıldıktan sonra istemci onları
almıştır; geri alınamaz. Bunu doğrulamak için yanıt yazımına başladıktan sonra başlık
eklemeyi ve durum kodunu değiştirmeyi deneyen bir sunucu yeterlidir.

```js
// baslik-sonrasi.mjs — durum satiri yazildiktan sonra kararin geri alinamadigini gosterir
import { createServer } from "node:http";

createServer((istek, yanit) => {
  yanit.sendDate = false;
  yanit.writeHead(201, { "content-type": "application/json; charset=utf-8" });
  yanit.write('{"durum":"odunc verildi"');            // govde akmaya basladi

  for (const [ad, deneme] of [
    ["setHeader", () => yanit.setHeader("x-uyari", "gec kalindi")],
    ["writeHead", () => yanit.writeHead(500)],
  ]) {
    try {
      deneme();
      console.log(`${ad}: gecti`);
    } catch (hata) {
      console.log(`${ad}: ${hata.code}`);
    }
  }

  yanit.end(',"not":"durum kodu 201 olarak kaldi"}');
}).listen(8433, "127.0.0.1", () => console.log("baslik sonrasi 127.0.0.1:8433"));
```

```bash
#!/usr/bin/env bash
# baslik-sonrasi.mjs'yi baslatir; yanitin basliklarini ve govdesini alir, sunucu kutugunu gosterir.
node baslik-sonrasi.mjs & sunucu=$!
sleep 0.6
curl -sS -D - http://127.0.0.1:8433/odunc
echo
kill "$sunucu"
```

```
baslik sonrasi 127.0.0.1:8433
setHeader: ERR_HTTP_HEADERS_SENT
writeHead: ERR_HTTP_HEADERS_SENT
HTTP/1.1 201 Created
content-type: application/json; charset=utf-8
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"durum":"odunc verildi","not":"durum kodu 201 olarak kaldi"}
```

Sunucunun kütük satırları ile `curl` çıktısı aynı uçbirime yazıldığı için ilk iki satır
sunucudan, kalanı istemciden gelir.

İki deneme de aynı hata koduyla reddedilmiştir ve yanıt `201` olarak kalmıştır. Çıktıda
ayrıca `content-length` yerine `Transfer-Encoding: chunked` görünür: gövde parça parça
yazıldığı için sunucu uzunluğu önceden bildirememiştir.

Sonucun kural hâline gelmiş biçimi şudur: **yanıtla ilgili bütün kararlar ilk baytı yazmadan
önce alınmış olmalıdır.** İşleyicinin ortasında yazmaya başlayıp sonra bir hatayla
karşılaşan kod, o hatayı istemciye bildiremez — yarım bir gövde ve yanlış bir durum kodu
kalır. Yanıt gövdesini önce bellekte üretip sonra tek seferde yazmanın, ya da akışlı
gönderimde hata olasılığını akış başlamadan tüketmenin nedeni budur.

#### Özet

- Bir isteğin yolu yedi aşamadan geçer: bağlantı kabulü, başlıkların çözülmesi, gövdenin
  okunması, yönlendirme, işleyicinin çalışması, başlığın yazılması ve yanıtın tamamlanması.
- Ölçümde eşleşmeyen bir yol yarım milisaniyede tamamlanmıştır; bu süre protokol işidir.
  Uygulamanın kendi işi, yönlendirme ile işleyicinin bitişi arasındaki aralıkta görünür.
- Bağlantı ile istek ayrı yaşam sürelerine sahiptir: aynı bağlantı üzerindeki ikinci istek
  yeni bir kabul aşaması içermez, bu yüzden kimlik ve durum bağlantıya değil isteğe bağlanır.
- Gövde parça parça gelir; yönlendirme gövdeden önce yapılabilir ve gövdenin boyutu
  sınırlanmadığında sunucunun bellek tüketimini istemci belirler.
- Başlıklar yazıldıktan sonra başlık eklemek ve durum kodunu değiştirmek
  `ERR_HTTP_HEADERS_SENT` ile reddedilir; yanıtla ilgili kararlar ilk bayttan önce alınır.

#### Sonraki Adım

Ölçülen aşamaların hepsi tek bir işleyicinin içindeydi ve her aşama koda elle yazılmıştı.
Gerçek bir uygulamada aynı işler — kimliğin çözülmesi, gövdenin ayrıştırılması, isteğin
kütüğe yazılması, hatanın yakalanması — her işleyicide tekrarlanır. Bu tekrarları tek bir
yerde toplayan yapı, isteği sırayla geçiren bir zincirdir. Sonraki ders o zinciri kurar:
halkaların sırası değiştiğinde sonucun nasıl değiştiğini, kimlik denetimiyle günlüklemenin
hangi sırayla dizilmesi gerektiğini ve hata yakalayıcının zincirin neresinde durduğunu
ölçerek gösterir.

### Ara Katman Zinciri

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/ara-katman-zinciri

Her istekte tekrarlanan işleri tek yerde toplayan soğan modeli zincir kurulur; aynı halkalar dört ayrı sırayla çalıştırılıp kimlik denetiminin günlüklemeye göre yeri, hata yakalayıcının kapsamı ve kütük satırlarının doğruluğu ölçülerek karşılaştırılır.

Önceki ders bir isteğin yedi aşamasını ölçtü ve bütün işleri tek bir işleyicinin içine
yazdı. Gerçek bir uygulamada aynı işlerin çoğu her uçta tekrarlanır: gövdenin ayrıştırılması,
kimliğin çözülmesi, isteğin kütüğe yazılması, hatanın yakalanması. Bunlar tek bir uca ait
olmadıkları için **kesişen ilgiler** (cross-cutting concerns) adını alır.

Kesişen ilgileri tek yerde toplayan yapı, isteği sırayla geçiren bir zincirdir. Bu ders o
zinciri kütüphane ödünç servisi için kurar ve bir soruya yanıt arar: sıra ne kadar önemlidir?
Yanıt, aynı halkaların dört ayrı sırayla çalıştırılıp sonuçların karşılaştırılmasıyla
verilecektir.

#### Soğan Modeli

Bir **ara katman** (middleware), isteği ve yanıtı temsil eden bir bağlam ile zincirin geri
kalanını çağıran bir fonksiyon alır. Halka işini yapar, `sonraki()` çağırır, denetim geri
döndüğünde ikinci bir iş daha yapabilir. Bu yüzden zincir düz bir boru değil, iç içe geçmiş
katmanlardır: istek dıştan içe girer, yanıt içten dışa çıkar.

Modelin üç sonucu vardır ve üçü de bu dersin ölçümlerini belirler:

- **Bir halka `sonraki()` çağırmazsa zincir orada durur.** Kimlik denetimi başarısız
  olduğunda arkasındaki hiçbir halka çalışmaz.
- **Bir halkanın `sonraki()` sonrası kodu, kendisinden içerideki her şey bittikten sonra
  çalışır.** Süre ölçümü ve kütük çıkış satırı buraya yazılır.
- **Bir halka yalnız kendisinden içerideki hataları yakalayabilir.** Dışarıdaki bir halkanın
  fırlattığı hata ona ulaşmaz.

#### Zinciri Kurmak

Aşağıdaki program beş halka tanımlar ve bunları dört ayrı sırada dizer. Sıra komut satırından
seçilir; halkaların kendisi hiç değişmez.

```js
// ara-katman.mjs — istegi sirayla gecen halkalardan bir zincir; sira komut satirindan secilir
import { createServer } from "node:http";

let sayac = 0;

const yaz = (b, durum, govde) => {
  const metin = JSON.stringify(govde);
  b.yanit.writeHead(durum, { "content-type": "application/json; charset=utf-8",
    "content-length": Buffer.byteLength(metin) });
  b.yanit.end(metin);
};

// --- halkalar: her biri (baglam, sonraki) alir; sonraki() cagrilmazsa zincir orada durur ---

const kutukle = async (b, sonraki) => {
  console.log(`${b.id} -> ${b.istek.method} ${b.istek.url}`);
  try {
    await sonraki();
  } finally {
    console.log(`${b.id} <- ${b.yanit.headersSent ? b.yanit.statusCode : "yanit yazilmadi"}`);
  }
};

const hataYakala = async (b, sonraki) => {
  try {
    await sonraki();
  } catch (hata) {
    console.log(`${b.id} !! yakalandi: ${hata.kod ?? hata.name}`);
    yaz(b, hata.kod === "govde_bicimsiz" ? 400 : 500,
      { hata: hata.kod ?? "ic_hata", istekKimligi: b.id });
  }
};

const govdeCoz = async (b, sonraki) => {
  const parcalar = [];
  for await (const parca of b.istek) parcalar.push(parca);
  const metin = Buffer.concat(parcalar).toString();
  try {
    b.govde = metin ? JSON.parse(metin) : {};
  } catch {
    const hata = new Error("govde JSON olarak cozulemedi");
    hata.kod = "govde_bicimsiz";
    throw hata;
  }
  await sonraki();
};

const kimlikDenetle = async (b, sonraki) => {
  const uye = (b.istek.headers.authorization ?? "").replace(/^Uye /, "");
  if (!/^U-\d{4}$/.test(uye)) return yaz(b, 401, { hata: "kimlik_yok", istekKimligi: b.id });
  b.uye = uye;
  await sonraki();
};

const yonlendir = async (b) => {
  const yol = new URL(b.istek.url, "http://yerel").pathname;
  if (yol === "/odunc" && b.istek.method === "POST") {
    return yaz(b, 201, { odunc: { isbn: b.govde?.isbn, uye: b.uye }, istekKimligi: b.id });
  }
  yaz(b, 404, { hata: "yol_yok", istekKimligi: b.id });
};

// --- ayni halkalar, dort ayri sira ---

const ZINCIRLER = {
  "yakalayici-disarida": [hataYakala, kutukle, govdeCoz, kimlikDenetle, yonlendir],
  "kimlik-once": [hataYakala, kimlikDenetle, kutukle, govdeCoz, yonlendir],
  "yakalayici-icte": [kutukle, govdeCoz, hataYakala, kimlikDenetle, yonlendir],
  "kutuk-disarida": [kutukle, hataYakala, govdeCoz, kimlikDenetle, yonlendir],
};

const secim = process.argv[2] ?? "yakalayici-disarida";
const halkalar = ZINCIRLER[secim];
if (!halkalar) {
  console.error(`bilinmeyen zincir: ${secim}`);
  process.exit(2);
}

const zinciriCalistir = (b) => {
  const ilerle = (i) =>
    i === halkalar.length ? Promise.resolve() : halkalar[i](b, () => ilerle(i + 1));
  return ilerle(0);
};

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const b = { istek, yanit, id: `i-${++sayac}` };
  zinciriCalistir(b).catch((hata) => {                  // son care: zincir disina cikan hata
    console.log(`${b.id} XX zincir disina cikti: ${hata.kod ?? hata.name}`);
    if (!yanit.headersSent) yaz(b, 500, { hata: "ic_hata" });
  });
}).listen(8434, "127.0.0.1", () => console.log(`zincir=${secim} 127.0.0.1:8434`));
```

Zincir çalıştırıcısı beş satırdır: `ilerle(i)` i. halkayı çağırır ve ona `sonraki` olarak
`ilerle(i + 1)` verir. Listenin sonuna gelindiğinde çözülmüş bir söz döner. Sunucu
geri çağrısındaki `catch`, zincirden dışarı çıkan bir hatanın süreci düşürmesini engelleyen
**son çare** halkasıdır; ölçümde bunun ne zaman devreye girdiği görünecektir.

Ölçüm her sıra için aynı üç isteği yapar: geçerli bir ödünç isteği, kimlik bilgisi
taşımayan bir istek ve gövdesi bozuk bir istek.

```bash
#!/usr/bin/env bash
# Ayni uc istegi dort ayri halka sirasiyla tekrarlar; her turda yanitlari ve sunucu kutugunu basar.
UC=http://127.0.0.1:8434/odunc

istekler() {
  echo "  [1] gecerli    : $(curl -sS -X POST -H 'authorization: Uye U-4711' \
    -H 'content-type: application/json' -d '{"isbn":"978-0262033848"}' "$UC")"
  echo "  [2] kimliksiz  : $(curl -sS -X POST \
    -H 'content-type: application/json' -d '{"isbn":"978-0262033848"}' "$UC")"
  echo "  [3] bozuk govde: $(curl -sS -X POST -H 'authorization: Uye U-4711' \
    -H 'content-type: application/json' -d '{isbn' "$UC")"
}

for zincir in yakalayici-disarida kimlik-once yakalayici-icte kutuk-disarida; do
  echo "=== zincir: $zincir ==="
  node ara-katman.mjs "$zincir" > "kutuk-$zincir.txt" 2>&1 & s=$!
  sleep 0.6
  istekler
  sleep 0.3
  kill "$s"; wait "$s" 2>/dev/null
  echo "  --- sunucu kutugu ---"
  sed 's/^/  /' "kutuk-$zincir.txt"
done

rm -f kutuk-*.txt
```

```
=== zincir: yakalayici-disarida ===
  [1] gecerli    : {"odunc":{"isbn":"978-0262033848","uye":"U-4711"},"istekKimligi":"i-1"}
  [2] kimliksiz  : {"hata":"kimlik_yok","istekKimligi":"i-2"}
  [3] bozuk govde: {"hata":"govde_bicimsiz","istekKimligi":"i-3"}
  --- sunucu kutugu ---
  zincir=yakalayici-disarida 127.0.0.1:8434
  i-1 -> POST /odunc
  i-1 <- 201
  i-2 -> POST /odunc
  i-2 <- 401
  i-3 -> POST /odunc
  i-3 <- yanit yazilmadi
  i-3 !! yakalandi: govde_bicimsiz
=== zincir: kimlik-once ===
  [1] gecerli    : {"odunc":{"isbn":"978-0262033848","uye":"U-4711"},"istekKimligi":"i-1"}
  [2] kimliksiz  : {"hata":"kimlik_yok","istekKimligi":"i-2"}
  [3] bozuk govde: {"hata":"govde_bicimsiz","istekKimligi":"i-3"}
  --- sunucu kutugu ---
  zincir=kimlik-once 127.0.0.1:8434
  i-1 -> POST /odunc
  i-1 <- 201
  i-3 -> POST /odunc
  i-3 <- yanit yazilmadi
  i-3 !! yakalandi: govde_bicimsiz
=== zincir: yakalayici-icte ===
  [1] gecerli    : {"odunc":{"isbn":"978-0262033848","uye":"U-4711"},"istekKimligi":"i-1"}
  [2] kimliksiz  : {"hata":"kimlik_yok","istekKimligi":"i-2"}
  [3] bozuk govde: {"hata":"ic_hata"}
  --- sunucu kutugu ---
  zincir=yakalayici-icte 127.0.0.1:8434
  i-1 -> POST /odunc
  i-1 <- 201
  i-2 -> POST /odunc
  i-2 <- 401
  i-3 -> POST /odunc
  i-3 <- yanit yazilmadi
  i-3 XX zincir disina cikti: govde_bicimsiz
=== zincir: kutuk-disarida ===
  [1] gecerli    : {"odunc":{"isbn":"978-0262033848","uye":"U-4711"},"istekKimligi":"i-1"}
  [2] kimliksiz  : {"hata":"kimlik_yok","istekKimligi":"i-2"}
  [3] bozuk govde: {"hata":"govde_bicimsiz","istekKimligi":"i-3"}
  --- sunucu kutugu ---
  zincir=kutuk-disarida 127.0.0.1:8434
  i-1 -> POST /odunc
  i-1 <- 201
  i-2 -> POST /odunc
  i-2 <- 401
  i-3 -> POST /odunc
  i-3 !! yakalandi: govde_bicimsiz
  i-3 <- 400
```

#### Kimlik Denetimi ile Günlüklemenin Sırası

İlk iki turun istemciye dönen yanıtları **birebir aynıdır**. Üç istek, üç aynı gövde. Sıra
değişikliği istemcinin gördüğü hiçbir şeyi değiştirmemiştir.

Sunucu kütüğünde ise bir satır çifti eksiktir. `kimlik-once` turunda `i-2` hiç görünmez.
Kimlik denetimi günlüklemeden önce geldiği için kimliksiz istek zincirin ikinci halkasında
durmuş, kütük halkasına hiç ulaşmamıştır. Uygulama kimliksiz isteği reddetmiş ama bunu
kaydetmemiştir.

Bunun pratik sonucu ağırdır. Kütüphane servisine bir saat boyunca kimliksiz istek yağdıran
bir istemci, bu sırada hiçbir iz bırakmaz. Reddedilen istekler tam da en çok kaydedilmesi
gereken isteklerdir: hız sınırı kararları, kötüye kullanım tespiti ve hata ayıklama bu
kayıtlara dayanır.

Sıranın ters yönde bir gerekçesi de vardır ve göz ardı edilmemelidir. `kimlik-once`
düzeninde kimliksiz istek, gövdesi hiç okunmadan reddedilir; `yakalayici-disarida`
düzeninde gövde önce belleğe alınır, sonra kimlik denetimi başarısız olur. Büyük gövdeli
istekler söz konusu olduğunda erken reddetme gerçek bir tasarruftur.

İki gerekçenin uzlaştığı düzen, **kütüklemenin en dışta, kimlik denetiminin gövde
ayrıştırmasından önce** olduğu düzendir: her istek kaydedilir, kimliksiz istek yine de
gövdesi okunmadan reddedilir. Ölçümdeki `kutuk-disarida` sırası tam olarak budur ve `i-2`
satırları kütükte yerini almıştır.

#### Hata Yakalayıcının Kapsamı Yerinden Gelir

Üçüncü turda istemcinin gördüğü yanıt değişir. `yakalayici-icte` düzeninde bozuk gövdeli
istek `{"hata":"ic_hata"}` alır; diğer üç düzende `{"hata":"govde_bicimsiz"}` ile birlikte
istek kimliği de gelir.

Nedeni doğrudan soğan modelidir. `yakalayici-icte` sırasında `hataYakala`, `govdeCoz`
halkasının **içindedir**. Gövde ayrıştırması hata fırlattığında bu hata dışa doğru yayılır ve
yakalayıcıya hiç uğramaz. Kütükteki `XX zincir disina cikti` satırı bunu söyler: hatayı
zincirin kendisi değil, sunucu geri çağrısındaki son çare yakalamıştır.

Son çare yanıtı kaçınılmaz olarak niteliksizdir. O noktada isteğin hangi halkada
kırıldığı, hangi alanın bozuk olduğu ve kullanıcıya ne söylenmesi gerektiği bilinmez; geriye
`500` ve genel bir ileti kalır. Kullanıcı hatası olan bir durum iç hata olarak raporlanmış,
istek kimliği yanıta konulamamış ve destek isteyen üyenin elinde izlenecek bir numara
kalmamıştır.

Kural şudur: **bir hata yakalayıcı yalnız kendisinden içeride kalanı korur.** Zincirin
tamamını koruması isteniyorsa mümkün olduğunca dışta durmalıdır. Son çare bir yedektir, bir
tasarım değil; devreye girdiği her istek, zincirde eksik kalan bir kapsamın işaretidir.

#### Kütük Satırının Doğru Durumu Yazması

Dört turun kütükleri karşılaştırıldığında `yakalayici-disarida` düzeninde bir kusur görünür:
üçüncü istek için `i-3 <- yanit yazilmadi` yazılmış, yakalayıcının kaydı ise ancak ondan
sonra gelmiştir. Kütük satırı, yanıtın `400` olduğunu bilmeden yazılmıştır.

Bu da sıradan gelir. `hataYakala` en dışta olduğunda, `kutukle` halkasının `finally` bloğu
hata dışa doğru yayılırken çalışır — yani yakalayıcı yanıtı yazmadan **önce**. Kütük,
kendisinden dışarıda alınan bir kararı göremez.

`kutuk-disarida` düzeninde iki halkanın yeri değişir: kütükleme en dışta, hata yakalayıcı
onun hemen içinde. Şimdi yakalayıcı yanıtı yazar, denetim `kutukle` halkasına döner ve
`finally` bloğu `i-3 <- 400` yazar. Aynı üç istek, aynı beş halka; yalnız iki halkanın yeri
değişmiştir ve kütük artık doğrudur.

Buradan zincir dizmenin genel kuralı çıkar: **bir halka, gözlemlemek veya değiştirmek
istediği her şeyin dışında olmalıdır.** Kütükleme yanıtın son hâlini görmek istiyorsa yanıtı
üreten her halkanın dışında durur; hata yakalayıcı bütün hataları görmek istiyorsa hata
fırlatabilecek her halkanın dışında durur. İkisi birlikte, kütüklemenin yakalayıcıdan da
dışta olması gerektiğini söyler.

#### Özet

- Ara katman zinciri soğan modelidir: istek dıştan içe girer, yanıt içten dışa çıkar; bir
  halkanın `sonraki()` sonrası kodu, kendisinden içerideki her şey bittikten sonra çalışır.
- Kimlik denetimi günlüklemeden önce geldiğinde reddedilen istek kütükte hiç görünmez;
  ölçümde `kimlik-once` turunda `i-2` satırları eksiktir, istemciye dönen yanıtlar ise
  bu iki düzende birebir aynıdır.
- Erken kimlik denetimi gövdeyi hiç okumadan reddetme kazancı sağlar; iki gerekçe,
  kütükleme en dışta ve kimlik denetimi gövde ayrıştırmasından önce dizildiğinde uzlaşır.
- Bir hata yakalayıcı yalnız kendisinden içerideki hataları görür; `yakalayici-icte`
  düzeninde bozuk gövde `{"hata":"ic_hata"}` ile yanıtlanmış, diğerlerinde istek kimliğiyle
  birlikte `{"hata":"govde_bicimsiz"}` dönmüştür.
- Kütük satırı yalnız kendisinden içeride alınan kararları görebilir; yakalayıcı kütüklemenin
  dışında kaldığında durum `yanit yazilmadi` olarak kaydedilmiş, sıra düzeltildiğinde
  `i-3 <- 400` doğru değerle yazılmıştır.

#### Sonraki Adım

Bu bölüm bir isteğin sunucu tarafındaki yolunu bileşenlerine ayırdı: web sunucusu ve vekil
rolleri, çalışma zamanının süreç modeli, isteğin aşamaları ve kesişen ilgileri sıraya koyan
zincir. Ortaya çıkan zincirin halkaları ise hâlâ tek bir dosyada, hepsi yan yana durur.
Sonraki bölüm bu parçaları taşıyabilecek bir iskelet kurar: dosyalar hangi ölçüte göre
bölünür, hangi modül hangisini çağırabilir ve bu sınır elle mi yoksa bir denetimle mi
korunur? İlk ders proje yapısını ele alır ve bağımlılık yönünü sınayan bir denetim yazar.

## Uygulama İskeleti

Sunucu uygulamasının taşıyıcı yapısı; katman ve modül düzeni, ortamdan okunan ve açılışta doğrulanan yapılandırma, gizli değerlerin ayrılması, yapılandırılmış günlükleme, ortak hata yanıtı üretimi ve taşınabilirlik ilkeleri.

### Proje Yapısı

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/proje-yapisi

Dosyaların katman odaklı ve özellik odaklı iki düzeni ölçülerek karşılaştırılır; alan, veri, taşıma ve birleştirme sorumlulukları ayrılır ve bağımlılık yönü kuralı, ihlali çıkış koduyla bildiren bir denetim programına bağlanır.

Önceki bölüm bir isteğin sunucu tarafındaki yolunu bileşenlerine ayırdı. Ortaya çıkan
parçalar — zincir halkaları, işleyiciler, iş kuralları, veri erişimi — henüz tek bir dosyada,
yan yana duruyor. Bir dosya büyüdükçe değişmesi zorlaşır; asıl sorun ise satır sayısı değil,
birbirini bilmemesi gereken parçaların birbirine erişebilmesidir.

Bu ders kütüphane ödünç servisini taşıyabilecek bir dizin düzeni kurar ve tek bir soruya
yanıt arar: hangi modül hangisini çağırabilir? Yanıt bir belge olarak değil, ihlali çıkış
koduyla bildiren bir denetim programı olarak yazılacaktır.

#### İki Düzen: Katman Odaklı ve Özellik Odaklı

Bir uygulamanın dosyalarını bölmenin iki yaygın ölçütü vardır. **Katman odaklı** düzen üst
dizinleri teknik ilgiye göre açar: bütün iş kuralları bir dizinde, bütün veri erişimi
başkasında. **Özellik odaklı** düzen üst dizinleri iş alanına göre açar: ödünç işlemine ait
her şey bir dizinde, katalog işlemine ait her şey başkasında.

Aynı dokuz modül iki düzene de yerleştirilebilir. Aşağıdaki betik ikisini de kurar ve iki
farklı değişikliğin kaç dizine dokunduğunu sayar.

```bash
#!/usr/bin/env bash
# Ayni dokuz modulu iki ayri duzende yerlestirir ve bir degisikligin kac dizine dokundugunu sayar.
mkdir -p duzenler

for ozellik in odunc katalog uye; do
  for ilgi in alan veri http; do
    mkdir -p "duzenler/katman-odakli/kaynak/$ilgi" "duzenler/ozellik-odakli/kaynak/$ozellik"
    printf '// %s ozelligi, %s ilgisi\n' "$ozellik" "$ilgi" \
      > "duzenler/katman-odakli/kaynak/$ilgi/$ozellik.mjs"
    printf '// %s ozelligi, %s ilgisi\n' "$ozellik" "$ilgi" \
      > "duzenler/ozellik-odakli/kaynak/$ozellik/$ilgi.mjs"
  done
done

echo "--- katman odakli agac ---"
find duzenler/katman-odakli -name '*.mjs' | sort
echo "--- ozellik odakli agac ---"
find duzenler/ozellik-odakli -name '*.mjs' | sort

dizinSayisi() {  # $1 = agac koku, $2 = aranan isaret
  grep -rl "$2" "$1" | xargs -n1 dirname | sort -u | wc -l | tr -d ' '
}

echo "--- bir degisiklik kac dizine dokunur ---"
printf '%-16s %-22s %s\n' "duzen" "odunc ozelligi" "veri ilgisi"
for duzen in katman-odakli ozellik-odakli; do
  printf '%-16s %-22s %s\n' "$duzen" \
    "$(dizinSayisi "duzenler/$duzen" 'odunc ozelligi') dizin" \
    "$(dizinSayisi "duzenler/$duzen" 'veri ilgisi') dizin"
done
```

```
--- katman odakli agac ---
duzenler/katman-odakli/kaynak/alan/katalog.mjs
duzenler/katman-odakli/kaynak/alan/odunc.mjs
duzenler/katman-odakli/kaynak/alan/uye.mjs
duzenler/katman-odakli/kaynak/http/katalog.mjs
duzenler/katman-odakli/kaynak/http/odunc.mjs
duzenler/katman-odakli/kaynak/http/uye.mjs
duzenler/katman-odakli/kaynak/veri/katalog.mjs
duzenler/katman-odakli/kaynak/veri/odunc.mjs
duzenler/katman-odakli/kaynak/veri/uye.mjs
--- ozellik odakli agac ---
duzenler/ozellik-odakli/kaynak/katalog/alan.mjs
duzenler/ozellik-odakli/kaynak/katalog/http.mjs
duzenler/ozellik-odakli/kaynak/katalog/veri.mjs
duzenler/ozellik-odakli/kaynak/odunc/alan.mjs
duzenler/ozellik-odakli/kaynak/odunc/http.mjs
duzenler/ozellik-odakli/kaynak/odunc/veri.mjs
duzenler/ozellik-odakli/kaynak/uye/alan.mjs
duzenler/ozellik-odakli/kaynak/uye/http.mjs
duzenler/ozellik-odakli/kaynak/uye/veri.mjs
--- bir degisiklik kac dizine dokunur ---
duzen            odunc ozelligi         veri ilgisi
katman-odakli    3 dizin                1 dizin
ozellik-odakli   1 dizin                3 dizin
```

Tablo simetriktir ve seçimin niteliğini gösterir: hiçbiri diğerinden iyi değildir, ikisi
farklı değişiklik biçimlerini ucuzlatır. Ödünç kuralına yeni bir koşul eklemek özellik odaklı
düzende tek dizine dokunur; veri erişiminin biçimini değiştirmek katman odaklı düzende tek
dizine dokunur.

Seçimi belirleyen soru şudur: bu uygulamada hangi değişiklik daha sık olur? Az sayıda iş
alanı ve sık değişen teknik ilgiler varsa katman odaklı düzen daha az dosya dolaştırır;
birbirinden bağımsız çok sayıda iş alanı varsa özellik odaklı düzen aynı işi yapar. Bu dersin
geri kalanı katman odaklı düzeni kullanır, çünkü kurulacak kural — bağımlılık yönü — orada
dizin adlarıyla doğrudan ifade edilebilir.

#### Dört Sorumluluk

Katman odaklı düzende dört dizin vardır ve her biri bilmediği bir şeyle tanımlanır.

**Alan** (`kaynak/alan`) iş kurallarını taşır ve başka hiçbir katmanı çağırmaz. Veritabanının
nerede olduğunu, isteğin HTTP ile geldiğini, yanıtın JSON olduğunu bilmez.

```js
// kaynak/alan/odunc.mjs — odunc verme kurali; hicbir katmani cagirmaz
export const ODUNC_GUN = 14;
export const UYE_ODUNC_SINIRI = 5;

export const oduncVerilebilir = (kitap, acikOduncSayisi) => {
  if (!kitap) return { olur: false, kod: "kitap_yok" };
  if (kitap.rafta < 1) return { olur: false, kod: "raf_bos" };
  if (acikOduncSayisi >= UYE_ODUNC_SINIRI) return { olur: false, kod: "sinir_asildi" };
  return { olur: true };
};

export const sonTarih = (baslangic) =>
  new Date(baslangic.getTime() + ODUNC_GUN * 86400000);
```

Bu fonksiyonların hiçbiri eşzamansız değildir ve hiçbiri dış dünyaya dokunmaz. Girdileri
verilmiş bir kararı hesaplarlar; bu yüzden sınanmaları için ne sunucu ne veritabanı gerekir.

**Veri** (`kaynak/veri`) kalıcılığı üstlenir ve iş kurallarını bilmez. Kaç ödünç sınırının
olduğu ona sorulmaz; ona yalnız sayı sorulur.

```js
// kaynak/veri/kitap-deposu.mjs — kaliciligi ustlenir; is kurallarini bilmez
const KITAPLAR = new Map([
  ["978-0262033848", { ad: "Algoritmalara Giris", rafta: 1 }],
  ["978-0201835953", { ad: "Efsanevi Adam-Ay", rafta: 0 }],
]);
const ODUNCLER = [];

export const kitapBul = async (isbn) => KITAPLAR.get(isbn) ?? null;

export const acikOduncSayisi = async (uye) =>
  ODUNCLER.filter((o) => o.uye === uye && !o.iadeEdildi).length;

export const oduncKaydet = async (kayit) => {
  KITAPLAR.get(kayit.isbn).rafta -= 1;
  ODUNCLER.push(kayit);
  return kayit;
};
```

Depo burada bellekte tutulur. Önceki derste kurulan kural gereği bu, birden çok süreçle
çalışmayan geçici bir uygulamadır; kalıcı bir depoya geçildiğinde değişmesi gereken tek
dizin budur ve alan katmanı bu değişiklikten etkilenmez.

**Taşıma** (`kaynak/http`) protokolü alan diline çevirir ve iş kuralı içermez. Yaptığı iş
veriyi toplamak, karar almayı alan katmanına bırakmak ve sonucu bir durum koduna eşlemektir.

```js
// kaynak/http/odunc-ucu.mjs — HTTP istemini alan diline cevirir; is kurali icermez
import { oduncVerilebilir, sonTarih } from "../alan/odunc.mjs";
import { kitapBul, acikOduncSayisi, oduncKaydet } from "../veri/kitap-deposu.mjs";

export const oduncVer = async ({ isbn, uye }, bugun = new Date()) => {
  const kitap = await kitapBul(isbn);
  const karar = oduncVerilebilir(kitap, await acikOduncSayisi(uye));
  if (!karar.olur) return { durum: 409, govde: { hata: karar.kod } };

  const kayit = await oduncKaydet({ isbn, uye, sonTarih: sonTarih(bugun), iadeEdildi: false });
  return { durum: 201, govde: { isbn, uye, sonTarih: kayit.sonTarih.toISOString() } };
};
```

**Birleştirme** (`kaynak/kurulum`) parçaları bir araya getiren tek yerdir. Bir uygulamada
bağımlılıkların bağlandığı, yapılandırmanın okunduğu ve sunucunun ayağa kaldırıldığı nokta
buradadır.

```js
// kaynak/kurulum/uygulama.mjs — parcalari birlestiren tek yer
import { oduncVer } from "../http/odunc-ucu.mjs";

const bugun = new Date("2026-03-01T00:00:00Z");
for (const istem of [
  { isbn: "978-0262033848", uye: "U-4711" },   // rafta tek kopya var
  { isbn: "978-0262033848", uye: "U-4712" },   // ayni kopya artik odunc verildi
  { isbn: "978-0000000000", uye: "U-4713" },   // katalogda yok
]) {
  console.log(JSON.stringify(await oduncVer(istem, bugun)));
}
```

#### Bağımlılık Yönü

Dizinlerin varlığı tek başına hiçbir şeyi güvenceye almaz. Ayrımı anlamlı kılan, aralarındaki
içe aktarımların **tek yönlü** olmasıdır: dış katman iç katmanı çağırabilir, iç katman dışı
çağıramaz.

| Katman | İçe aktarabildikleri |
|---|---|
| `alan` | — |
| `veri` | `alan` |
| `http` | `alan`, `veri` |
| `kurulum` | `alan`, `veri`, `http` |

Kuralın gerekçesi somuttur. Alan katmanı veri katmanını çağırırsa iş kuralını sınamak için
bir depo gerekir; depo değiştiğinde kural da değişir. Ödünç sınırının beş olduğunu bilen
fonksiyonun aynı zamanda kaydın nerede tutulduğunu da bilmesi, iki ayrı nedenle değişen bir
modül üretir.

Kural, alan katmanının ihtiyaç duyduğu veriyi **parametre olarak alması** ile sağlanır.
`oduncVerilebilir` fonksiyonu üyenin açık ödünç sayısını kendisi sorgulamaz; sayıyı çağırandan
alır. Sorgulama işi bir dış katmana, burada taşıma katmanına düşer.

#### Kuralı Denetime Bağlamak

Belgede yazan bir kural, ihlal edildiğinde kimseyi uyarmaz. Aşağıdaki program içe aktarım
satırlarını okur, her dosyanın katmanını dizin adından çıkarır ve izin tablosuna uymayan her
satırı bildirir. İhlal varsa sıfırdan farklı bir çıkış kodu üretir; bu, denetimin bir sürekli
tümleştirme adımına bağlanabilmesi için gereken tek şeydir.

```js
// denetim/bagimlilik.mjs — katmanlar arasi bagimlilik yonunu sinar; ihlalde 1 ile cikar
import { readdir, readFile } from "node:fs/promises";
import { join, relative, sep } from "node:path";

const IZIN = {                          // her katmanin ice aktarabilecegi katmanlar
  alan: [],
  veri: ["alan"],
  http: ["alan", "veri"],
  kurulum: ["alan", "veri", "http"],
};

const ICE_AKTARIM = /^\s*import[^"']*["']([^"']+)["']/gm;

const girdiler = await readdir("kaynak", { withFileTypes: true, recursive: true });
const dosyalar = girdiler.filter((g) => g.isFile() && g.name.endsWith(".mjs"))
  .map((g) => join(g.parentPath, g.name)).sort();

let ihlal = 0;
for (const yol of dosyalar) {
  const kaynakKatman = relative("kaynak", yol).split(sep)[0];
  const metin = await readFile(yol, "utf8");
  for (const [, tanimlayici] of metin.matchAll(ICE_AKTARIM)) {
    if (!tanimlayici.startsWith(".")) continue;            // paket ve yerlesik moduller disinda
    const hedef = tanimlayici.replace(/^(\.\.\/|\.\/)+/, "").split("/")[0];
    if (!(hedef in IZIN) || hedef === kaynakKatman) continue;
    if (!IZIN[kaynakKatman].includes(hedef)) {
      console.log(`IHLAL  ${yol}: ${kaynakKatman} -> ${hedef}`);
      ihlal++;
    }
  }
}

console.log(ihlal === 0
  ? `${dosyalar.length} dosya tarandi, bagimlilik yonu kurala uygun`
  : `${dosyalar.length} dosya tarandi, ${ihlal} ihlal bulundu`);
process.exit(ihlal === 0 ? 0 : 1);
```

Denetimin gerçekten çalıştığını görmek için kuralın kırılması gerekir. Aşağıdaki betik önce
uygulamayı çalıştırır, sonra denetimi geçirir, ardından alan katmanına veri katmanından bir
içe aktarım ekleyip denetimi yeniden çalıştırır ve ağacı eski hâline döndürür. Betik yukarıda
gösterilen dört kaynak dosyası ile `denetim/bagimlilik.mjs` dosyasının bulunduğu dizinde
çalıştırılır.

```bash
#!/usr/bin/env bash
# Iskeleti calistirir, denetimi gecirir, sonra kurali bilerek kirip denetimin yakalamasini gosterir.
echo "--- uygulama ---"
node kaynak/kurulum/uygulama.mjs

echo "--- denetim: kurala uygun agac ---"
node denetim/bagimlilik.mjs; echo "cikis kodu=$?"

echo "--- alan katmanina veri katmani ice aktarimi eklenir ---"
cp kaynak/alan/odunc.mjs odunc.yedek
printf '%s\n' 'import { kitapBul } from "../veri/kitap-deposu.mjs";' > odunc.ihlalli
cat odunc.yedek >> odunc.ihlalli
cp odunc.ihlalli kaynak/alan/odunc.mjs

echo "--- denetim: kural kirilmis agac ---"
node denetim/bagimlilik.mjs; echo "cikis kodu=$?"

cp odunc.yedek kaynak/alan/odunc.mjs           # agac eski haline dondurulur
rm -f odunc.yedek odunc.ihlalli
```

```
--- uygulama ---
{"durum":201,"govde":{"isbn":"978-0262033848","uye":"U-4711","sonTarih":"2026-03-15T00:00:00.000Z"}}
{"durum":409,"govde":{"hata":"raf_bos"}}
{"durum":409,"govde":{"hata":"kitap_yok"}}
--- denetim: kurala uygun agac ---
4 dosya tarandi, bagimlilik yonu kurala uygun
cikis kodu=0
--- alan katmanina veri katmani ice aktarimi eklenir ---
--- denetim: kural kirilmis agac ---
IHLAL  kaynak/alan/odunc.mjs: alan -> veri
4 dosya tarandi, 1 ihlal bulundu
cikis kodu=1
```

Uygulamanın çıktısı katmanların iş bölümünü de doğrular: rafta tek kopyası olan kitap birinci
üyeye verilmiş (`201`), aynı kitabı isteyen ikinci üye `raf_bos` yanıtı almış, katalogda
bulunmayan istek `kitap_yok` ile karşılanmıştır. Üç kararın üçü de alan katmanında
hesaplanmış, durum koduna çevrilmeleri taşıma katmanında yapılmıştır.

Denetimin iki çalıştırması arasındaki tek fark tek bir içe aktarım satırıdır. Kural kırıldığı
anda ihlalin hangi dosyada ve hangi yönde olduğu adıyla bildirilmiş, çıkış kodu `0`'dan `1`'e
dönmüştür. Bir yapı adımı bu kodu gördüğünde durur; kural artık bir öneri değil, bir
kısıttır.

#### Özet

- Katman odaklı düzen üst dizinleri teknik ilgiye, özellik odaklı düzen iş alanına göre açar;
  ölçümde ödünç özelliğine dokunan bir değişiklik birinde üç, diğerinde tek dizine dokunmuş,
  veri erişimine dokunan değişiklikte oran tersine dönmüştür.
- Katman odaklı düzende dört sorumluluk ayrılır: alan iş kuralını taşır ve hiçbir katmanı
  çağırmaz, veri kalıcılığı üstlenir, taşıma protokolü alan diline çevirir, birleştirme
  parçaları tek yerde bağlar.
- Bağımlılık yönü tek yönlüdür: dış katman içi çağırır, iç katman dışı çağıramaz. Alan
  katmanının ihtiyaç duyduğu veri ona parametre olarak verilir.
- Kural bir denetim programına bağlandığında ihlal, dosya adı ve yönüyle bildirilir ve çıkış
  kodu `1` olur; bu kod bir yapı adımını durdurabildiği için kural kısıt hâline gelir.
- Bellekteki depo yalnız tek süreçli bir uygulamada geçerlidir; kalıcı depoya geçiş yalnız
  veri dizinini değiştirir ve alan katmanına dokunmaz.

#### Sonraki Adım

İskelet ayakta ama bütün değerleri kodun içinde yazılı: bağlantı noktası, ödünç günü, üye
ödünç sınırı, deponun yeri. Aynı uygulamanın yerelde, sınama ortamında ve yayında farklı
değerlerle çalışması gerekir ve bu değerler kodun içinden okunamaz. Sonraki ders
yapılandırmayı ortamdan okumayı ele alır: hangi değer değişken, hangisi sabit; eksik ya da
yanlış tipte bir değişkenle uygulama ne yapmalı? Ölçüm, eksik bir değişkenle başlatılan
uygulamanın açılışta hangi iletiyle ve hangi çıkış koduyla düştüğünü gösterecek.

### Yapılandırma Yönetimi

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/yapilandirma-yonetimi

Ortam değişkenlerinden okunan yapılandırma bir şemayla tanımlanır; tip, aralık ve seçenek doğrulaması açılışta yapılır, bütün sorunlar tek seferde bildirilir ve geçersiz ortamda uygulamanın sysexits çıkış koduyla düşmesi ölçülür.

Önceki ders iskeletin katmanlarını ayırdı, ama bütün değerler kodun içinde yazılı kaldı:
bağlantı noktası, ödünç günü, üye ödünç sınırı, deponun yeri. Aynı uygulama yerelde, sınama
ortamında ve yayında farklı değerlerle çalışmalıdır; bu değerler kodda durduğu sürece her
ortam için ayrı bir yapı üretmek gerekir.

Bu ders değerleri koddan çıkarıp **ortam değişkenlerine** taşır. Asıl konu değerlerin nereden
okunduğu değil, ne zaman ve nasıl doğrulandığıdır: eksik ya da yanlış tipte bir değişkenle
başlatılan uygulama ne yapmalıdır?

#### Neyin Yapılandırma Olduğu

Kodda duran her sabit yapılandırma değildir. Ayrımı veren ölçüt tektir: **ortamdan ortama
değişen değer yapılandırmadır.** Ödünç süresinin on dört gün olması kütüphanenin kuralıdır ve
yayında da yerelde de aynıdır; bağlantı noktası ise makineden makineye değişir.

Bu ölçüt uygulandığında üç öbek oluşur:

- **Ortama bağlı değerler.** Bağlantı noktası, deponun adresi, kütük düzeyi, bakım kipi.
  Bunlar ortam değişkenidir.
- **İş kuralı sabitleri.** Ödünç süresi, üye ödünç sınırı. Bunlar alan katmanına aittir ve
  koddadır — ama ortamlar arasında ayarlanabilmeleri isteniyorsa yapılandırmaya geçerler.
- **Gizli değerler.** Veritabanı parolası, imzalama anahtarı. Bunlar da ortamdan gelir, ama
  ayrı bir sınıfı vardır ve sonraki dersin konusudur.

Aşağıdaki şema ilk iki öbeği bir arada tanımlar; ödünç süresi ve üye sınırı, ortamlar arası
denemeye açık olsunlar diye yapılandırmaya alınmıştır ve varsayılanları iş kuralının kendisidir.

#### Şema, Varsayılan ve Doğrulama

Bir yapılandırma okuyucusunun üç işi vardır: değeri okumak, tipini doğrulamak, eksikse
varsayılanı koymak. Aşağıdaki modül bunları bir şema üzerinden yapar. Ortam değişkenleri tek
bir önek taşır; bu önek, uygulamaya ait olmayan değişkenlerin şemaya karışmasını önler.

```js
// kaynak/kurulum/yapilandirma.mjs — ortamdan okur, tumunu dogrular, degistirilemez nesne dondurur
const ONEK = "KUTUPHANE_";

export const SEMA = {
  BAGLANTI_NOKTASI: { tip: "sayi", varsayilan: 8435, alt: 1024, ust: 65535 },
  ODUNC_GUN: { tip: "sayi", varsayilan: 14, alt: 1, ust: 90 },
  UYE_ODUNC_SINIRI: { tip: "sayi", varsayilan: 5, alt: 1, ust: 50 },
  DEPO_ADRESI: { tip: "metin", zorunlu: true },
  KUTUK_DUZEYI: { tip: "secenek", secenekler: ["hata", "uyari", "bilgi", "ayrintili"],
    varsayilan: "bilgi" },
  BAKIM_KIPI: { tip: "mantiksal", varsayilan: false },
};

const cevir = (ad, kural, ham) => {
  if (kural.tip === "sayi") {
    const sayi = Number(ham);
    if (!Number.isInteger(sayi)) return { hata: `tam sayi bekleniyor, "${ham}" verildi` };
    if (sayi < kural.alt || sayi > kural.ust) {
      return { hata: `${kural.alt}-${kural.ust} araliginda olmali, ${sayi} verildi` };
    }
    return { deger: sayi };
  }
  if (kural.tip === "mantiksal") {
    if (ham !== "true" && ham !== "false") {
      return { hata: `"true" ya da "false" bekleniyor, "${ham}" verildi` };
    }
    return { deger: ham === "true" };
  }
  if (kural.tip === "secenek") {
    if (!kural.secenekler.includes(ham)) {
      return { hata: `secenekler [${kural.secenekler.join(", ")}], "${ham}" verildi` };
    }
    return { deger: ham };
  }
  return ham.length > 0 ? { deger: ham } : { hata: "bos olamaz" };
};

export const yapilandirmayiOku = (ortam) => {
  const sorunlar = [];
  const ayarlar = {};

  for (const [ad, kural] of Object.entries(SEMA)) {
    const anahtar = ONEK + ad;
    const ham = ortam[anahtar];
    if (ham === undefined) {
      if (kural.zorunlu) sorunlar.push(`${anahtar}: zorunlu, tanimli degil`);
      else ayarlar[ad] = kural.varsayilan;
      continue;
    }
    const sonuc = cevir(ad, kural, ham);
    if (sonuc.hata) sorunlar.push(`${anahtar}: ${sonuc.hata}`);
    else ayarlar[ad] = sonuc.deger;
  }

  for (const anahtar of Object.keys(ortam)) {          // yazim hatalari sessizce gecmesin
    if (anahtar.startsWith(ONEK) && !(anahtar.slice(ONEK.length) in SEMA)) {
      sorunlar.push(`${anahtar}: bilinmeyen anahtar`);
    }
  }

  if (sorunlar.length > 0) {
    const hata = new Error("yapilandirma gecersiz");
    hata.sorunlar = sorunlar.sort();
    throw hata;
  }
  return Object.freeze(ayarlar);
};
```

Üç tasarım kararı burada gizlidir ve üçü de ölçümde görünecektir.

**Bütün sorunlar toplanır.** Okuyucu ilk hatada durmaz; şemanın tamamını gezer, sorunları bir
listede biriktirir ve sonunda tek seferde bildirir. Sekiz değişkeni yanlış olan bir ortamı
sekiz kez başlatmak zorunda kalmamanın yolu budur.

**Bilinmeyen anahtar hatadır.** Önekli ama şemada karşılığı olmayan bir değişken, neredeyse
her zaman bir yazım hatasıdır. Sessizce yok sayıldığında uygulama açılır, varsayılanla çalışır
ve yanlış davranışın nedeni ortam dosyasında saatlerce aranır.

**Sonuç dondurulur.** `Object.freeze` çağrısı yapılandırmayı **değiştirilemez** hâle getirir.
Çalışma sırasında bir ayarı değiştiren kod, sürecin hangi anda hangi değerle çalıştığını
belirsizleştirir; dondurma bu yolu kapatır.

#### Açılışta Düşmek

Doğrulamanın değerini belirleyen, ne zaman yapıldığıdır. Yapılandırma isteği karşılarken
okunursa hata ilk istekte, belki de yayından saatler sonra ortaya çıkar. Açılışta okunursa
hatalı ortam hiç hizmet vermez.

Aşağıdaki açılış dosyası bunu uygular: yapılandırma sunucu kurulmadan **önce** okunur,
geçersizse sunucu hiç oluşturulmaz.

```js
// kaynak/kurulum/ac.mjs — acilis: yapilandirma once okunur; gecersizse sunucu hic kurulmaz
import { createServer } from "node:http";
import { yapilandirmayiOku } from "./yapilandirma.mjs";

const EX_CONFIG = 78;                       // sysexits.h: yapilandirma hatasi

let ayar;
try {
  ayar = yapilandirmayiOku(process.env);
} catch (hata) {
  console.error(hata.message + ":");
  for (const sorun of hata.sorunlar ?? []) console.error("  - " + sorun);
  process.exit(EX_CONFIG);
}

console.log("yapilandirma: " + JSON.stringify(ayar));

try {
  ayar.ODUNC_GUN = 30;                      // degistirilemez nesne: yazma denemesi
} catch (hata) {
  console.log("ayar degistirme denemesi: " + hata.constructor.name);
}

createServer((istek, yanit) => {
  yanit.sendDate = false;
  yanit.writeHead(200, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify({ oduncGun: ayar.ODUNC_GUN, bakim: ayar.BAKIM_KIPI }));
}).listen(ayar.BAGLANTI_NOKTASI, "127.0.0.1",
  () => console.log(`acildi 127.0.0.1:${ayar.BAGLANTI_NOKTASI}`));
```

Çıkış kodu rastgele seçilmemiştir. `sysexits.h` başlığında tanımlı `EX_CONFIG` değeri
`78`'dir ve "yapılandırma hatası" anlamına gelir. Uygulamayı gözeten bir denetçi bu kodu
gördüğünde süreci yeniden başlatmanın işe yaramayacağını bilir; aynı ortam aynı hatayı
üretecektir.

Ölçüm dört ortamla açılışı dener. `env -i` çağrısı ortamı boşaltır, böylece kabuktaki var
olan değişkenler sonuca karışmaz.

```bash
#!/usr/bin/env bash
# Dort ayri ortamla acilisi dener; her denemede iletiyi ve cikis kodunu basar.
dene() {  # $1 = aciklama, kalan argumanlar ANAHTAR=deger ciftleri
  aciklama="$1"; shift
  echo "=== $aciklama ==="
  env -i PATH="$PATH" "$@" node kaynak/kurulum/ac.mjs & pid=$!
  sleep 0.8
  if kill -0 "$pid" 2>/dev/null; then
    curl -sS "http://127.0.0.1:8435/"; echo
    kill "$pid"; echo "surec ayakta, sonlandirildi"
  else
    wait "$pid"; echo "cikis kodu=$?"
  fi
}

dene "1. zorunlu degisken eksik"
dene "2. tip ve secenek hatasi bir arada" \
  KUTUPHANE_DEPO_ADRESI=file:./veri/kutuphane.db \
  KUTUPHANE_BAGLANTI_NOKTASI=seksen \
  KUTUPHANE_KUTUK_DUZEYI=detayli \
  KUTUPHANE_ODUC_GUN=14
dene "3. aralik disi deger" \
  KUTUPHANE_DEPO_ADRESI=file:./veri/kutuphane.db \
  KUTUPHANE_BAGLANTI_NOKTASI=80
dene "4. gecerli ortam (varsayilanlar tamamliyor)" \
  KUTUPHANE_DEPO_ADRESI=file:./veri/kutuphane.db \
  KUTUPHANE_BAKIM_KIPI=true
```

```
=== 1. zorunlu degisken eksik ===
yapilandirma gecersiz:
  - KUTUPHANE_DEPO_ADRESI: zorunlu, tanimli degil
cikis kodu=78
=== 2. tip ve secenek hatasi bir arada ===
yapilandirma gecersiz:
  - KUTUPHANE_BAGLANTI_NOKTASI: tam sayi bekleniyor, "seksen" verildi
  - KUTUPHANE_KUTUK_DUZEYI: secenekler [hata, uyari, bilgi, ayrintili], "detayli" verildi
  - KUTUPHANE_ODUC_GUN: bilinmeyen anahtar
cikis kodu=78
=== 3. aralik disi deger ===
yapilandirma gecersiz:
  - KUTUPHANE_BAGLANTI_NOKTASI: 1024-65535 araliginda olmali, 80 verildi
cikis kodu=78
=== 4. gecerli ortam (varsayilanlar tamamliyor) ===
yapilandirma: {"BAGLANTI_NOKTASI":8435,"ODUNC_GUN":14,"UYE_ODUNC_SINIRI":5,"DEPO_ADRESI":"file:./veri/kutuphane.db","KUTUK_DUZEYI":"bilgi","BAKIM_KIPI":true}
ayar degistirme denemesi: TypeError
acildi 127.0.0.1:8435
{"oduncGun":14,"bakim":true}
surec ayakta, sonlandirildi
```

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

**Birinci deneme** zorunlu bir değişkenin eksikliğini gösterir. Uygulama açılmamış, bağlantı
noktasını dinlememiş ve `78` ile çıkmıştır. Eksik olan değerin ne olduğu adıyla yazılmıştır;
"bir şeyler ters gitti" biçiminde bir ileti, ortam dosyasını düzeltecek kişiye hiçbir şey
söylemez.

**İkinci deneme** üç ayrı sorunu bir arada bildirir. `BAGLANTI_NOKTASI` sayıya çevrilememiş,
`KUTUK_DUZEYI` seçenek listesinde bulunmamış ve `KUTUPHANE_ODUC_GUN` bilinmeyen anahtar
olarak işaretlenmiştir. Üçüncüsü, `ODUNC` yerine `ODUC` yazılmış bir değişkendir; şema bunu
yakalamasa uygulama açılır ve ödünç süresi sessizce varsayılanda kalırdı. İletide seçenek
listesinin de yazılmış olması, düzeltmeyi belgeye bakmadan yapılabilir kılar.

**Üçüncü deneme** tip doğru, değer yanlış durumunu ayırır. `80` geçerli bir tam sayıdır ama
ayrıcalıklı bağlantı noktası aralığındadır. Tip denetimi tek başına yetmez; anlamlı aralık da
şemanın parçasıdır.

**Dördüncü deneme** geçerli ortamı gösterir. Yalnız iki değişken verilmiştir; kalan dördü
varsayılanlarıyla tamamlanmış ve tam yapılandırma tek satırda yazdırılmıştır. Açılışta
yapılandırmanın kütüğe yazılması, bir sürecin hangi ayarlarla çalıştığını sonradan sormaya
gerek bırakmaz.

Aynı çıktıda `ayar degistirme denemesi: TypeError` satırı, dondurulmuş nesneye yazma
girişiminin sessizce yutulmadığını doğrular. Modül kodu katı kipte çalıştığı için atama bir
hata fırlatır; katı kip dışında aynı atama sessizce yok sayılırdı.

#### Öncelik Sırası ve Ortam Dosyası

Ortam değişkeni tek kaynak değildir. Node.js Çalışma Zamanı kursunun Yapılandırma Yönetimi
dersinde kurulan **katmanlı yapılandırma** düzeni burada da geçerlidir: bir değer birden çok
yerde tanımlanabilir ve hangisinin kazandığı önceden belirlenmiş olmalıdır. Yaygın sıra,
soldan sağa artan öncelikle şudur: şemadaki varsayılan, ortam dosyası, gerçek ortam
değişkeni, komut satırı seçeneği.

Bu okuyucu yalnız iki katman kullanır — varsayılan ve ortam değişkeni — ama sıra genişletmeye
açıktır: `yapilandirmayiOku` fonksiyonu ortamı parametre olarak aldığı için, çağıran taraf
katmanları birleştirip tek bir nesne verebilir. Fonksiyonun `process.env` değerini kendi
içinden okumaması bilinçli bir karardır; bu sayede sınamalarda gerçek ortam kirletilmeden
farklı ortamlar denenebilir.

Ortam dosyasının kendisi bir kolaylıktır ve sınırı vardır: yerel geliştirmede rahatlık
sağlar, yayında ise değerlerin nereden geldiğini gizler. Dosyanın sürüm denetimine
girmemesinin nedeni de budur ve bu, sonraki dersin sınırını çizer.

#### Özet

- Ortamdan ortama değişen değer yapılandırmadır; değişmeyen kural koda, gizli değerler ise
  ayrı bir sınıfa aittir.
- Şema her değişkenin tipini, aralığını, seçeneklerini ve varsayılanını tek yerde tanımlar;
  okuyucu ilk hatada durmaz, bütün sorunları toplayıp tek seferde bildirir.
- Önekli ama şemada bulunmayan bir anahtar hata sayılır; ölçümde `KUTUPHANE_ODUC_GUN` yazım
  hatası bu kuralla yakalanmıştır.
- Doğrulama açılışta yapılır: geçersiz ortamda sunucu hiç kurulmaz ve süreç `sysexits.h`
  başlığındaki `EX_CONFIG` değeriyle, yani `78` ile çıkar.
- Okunan yapılandırma dondurulur; katı kipte çalışan modül kodunda değiştirme girişimi
  `TypeError` üretir. Eksik bırakılan ayarlar varsayılanlarla tamamlanır ve açılışta kütüğe
  yazılır.

#### Sonraki Adım

Bu dersin şemasında bir değişken kasten yoktu: deponun parolası. Bağlantı noktası ile parola
aynı yerden, ortamdan okunur; buna karşılık biri kütüğe yazılabilir, diğeri yazılamaz. Sonraki
ders bu sınırı çizer: yapılandırma ile gizli değer arasındaki fark nedir, gizli bir değerin
kütüğe, hata yanıtına ve istemciye ulaşmasını hangi kurallar engeller ve maskelemenin
gerçekten uygulandığı nasıl denetlenir?

### Gizli Değerlerin Ayrılması

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/gizli-degerlerin-ayrilmasi

Yapılandırma ile gizli değer arasındaki sınır çıkış yollarına göre çizilir; anahtar adına ve değerin kendisine dayanan iki katlı maskeleme uygulanır, iç ayrıntının kütükte kalıp istemciye geçmemesi sağlanır ve bir sızıntı denetimi maskelenmemiş alanı çıkış koduyla yakalar.

Önceki dersin şemasında bir değişken kasten yoktu: deponun parolası. Bağlantı noktası ile
parola aynı yerden, ortamdan okunur ve aynı nesnede taşınır; buna karşılık biri açılışta
kütüğe yazılabilir, diğeri yazılamaz. Bu ders o sınırı çizer ve sınırın kod içinde nerede
uygulandığını gösterir.

Konu, gizli bir değeri saklamak değildir — değer zaten süreç belleğindedir ve uygulama onu
kullanmak zorundadır. Konu, o değerin sürecin **çıkış yollarına** ulaşmasını engellemektir:
kütük satırları, hata yanıtları, istemciye dönen gövdeler ve başlıklar.

#### Sınır Nerede

Yapılandırma ile gizli değeri ayıran ölçüt, değerin ne olduğu değil, **açığa çıktığında ne
olacağıdır.** Bağlantı noktasının kütükte görünmesi bir sorun değildir; parolanın görünmesi,
kütüğü okuyabilen herkesin depoya erişebilmesi anlamına gelir.

Bu ölçüt üç pratik kural üretir:

- **Gizli değer koda yazılmaz.** Sürüm denetimindeki bir dosyaya giren değer, o deponun
  bütün kopyalarına ve bütün geçmişine girmiştir. Değiştirilmesi gerektiğinde geçmişten
  silinemez.
- **Gizli değer kütüğe yazılmaz.** Kütükler biriktirilir, taşınır ve genelde uygulamanın
  kendisinden daha geniş bir kitleye açıktır.
- **Gizli değer istemciye ulaşmaz.** Hata yanıtı, ayar görünümü ve tanı çıktıları bunun en
  sık kırıldığı yerlerdir.

Kurallar niyet olarak yeterli değildir; koda bağlanmaları gerekir. Aşağıdaki iki modül bunu
yapar.

#### İki Katlı Maskeleme

**Maskeleme** (redaction), bir değeri çıktıya yazmadan önce sabit bir imle değiştirmektir.
Uygulanmasının iki yolu vardır ve ikisi de tek başına eksiktir.

Birinci yol **anahtar adına** bakar: alan adı parola, anahtar, jeton gibi bir sözcük
içeriyorsa değeri maskele. Bu yol bildirimseldir ve okunaklıdır, ama gizli değer beklenmedik
bir alana düştüğünde kaçırır — örneğin bir hata iletisinin içine gömülmüş bağlantı
adresinde.

İkinci yol **değerin kendisine** bakar: açılışta kaydedilmiş gizli değerlerin herhangi
birine rastlanırsa, hangi alanda olursa olsun, değiştir. Bu yol beklenmedik yolları kapatır,
ama yalnız açılışta kaydedilen değerleri tanır.

İkisi birlikte uygulanır.

```js
// kaynak/kurulum/gizli.mjs — gizli degerleri kaydeder; kutuk ve yanit yollarinda maskeler
export const MASKE = "***";

const GIZLI_DEGERLER = new Set();
const GIZLI_ANAHTAR = /parola|anahtar|jeton|gizli|authorization|cookie/i;

// Acilista her gizli degerin bir kez kaydedilmesi, deger tabanli maskelemenin kaynagidir.
export const gizliKaydet = (deger) => {
  if (typeof deger === "string" && deger.length >= 6) GIZLI_DEGERLER.add(deger);
};

export const maskele = (deger, anahtar = "") => {
  if (typeof deger === "string") {
    if (GIZLI_ANAHTAR.test(anahtar)) return MASKE;              // 1. anahtar adina gore
    let sonuc = deger;
    for (const gizli of GIZLI_DEGERLER) sonuc = sonuc.split(gizli).join(MASKE);
    return sonuc;                                               // 2. degerin kendisine gore
  }
  if (Array.isArray(deger)) return deger.map((e) => maskele(e));
  if (deger instanceof Error) {
    return { ad: deger.name, ileti: maskele(deger.message), kod: deger.kod ?? null };
  }
  if (deger && typeof deger === "object") {
    return Object.fromEntries(Object.entries(deger).map(([a, d]) => [a, maskele(d, a)]));
  }
  return deger;
};
```

Hata nesnesinin ayrı ele alınması gerekir, çünkü `JSON.stringify` bir `Error` nesnesini boş
nesneye çevirir; ileti ve yığıt izi kaybolur. Buradaki dönüşüm iletiyi korur ama maskeden
geçirir.

Yapılandırma okuyucusu, gizli alanları okurken aynı zamanda kaydeder. Bu tek satır, değer
tabanlı maskelemenin dayandığı kayıttır.

```js
// kaynak/kurulum/ayar.mjs — ortamdan okur; gizli alanlari isaretler ve gizli kutugune kaydeder
import { gizliKaydet } from "./gizli.mjs";

const SEMA = {
  BAGLANTI_NOKTASI: { varsayilan: "8436" },
  DEPO_ADRESI: { varsayilan: "depo://kutuphane@10.0.0.7:5432/odunc" },
  DEPO_PAROLASI: { zorunlu: true, gizli: true },
  IMZALAMA_ANAHTARI: { zorunlu: true, gizli: true },
};

export const ayariOku = (ortam) => {
  const ayar = {}, eksik = [];
  for (const [ad, kural] of Object.entries(SEMA)) {
    const deger = ortam["KUTUPHANE_" + ad] ?? kural.varsayilan;
    if (deger === undefined) { eksik.push("KUTUPHANE_" + ad); continue; }
    if (kural.gizli) gizliKaydet(deger);
    ayar[ad] = deger;
  }
  if (eksik.length > 0) {
    console.error("yapilandirma gecersiz: " + eksik.join(", "));
    process.exit(78);
  }
  return Object.freeze(ayar);
};
```

Şemadaki `gizli` bayrağı, önceki dersin şemasına eklenen tek alandır. Ayrım şemada tanımlanır,
uygulanması ise tek bir yerde toplanır.

#### Çıkış Yollarını Kapatmak

Sunucunun kuralı şudur: **kütüğe ve yanıta giden her nesne maskeden geçer.** Bu, iki
fonksiyonun tek noktada tanımlanmasıyla sağlanır; başka hiçbir yerde doğrudan yazma yapılmaz.

```js
// kaynak/http/sunucu.mjs — her cikis yolu maskeden gecer: kutuk, hata yaniti ve ayar gorunumu
import { createServer } from "node:http";
import { ayariOku } from "../kurulum/ayar.mjs";
import { maskele } from "../kurulum/gizli.mjs";

const ayar = ayariOku(process.env);
let sayac = 0;

const kutukle = (kayit) => console.log(JSON.stringify(maskele(kayit)));

const baglan = () => {                       // depoya baglanma denemesi: hata iletisi adresi tasir
  const hata = new Error(
    `depoya baglanilamadi: ${ayar.DEPO_ADRESI} (parola: ${ayar.DEPO_PAROLASI})`);
  hata.kod = "depo_erisilemez";
  throw hata;
};

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const kimlik = `i-${++sayac}`;
  const yol = new URL(istek.url, "http://yerel").pathname;
  const yaz = (durum, govde) => {
    const metin = JSON.stringify(maskele(govde));
    yanit.writeHead(durum, { "content-type": "application/json; charset=utf-8",
      "content-length": Buffer.byteLength(metin), "x-istek-kimligi": kimlik });
    yanit.end(metin);
  };

  try {
    if (yol === "/saglik") return yaz(200, { durum: "ayakta" });
    if (yol === "/ayarlar") return yaz(200, { ayar });          // gizli alanlar maskelenir
    if (yol === "/odunc") { baglan(); return; }
    yaz(404, { hata: "yol_yok", istekKimligi: kimlik });
  } catch (hata) {
    kutukle({ duzey: "hata", istekKimligi: kimlik, yol, hata });  // ic ayrinti yalniz kutukte
    yaz(500, { hata: "ic_hata", istekKimligi: kimlik });          // istemciye yalniz kod
  }
}).listen(Number(ayar.BAGLANTI_NOKTASI), "127.0.0.1",
  () => kutukle({ duzey: "bilgi", olay: "acildi", ayar }));
```

`baglan` fonksiyonu gerçek bir kazayı taklit eder: depoya bağlanan katmanların ürettiği hata
iletileri sık sık bağlantı adresini, bazen de kimlik bilgisini taşır. Bu ileti maskeden
geçmeden kütüğe yazılırsa gizli değer kütükte kalır.

Aynı `catch` bloğu ikinci bir ayrım daha kurar: iç ayrıntı kütüğe, istemciye yalnız bir hata
kodu ve istek kimliği gider. Kimliğin iki tarafta da bulunması, destek isteyen bir üyenin
verdiği numarayla kütükteki satırın bulunmasını sağlar; ayrıntının kendisi yanıta hiç geçmez.

#### Sızıntının Denetimi

Yukarıdaki kuralların uygulandığı, okunarak değil çalıştırılarak doğrulanır. Aşağıdaki
program sunucuyu bilinen gizli değerlerle açar, uçlara istek yapar, yanıt gövdelerini,
yanıt başlıklarını ve sürecin yazdığı bütün kütüğü toplar, sonra bu metinlerde gizli
değerleri arar.

```js
// denetim/sizinti.mjs — verilen sunucuyu bilinen gizli degerlerle acar; bu degerlerin yanit
// govdesine, yanit basliklarina ya da kutuge dusup dusmedigini sinar. Bulgu varsa 1 ile cikar.
// Kullanim: node denetim/sizinti.mjs <betik> <baglanti-noktasi>
import { spawn } from "node:child_process";
import { setTimeout as bekle } from "node:timers/promises";

const GIZLILER = {
  KUTUPHANE_DEPO_PAROLASI: "raf-parolasi-9f3a2b4c",
  KUTUPHANE_IMZALAMA_ANAHTARI: "imza-anahtari-7c1d8e5a",
};
const UCLAR = ["/saglik", "/ayarlar", "/odunc"];

const [betik, baglantiNoktasi] = process.argv.slice(2);

const cocuk = spawn("node", [betik], {
  env: { ...process.env, ...GIZLILER, KUTUPHANE_BAGLANTI_NOKTASI: baglantiNoktasi },
});
let kutuk = "";
cocuk.stdout.on("data", (p) => (kutuk += p));
cocuk.stderr.on("data", (p) => (kutuk += p));
await bekle(800);

const bulgular = [];
console.log(`### ${betik} — istemciye donen yanitlar`);
for (const yol of UCLAR) {
  const yanit = await fetch(`http://127.0.0.1:${baglantiNoktasi}${yol}`);
  const govde = await yanit.text();
  const basliklar = [...yanit.headers].map(([a, d]) => `${a}: ${d}`).join("\n");
  console.log(`  ${yanit.status} ${yol}  ${govde}`);
  for (const [ad, deger] of Object.entries(GIZLILER)) {
    if (govde.includes(deger)) bulgular.push(`${yol} yanit govdesi <- ${ad}`);
    if (basliklar.includes(deger)) bulgular.push(`${yol} yanit basliklari <- ${ad}`);
  }
}

await bekle(200);
cocuk.kill();
await bekle(300);
for (const [ad, deger] of Object.entries(GIZLILER)) {
  if (kutuk.includes(deger)) bulgular.push(`kutuk <- ${ad}`);
}

console.log("--- kutuk ---");
console.log(kutuk.trimEnd().split("\n").map((s) => "  " + s).join("\n"));
console.log("--- denetim ---");
for (const bulgu of bulgular) console.log(`  SIZINTI  ${bulgu}`);
console.log(bulgular.length === 0
  ? "  gizli deger hicbir cikis yolunda bulunmadi"
  : `  ${bulgular.length} sizinti bulundu`);
process.exit(bulgular.length === 0 ? 0 : 1);
```

Hiçbir sızıntı bulamayan bir denetim, iki nedenden dolayı sessiz kalabilir: gerçekten sızıntı
yoktur ya da denetim bozuktur. İkisini ayırmak için denetimin bilerek kusurlu bırakılmış bir
örnek üzerinde de çalıştırılması gerekir. Aşağıdaki dosya bu amaçla `denetim/` dizininde
durur — kaynak ağacının dışındadır ve uygulamanın hiçbir yolundan çağrılmaz.

```js
// denetim/kusurlu-ornek.mjs — DENETIM ORNEGI, uretim kodu degildir.
// Maskeleme uygulanmamis bir surumu taklit eder; sizinti denetiminin gercekten
// yakaladigini kanitlamak icin durur ve kaynak agacinin disindadir.
import { createServer } from "node:http";
import { ayariOku } from "../kaynak/kurulum/ayar.mjs";

const ayar = ayariOku(process.env);

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const yol = new URL(istek.url, "http://yerel").pathname;
  const govde = yol === "/ayarlar" ? { ayar } : { durum: "ayakta" };
  const metin = JSON.stringify(govde);
  yanit.writeHead(200, { "content-type": "application/json; charset=utf-8",
    "content-length": Buffer.byteLength(metin) });
  yanit.end(metin);
}).listen(Number(ayar.BAGLANTI_NOKTASI), "127.0.0.1",
  () => console.log(JSON.stringify({ olay: "acildi", ayar })));
```

```bash
#!/usr/bin/env bash
# Sizinti denetimini once gercek sunucuya, sonra bilerek kusurlu birakilmis ornege uygular.
node denetim/sizinti.mjs kaynak/http/sunucu.mjs 8436; echo "cikis kodu=$?"
echo
node denetim/sizinti.mjs denetim/kusurlu-ornek.mjs 8437; echo "cikis kodu=$?"
```

```
### kaynak/http/sunucu.mjs — istemciye donen yanitlar
  200 /saglik  {"durum":"ayakta"}
  200 /ayarlar  {"ayar":{"BAGLANTI_NOKTASI":"8436","DEPO_ADRESI":"depo://kutuphane@10.0.0.7:5432/odunc","DEPO_PAROLASI":"***","IMZALAMA_ANAHTARI":"***"}}
  500 /odunc  {"hata":"ic_hata","istekKimligi":"i-3"}
--- kutuk ---
  {"duzey":"bilgi","olay":"acildi","ayar":{"BAGLANTI_NOKTASI":"8436","DEPO_ADRESI":"depo://kutuphane@10.0.0.7:5432/odunc","DEPO_PAROLASI":"***","IMZALAMA_ANAHTARI":"***"}}
  {"duzey":"hata","istekKimligi":"i-3","yol":"/odunc","hata":{"ad":"Error","ileti":"depoya baglanilamadi: depo://kutuphane@10.0.0.7:5432/odunc (parola: ***)","kod":"depo_erisilemez"}}
--- denetim ---
  gizli deger hicbir cikis yolunda bulunmadi
cikis kodu=0

### denetim/kusurlu-ornek.mjs — istemciye donen yanitlar
  200 /saglik  {"durum":"ayakta"}
  200 /ayarlar  {"ayar":{"BAGLANTI_NOKTASI":"8437","DEPO_ADRESI":"depo://kutuphane@10.0.0.7:5432/odunc","DEPO_PAROLASI":"raf-parolasi-9f3a2b4c","IMZALAMA_ANAHTARI":"imza-anahtari-7c1d8e5a"}}
  200 /odunc  {"durum":"ayakta"}
--- kutuk ---
  {"olay":"acildi","ayar":{"BAGLANTI_NOKTASI":"8437","DEPO_ADRESI":"depo://kutuphane@10.0.0.7:5432/odunc","DEPO_PAROLASI":"raf-parolasi-9f3a2b4c","IMZALAMA_ANAHTARI":"imza-anahtari-7c1d8e5a"}}
--- denetim ---
  SIZINTI  /ayarlar yanit govdesi <- KUTUPHANE_DEPO_PAROLASI
  SIZINTI  /ayarlar yanit govdesi <- KUTUPHANE_IMZALAMA_ANAHTARI
  SIZINTI  kutuk <- KUTUPHANE_DEPO_PAROLASI
  SIZINTI  kutuk <- KUTUPHANE_IMZALAMA_ANAHTARI
  4 sizinti bulundu
cikis kodu=1
```

Çıktının en öğretici satırı kütükteki hata kaydıdır. `parola: ***` yazan bu ileti, hiçbir
alanı "parola" adını taşımadığı hâlde maskelenmiştir. Gizli değer bir hata iletisinin ortasına
gömülüdür; onu yakalayan, açılışta kaydedilmiş değere göre çalışan ikinci kattır. Yalnız
anahtar adına bakan bir maskeleme bu satırı olduğu gibi yazardı.

`/ayarlar` yanıtı ikinci ayrımı gösterir: aynı nesnedeki `DEPO_ADRESI` görünür, iki gizli alan
`***` olmuştur. Ayar görünümünün var olması bir sorun değildir; hangi alanların göründüğünün
şemada tanımlanmış olması onu güvenli kılar.

`/odunc` yanıtı üçüncüsünü verir. İstemci `{"hata":"ic_hata","istekKimligi":"i-3"}` almıştır;
deponun adresi, bağlantının neden kurulamadığı ve hatanın iç kodu yanıtta yoktur. Aynı isteğin
kütük satırı bunların hepsini taşır ve iki kayıt `i-3` ile eşleşir.

Kusurlu örnek üzerindeki çalıştırma denetimin boş bir onay üretmediğini kanıtlar: dört ayrı
çıkış noktası adıyla bildirilmiş ve çıkış kodu `1` olmuştur. Bir yapı adımına bağlandığında
bu kod, maskelemesi unutulmuş bir alanın yayına çıkmasını durdurur.

#### Değişimin Maliyeti

Maskeleme tek başına yeterli değildir, çünkü açığa çıkmış bir gizli değerin tek çaresi
**değiştirilmesidir**. Uygulamanın bunu ucuz kılması gerekir: değer yalnız ortamdan okunuyorsa
değişim, ortamı güncelleyip süreçleri yeniden başlatmaktan ibarettir. Aynı değer koda
gömülüyse ya da birden çok yere kopyalanmışsa, değişim bir arama işine dönüşür.

Bunun ölçülebilir karşılığı şudur: gizli bir değerin uygulamada kaç yerde okunduğu sayılabilir
olmalıdır. Yukarıdaki iskelette bu sayı birdir — `ayariOku` fonksiyonu. Değeri kullanan her
kod parçası onu yapılandırma nesnesinden alır; hiçbiri `process.env` değerine kendi başına
bakmaz.

#### Özet

- Yapılandırma ile gizli değeri ayıran ölçüt, değerin açığa çıkması hâlinde doğacak sonuçtur;
  gizli değer koda yazılmaz, kütüğe yazılmaz ve istemciye ulaşmaz.
- Maskeleme iki katlıdır: anahtar adına bakan kat bildirimsel ama eksiktir, açılışta
  kaydedilmiş değere bakan kat beklenmedik alanları kapatır; ölçümde hata iletisine gömülü
  parolayı ikinci kat yakalamıştır.
- Hata nesneleri ayrı ele alınır, çünkü doğrudan dizileştirildiklerinde ileti kaybolur;
  dönüşüm iletiyi korur ve maskeden geçirir.
- İç ayrıntı kütükte kalır, istemciye yalnız hata kodu ve istek kimliği gider; aynı kimlik iki
  kayıtta bulunduğu için sonradan eşleştirme yapılabilir.
- Sızıntı denetimi sunucuyu bilinen gizli değerlerle açar, yanıtları ve kütüğü tarar; gerçek
  sunucuda `0`, bilerek kusurlu bırakılmış örnekte dört bulgu ve `1` üretmiştir.

#### Sonraki Adım

Bu dersin kütük satırları JSON biçimindeydi ve her satır bir istek kimliği taşıyordu, ama bu
kimlik yalnız iki yerde elle yazıldı. Bir istek zincirin beş halkasından ve üç modülden
geçtiğinde her satırın aynı kimliği taşıması nasıl sağlanır? Düzey eşiği ne işe yarar ve bir
isteğin bütün satırları sonradan hangi sorguyla toplanır? Sonraki ders yapılandırılmış
günlüklemeyi kurar: istek bağlamını zincir boyunca taşır ve tek bir isteğin bütün kayıtlarını
tek komutla toplar.

### Günlükleme Kurulumu

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/gunlukleme-kurulumu

Kütük satırları JSON nesnesi olarak üretilir; istek bağlamı eşzamansız yerel depoyla taşınır, derin katmanlar istek kimliğini parametre almadan kaydeder, düzey eşiği satır sayısını belirler ve tek bir isteğin bütün satırları sorguyla toplanır.

Önceki dersin kütük satırları JSON biçimindeydi ve her satır bir istek kimliği taşıyordu, ama
o kimlik yalnız iki yerde elle yazıldı. Gerçek bir uygulamada aynı istek zincirin halkalarından,
işleyiciden, alan katmanından ve veri katmanından geçer; her katman kendi kaydını üretir.
Kimliğin her satırda bulunması için her fonksiyonun onu parametre olarak alması gerekir mi?

Bu ders üç şeyi kurar: kütük satırının biçimi, istek bağlamının katmanlar arasında taşınması
ve satırların sonradan sorgulanması. Ölçüm, eşzamanlı üç isteğin kayıtlarının birbirine
karışmadığını gösterecek.

#### Satır Neden Nesnedir

Serbest metin bir kütük satırı insan tarafından okunur, makine tarafından okunmaz. "Ödünç
isteği reddedildi, ISBN 978-0262033848" satırından ISBN'i çıkarmak için bir kalıp yazmak
gerekir ve kalıp, ileti her değiştiğinde bozulur.

**Yapılandırılmış kayıt** (structured log record) bu sorunu adlandırılmış alanlarla çözer.
Her satır bağımsız bir JSON nesnesidir; satırlar arasında ayraç yoktur, dosya satır satır
işlenir. Bir kaydın taşıması gereken alanlar dört öbekte toplanır:

- **Ne zaman**: ISO 8601 biçiminde bir zaman damgası. Sabit uzunlukta, sıralanabilir ve saat
  dilimi taşır.
- **Ne kadar önemli**: düzey. Kaydın eşiğe göre yazılıp yazılmayacağını belirler.
- **Ne oldu**: olay adı. Serbest cümle değil, sabit bir tanımlayıcı — çünkü sorgulanacaktır.
- **Hangi bağlamda**: istek kimliği ve olaya özgü alanlar.

#### Bağlamı Taşımak

İstek kimliğini her fonksiyona parametre olarak vermek işe yarar ama bedeli yüksektir: kaydın
kendisiyle ilgisi olmayan fonksiyonların imzaları değişir ve zincirin ortasındaki bir
fonksiyon parametreyi iletmeyi unuttuğunda bağ kopar.

Çalışma zamanı bunun için **eşzamansız yerel depo** (async local storage) sağlar. Bir değer
belirli bir çağrı ağacına bağlanır; o ağacın içinden yapılan her okuma — kaç `await` sonrası
olursa olsun — aynı değeri görür. Eşzamanlı isteklerin ağaçları ayrı olduğu için değerler
karışmaz.

```js
// kaynak/kurulum/kutuk.mjs — JSON satirlari uretir; istek baglamini es zamansiz depodan okur
import { AsyncLocalStorage } from "node:async_hooks";

const DUZEYLER = { hata: 0, uyari: 1, bilgi: 2, ayrintili: 3 };

export const istekDeposu = new AsyncLocalStorage();

export const kutukKur = (esikAdi) => {
  const esik = DUZEYLER[esikAdi] ?? DUZEYLER.bilgi;
  const yaz = (duzey, olay, alanlar = {}) => {
    if (DUZEYLER[duzey] > esik) return;                  // duzey esigi: altindakiler yazilmaz
    const baglam = istekDeposu.getStore() ?? {};         // istek kimligi elden gecirilmez
    console.log(JSON.stringify({
      an: new Date().toISOString(), duzey, olay, ...baglam, ...alanlar,
    }));
  };
  return Object.fromEntries(
    Object.keys(DUZEYLER).map((d) => [d, (olay, alanlar) => yaz(d, olay, alanlar)]));
};
```

Düzeyler sayısal karşılıklarıyla tanımlanmıştır; karşılaştırma bu sayılar üzerinden yapılır.
Eşik `bilgi` olduğunda `ayrintili` kayıtlar hiç yazılmaz — dosyaya yazılıp sonra süzülmezler,
üretilmezler bile. Bu ayrım maliyettedir: eşiğin altındaki bir kayıt için dizileştirme
yapılmaz.

Sunucu her isteği kendi bağlamında çalıştırır. Veri katmanındaki `rafKontrol` fonksiyonu
istek kimliğini almaz, bilmez ve iletmez; buna rağmen ürettiği satırlar kimliği taşır.

```js
// kaynak/http/sunucu.mjs — her istek kendi baglaminda calisir; derin katmanlar kimligi tasimaz
import { createServer } from "node:http";
import { setTimeout as bekle } from "node:timers/promises";
import { kutukKur, istekDeposu } from "../kurulum/kutuk.mjs";

const kutuk = kutukKur(process.env.KUTUPHANE_KUTUK_DUZEYI ?? "bilgi");
const RAFTAKI = new Set(["978-0262033848"]);
let sayac = 0;

// Veri katmanindaki derin fonksiyon: istek kimligini parametre olarak ALMAZ.
const rafKontrol = async (isbn) => {
  kutuk.ayrintili("raf_sorgusu", { isbn });
  await bekle(150);                                   // depo erisimini taklit eden gecikme
  const bulundu = RAFTAKI.has(isbn);
  kutuk.bilgi("raf_sonucu", { isbn, bulundu });
  return bulundu;
};

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const baglam = { istekKimligi: `i-${++sayac}` };
  istekDeposu.run(baglam, async () => {               // baglam bu istegin butun zincirinde
    const baslangic = process.hrtime.bigint();
    const adres = new URL(istek.url, "http://yerel");
    kutuk.bilgi("istek_alindi", { yontem: istek.method, yol: adres.pathname });

    let durum = 404, govde = { hata: "yol_yok" };
    if (adres.pathname === "/odunc") {
      const bulundu = await rafKontrol(adres.searchParams.get("isbn") ?? "");
      durum = bulundu ? 201 : 409;
      govde = bulundu ? { odunc: "verildi" } : { hata: "raf_bos" };
      if (!bulundu) kutuk.uyari("odunc_reddedildi", { neden: "raf_bos" });
    } else if (adres.pathname === "/kitaplar") {
      durum = 200;
      govde = { kitapSayisi: RAFTAKI.size };
    }

    const metin = JSON.stringify({ ...govde, istekKimligi: baglam.istekKimligi });
    yanit.writeHead(durum, { "content-type": "application/json; charset=utf-8",
      "content-length": Buffer.byteLength(metin), "x-istek-kimligi": baglam.istekKimligi });
    yanit.end(metin);

    const sure = Number(process.hrtime.bigint() - baslangic) / 1e6;
    kutuk.bilgi("istek_bitti", { durum, sureMs: Number(sure.toFixed(1)) });
  });
}).listen(8438, "127.0.0.1", () => kutuk.bilgi("sunucu_acildi", { baglantiNoktasi: 8438 }));
```

İstek kimliği yanıtta da bulunur: hem `x-istek-kimligi` başlığında hem gövdede. Bir üye
destek istediğinde verdiği numara doğrudan kütüğe götürür.

#### Ölçüm

Ölçüm sunucuyu iki farklı eşikle çalıştırır. Her turda üç istek eşzamanlıya yakın gönderilir:
rafta bulunan bir kitap için ödünç, hızlı bir katalog isteği ve rafta bulunmayan bir kitap için
ödünç. Süzme işi `jq` ile yapılır; bu, JSON akışı üzerinde çalışan bir süzgeçtir.

```bash
#!/usr/bin/env bash
# Sunucuyu iki duzey esigiyle acar; es zamanli uc istegin kutuk satirlarini toplar ve sorgular.
tur() {  # $1 = kutuk duzeyi, $2 = cikti dosyasi
  KUTUPHANE_KUTUK_DUZEYI="$1" node kaynak/http/sunucu.mjs > "$2" 2>&1 & s=$!
  sleep 0.7
  curl -sS -o /dev/null 'http://127.0.0.1:8438/odunc?isbn=978-0262033848' & p1=$!
  sleep 0.05
  curl -sS -o /dev/null 'http://127.0.0.1:8438/kitaplar' & p2=$!
  sleep 0.05
  curl -sS -o /dev/null 'http://127.0.0.1:8438/odunc?isbn=978-0000000000' & p3=$!
  wait "$p1" "$p2" "$p3"
  sleep 0.2
  kill "$s"; wait "$s" 2>/dev/null
}

tur bilgi kutuk-bilgi.jsonl
tur ayrintili kutuk-ayrintili.jsonl

echo "--- kutuk (esik=bilgi), zaman sirasinda ---"
cat kutuk-bilgi.jsonl

echo "--- tek bir istegin butun satirlari: i-1 ---"
jq -c 'select(.istekKimligi == "i-1")' kutuk-bilgi.jsonl

echo "--- duzeye gore satir sayisi ---"
for dosya in kutuk-bilgi.jsonl kutuk-ayrintili.jsonl; do
  printf '%-24s %s\n' "$dosya" \
    "$(jq -r .duzey "$dosya" | sort | uniq -c | tr '\n' ' ' | tr -s ' ')"
done

rm -f kutuk-bilgi.jsonl kutuk-ayrintili.jsonl
```

```
--- kutuk (esik=bilgi), zaman sirasinda ---
{"an":"2026-07-28T20:26:21.198Z","duzey":"bilgi","olay":"sunucu_acildi","baglantiNoktasi":8438}
{"an":"2026-07-28T20:26:21.897Z","duzey":"bilgi","olay":"istek_alindi","istekKimligi":"i-1","yontem":"GET","yol":"/odunc"}
{"an":"2026-07-28T20:26:21.954Z","duzey":"bilgi","olay":"istek_alindi","istekKimligi":"i-2","yontem":"GET","yol":"/kitaplar"}
{"an":"2026-07-28T20:26:21.955Z","duzey":"bilgi","olay":"istek_bitti","istekKimligi":"i-2","durum":200,"sureMs":1}
{"an":"2026-07-28T20:26:22.016Z","duzey":"bilgi","olay":"istek_alindi","istekKimligi":"i-3","yontem":"GET","yol":"/odunc"}
{"an":"2026-07-28T20:26:22.049Z","duzey":"bilgi","olay":"raf_sonucu","istekKimligi":"i-1","isbn":"978-0262033848","bulundu":true}
{"an":"2026-07-28T20:26:22.050Z","duzey":"bilgi","olay":"istek_bitti","istekKimligi":"i-1","durum":201,"sureMs":153.5}
{"an":"2026-07-28T20:26:22.166Z","duzey":"bilgi","olay":"raf_sonucu","istekKimligi":"i-3","isbn":"978-0000000000","bulundu":false}
{"an":"2026-07-28T20:26:22.167Z","duzey":"uyari","olay":"odunc_reddedildi","istekKimligi":"i-3","neden":"raf_bos"}
{"an":"2026-07-28T20:26:22.168Z","duzey":"bilgi","olay":"istek_bitti","istekKimligi":"i-3","durum":409,"sureMs":152.3}
--- tek bir istegin butun satirlari: i-1 ---
{"an":"2026-07-28T20:26:21.897Z","duzey":"bilgi","olay":"istek_alindi","istekKimligi":"i-1","yontem":"GET","yol":"/odunc"}
{"an":"2026-07-28T20:26:22.049Z","duzey":"bilgi","olay":"raf_sonucu","istekKimligi":"i-1","isbn":"978-0262033848","bulundu":true}
{"an":"2026-07-28T20:26:22.050Z","duzey":"bilgi","olay":"istek_bitti","istekKimligi":"i-1","durum":201,"sureMs":153.5}
--- duzeye gore satir sayisi ---
kutuk-bilgi.jsonl         9 bilgi 1 uyari
kutuk-ayrintili.jsonl     2 ayrintili 9 bilgi 1 uyari
```

Zaman damgaları ve süre alanları her çalıştırmada değişir; satırların birbirine göre sırası
da zamanlamaya bağlı olarak kayabilir.

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

**Satırlar iç içe geçmiştir.** İlk isteğin `istek_alindi` satırı ile `raf_sonucu` satırı
arasında ikinci ve üçüncü isteğe ait dört satır vardır. Zaman sırasında okunan bir kütükte
tek bir isteğin akışı görünmez; bu, tek süreçte eşzamanlı çalışan bir uygulamanın kaçınılmaz
sonucudur.

**Kimlikler karışmamıştır.** `rafKontrol` fonksiyonu üç istekten ikisi tarafından çağrılmış,
her çağrı yüz elli milisaniye beklemiş ve bu süre boyunca başka isteklerin kodu çalışmıştır.
Buna rağmen `raf_sonucu` satırlarının biri `i-1`, diğeri `i-3` kimliğini taşır. Fonksiyon bu
kimliği hiçbir parametreden almamıştır.

**Sorgu akışı geri kurar.** `jq` süzgeci tek bir kimliğe ait satırları seçtiğinde ortaya
isteğin kendi öyküsü çıkar: alındı, raf sorgulandı, bitti. Yapılandırılmış kaydın asıl kazancı
budur — kütük zaman sırasında yazılır, ihtiyaca göre yeniden düzenlenir.

**Süre ölçümü kaydın parçasıdır.** `istek_bitti` satırındaki `sureMs` alanı, ikinci isteğin
bir milisaniyede, ödünç isteklerinin yüz elli milisaniyenin üzerinde tamamlandığını gösterir.
Aynı alan bütün satırlarda aynı adı taşıdığı için yavaş isteklerin sorgulanması tek bir
süzgeç ifadesidir.

**Eşik satır sayısını belirler.** Eşik `bilgi` iken dosyada on satır vardır; `ayrintili`
olduğunda `raf_sorgusu` kayıtları eklenir ve sayı on ikiye çıkar. Eşiğin yapılandırmadan
gelmesi, aynı sürümü yeniden kurmadan ayrıntı düzeyini değiştirmeyi sağlar.

#### Kütük Nereye Yazılır

Sunucu kütüğü standart çıktıya yazar; dosya açmaz, döndürmez, sıkıştırmaz. Bu bir eksiklik
değil, bilinçli bir sınırdır: uygulamanın işi kaydı **üretmektir**, saklamak değil.

Gerekçesi önceki bölümde ölçülmüştü. Uygulama birden çok süreç olarak çalışır ve süreçler
ölümlüdür; her sürecin kendi dosyasına yazması, aynı isteğin izini birden çok dosyaya dağıtır
ve ölen sürecin dosyası sahipsiz kalır. Standart çıktıya yazıldığında toplama işini süreci
çalıştıran katman üstlenir: bütün süreçlerin çıktısı tek bir akışta birleşir.

Bu ayrımın adı ve gerekçesi bu bölümün son dersinde, taşınabilirlik ilkeleri arasında yeniden
ele alınacak.

#### Özet

- Yapılandırılmış kayıt her satırı bağımsız bir JSON nesnesi olarak yazar; zaman damgası
  ISO 8601 biçimindedir, olay adı sabit bir tanımlayıcıdır ve bağlam adlandırılmış alanlarda
  taşınır.
- Düzey eşiği karşılaştırması sayısaldır ve eşiğin altındaki kayıt hiç üretilmez; ölçümde eşik
  `bilgi` iken on, `ayrintili` iken on iki satır yazılmıştır.
- Eşzamansız yerel depo istek bağlamını çağrı ağacına bağlar; veri katmanındaki fonksiyon
  istek kimliğini parametre almadığı hâlde ürettiği satırlar doğru kimliği taşımıştır.
- Eşzamanlı isteklerin satırları zaman sırasında iç içe geçer; tek bir isteğin akışı ancak
  kimliğe göre süzülerek geri kurulur.
- Kütük standart çıktıya yazılır; toplama ve saklama işi süreci çalıştıran katmana bırakılır,
  çünkü süreçler çok sayıda ve ölümlüdür.

#### Sonraki Adım

Bu dersin sunucusunda hata durumu tek bir dala sıkışmıştı: rafta bulunmayan kitap `409`
üretiyordu. Gerçek bir uygulamada hatalar iki ayrı kaynaktan gelir — kullanıcının yanlış
isteği ve uygulamanın kendi kusuru — ve ikisi aynı biçimde ele alınamaz. Sonraki ders ortak
hata yanıtı üretimini kurar: hangi hata hangi durum koduna eşlenir, yanıt gövdesi hangi
alanları taşır ve gövdenin iç ayrıntı sızdırmadığı hangi sınamayla doğrulanır?

### Hata İşleme Katmanı

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/hata-isleme-katmani

İstem hatası ile programcı hatası ayrılır ve tek bir katman her ikisini de aynı yanıt biçimine çevirir; istemciye giden metinler sabit bir tablodan gelir, iç ayrıntı yalnız kütükte kalır ve yanıt gövdelerinin sızıntı içermediği yedi senaryoda sınanır.

Önceki dersin sunucusunda hata durumu tek bir dala sıkışmıştı: rafta bulunmayan kitap `409`
üretiyordu. Gerçek bir uygulamada hatalar iki ayrı kaynaktan gelir. Üye eksik alanla istek
gönderir; bu, uygulamanın öngördüğü bir durumdur ve yanıtı bilgilendiricidir. Uygulama
kendi kodundaki bir kusurla çöker; bu, öngörülmemiş bir durumdur ve yanıtı bilgi vermemelidir.

Bu ders iki kaynağı ayırır ve ikisini de tek bir katmandan geçirir. Sonuçta her hata aynı
biçimde yanıta dönüşür; aralarındaki fark, gövdenin ne kadarını taşıdığındadır.

#### İki Hata Sınıfı

**İstem hatası** (client error) uygulamanın beklediği bir durumdur: eksik alan, bozuk gövde,
bulunmayan kaynak, çakışan durum. Bunlar kusur değildir; iş kuralının kendisidir. Yanıtları
`4xx` aralığındadır ve istemcinin ne yapması gerektiğini söylemelidir.

**Programcı hatası** (programmer error) uygulamanın kendi kusurudur: var olmayan bir fonksiyonun
çağrılması, tanımsız bir değerin okunması, tutulmayan bir varsayım. Bunların yanıtı `5xx`
aralığındadır ve istemciye söyleyecek anlamlı hiçbir şeyleri yoktur — çünkü istemcinin
yapabileceği bir şey yoktur.

Ayrımın Node.js Çalışma Zamanı kursunda kurulan **işletimsel hata** ve **programcı hatası**
ayrımıyla ilişkisi doğrudandır; buradaki fark, ayrımın bir HTTP yanıtına çevrilmesidir.

Aşağıdaki modül ayrımı bir sınıfla ve bir çeviri fonksiyonuyla kurar.

```js
// kaynak/kurulum/hatalar.mjs — istem hatasi ile programci hatasini ayirir ve yanita cevirir
export class IstemHatasi extends Error {
  constructor(kod, durum, ayrintilar = null) {
    super(`istem hatasi: ${kod}`);
    this.name = "IstemHatasi";
    this.kod = kod;                       // sabit tanimlayici; istemci buna gore dallanir
    this.durum = durum;
    this.ayrintilar = ayrintilar;         // istemciye gosterilmesi guvenli alanlar
  }
}

// Istemciye giden metinlerin tek kaynagi. Hata nesnesinin kendi iletisi yanita hic girmez.
const ILETILER = {
  govde_bicimsiz: "Istek govdesi JSON olarak cozulemedi.",
  alan_eksik: "Istek zorunlu alanlari tasimiyor.",
  kimlik_yok: "Gecerli bir uye kimligi gerekiyor.",
  kitap_yok: "Katalogda bu ISBN bulunmuyor.",
  raf_bos: "Kitabin rafta kopyasi kalmadi.",
  ic_hata: "Istek islenemedi.",
};

export const yanitaCevir = (hata, istekKimligi) => {
  const bilinen = hata instanceof IstemHatasi && hata.kod in ILETILER;
  const kod = bilinen ? hata.kod : "ic_hata";
  const govde = { hata: { kod, ileti: ILETILER[kod] }, istekKimligi };
  if (bilinen && hata.ayrintilar) govde.hata.ayrintilar = hata.ayrintilar;
  return { durum: bilinen ? hata.durum : 500, govde };
};
```

Üç karar bu otuz satırda gizlidir.

**İstemciye giden metin hatadan gelmez.** `yanitaCevir` fonksiyonu `hata.message` değerini
hiç okumaz; iletiyi sabit bir tablodan alır. Hata iletileri geliştirici için yazılır ve dosya
yolu, sorgu metni, iç değişken adı taşıyabilir. Tablodan gelen ileti bunu yapısal olarak
imkânsız kılar.

**Bilinmeyen her şey iç hatadır.** Koşul yalnız `IstemHatasi` örneklerini değil, kodu tabloda
bulunan örnekleri geçirir. Tabloya eklenmemiş bir kodla oluşturulmuş bir hata da `ic_hata`
olur; varsayılan, sızdırmama yönündedir.

**Ayrıntılar açıkça işaretlenir.** `ayrintilar` alanı yalnız hatayı fırlatan kodun bilerek
koyduğu değerleri taşır — eksik alan adları, sorgulanan ISBN. Bu, istemciye yardım eden bilgi
ile iç ayrıntı arasındaki sınırı bir alan adına bağlar.

#### Tek Katmandan Geçirmek

Sunucunun bütün işleyicileri hata fırlatır; hiçbiri yanıt yazmaz. Yanıt yazma işi tek bir
`catch` bloğundadır. Bu, önceki bölümde kurulan kuralın uygulanmasıdır: hata yakalayıcı,
koruduğu her şeyin dışında durur.

```js
// kaynak/http/sunucu.mjs — tek bir hata katmani: her hata ayni bicimde yanita cevrilir
import { createServer } from "node:http";
import { IstemHatasi, yanitaCevir } from "../kurulum/hatalar.mjs";

const KATALOG = new Map([["978-0262033848", { rafta: 0 }], ["978-0201835953", { rafta: 3 }]]);
let sayac = 0;

const govdeyiCoz = (metin) => {
  try {
    return metin ? JSON.parse(metin) : {};
  } catch {
    throw new IstemHatasi("govde_bicimsiz", 400);
  }
};

const oduncVer = (istem, uye) => {
  if (!uye) throw new IstemHatasi("kimlik_yok", 401);
  const eksik = ["isbn"].filter((alan) => !istem[alan]);
  if (eksik.length > 0) throw new IstemHatasi("alan_eksik", 400, { eksikAlanlar: eksik });
  const kitap = KATALOG.get(istem.isbn);
  if (!kitap) throw new IstemHatasi("kitap_yok", 404, { isbn: istem.isbn });
  if (kitap.rafta < 1) throw new IstemHatasi("raf_bos", 409, { isbn: istem.isbn });
  kitap.rafta -= 1;
  return { odunc: { isbn: istem.isbn, uye }, kalan: kitap.rafta };
};

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const kimlik = `i-${++sayac}`;
  const yaz = (durum, govde) => {
    const metin = JSON.stringify(govde);
    yanit.writeHead(durum, { "content-type": "application/json; charset=utf-8",
      "content-length": Buffer.byteLength(metin), "x-istek-kimligi": kimlik });
    yanit.end(metin);
  };

  try {
    const parcalar = [];
    for await (const parca of istek) parcalar.push(parca);
    const yol = new URL(istek.url, "http://yerel").pathname;
    const uye = (istek.headers.authorization ?? "").replace(/^Uye /, "") || null;

    if (yol === "/odunc") return yaz(201, oduncVer(govdeyiCoz(Buffer.concat(parcalar).toString()), uye));
    if (yol === "/rapor") return yaz(200, { toplam: KATALOG.gec().length });   // programci hatasi
    throw new IstemHatasi("kitap_yok", 404);
  } catch (hata) {
    const beklenen = hata instanceof IstemHatasi;
    console.log(JSON.stringify({                                   // ic ayrinti yalniz kutukte
      duzey: beklenen ? "uyari" : "hata", istekKimligi: kimlik,
      tur: beklenen ? "istem_hatasi" : "programci_hatasi",
      ad: hata.name, ileti: hata.message,
      yigit: beklenen ? null : ((hata.stack ?? "").split("\n")[1] ?? "")
        .trim().replace(/\(file:.*\/(?=[^\/]+:\d+:\d+\))/, "("),
    }));
    const { durum, govde } = yanitaCevir(hata, kimlik);
    yaz(durum, govde);
  }
}).listen(8439, "127.0.0.1", () => console.log(JSON.stringify({ olay: "acildi", baglantiNoktasi: 8439 })));
```

`/rapor` yolundaki `KATALOG.gec()` çağrısı var olmayan bir yöntemi çağırır ve gerçek bir
programcı hatası üretir; taklit edilmiş bir hata değil, yazım yanlışının doğal sonucudur.

Kütük kaydı iki hata sınıfını ayrı düzeylere koyar. İstem hataları `uyari` düzeyindedir ve
yığıt izi taşımaz — çünkü kodun nerede olduğu bilinen bir şeydir. Programcı hataları `hata`
düzeyindedir ve yığıt izinin ilk satırını taşır; kusuru bulmak için gereken tek bilgi budur.

#### Yedi Senaryonun Sınanması

Yanıt biçiminin doğruluğu okunarak değil, bütün dalları çalıştırılarak doğrulanır. Aşağıdaki
sınama yedi senaryoyu sırayla dener, her yanıtın durum kodunu beklenenle karşılaştırır ve
gövdede yasaklı kalıp arar.

Sınamanın ikinci yarısı önemlidir: yanıtların temiz olması, uygulamanın hataları sessizce
yutmasıyla da sağlanabilirdi. Bunu dışlamak için sınama, iç ayrıntının kütükte **gerçekten
bulunduğunu** da doğrular. İki yön birlikte sınandığında geçen bir sınama anlamlıdır.

```js
// denetim/hata-yaniti.mjs — her hata yanitinin bicimini ve sizinti icermedigini sinar.
// Ayrica programci hatasinin ic ayrintisinin kutukte GERCEKTEN bulundugunu dogrular:
// sinamanin sessizce her seyi yutan bir uygulamayi onaylamamasi icin iki yon de gereklidir.
import { spawn } from "node:child_process";
import { setTimeout as bekle } from "node:timers/promises";

const YASAK = [/at\s+\S+\s+\(/, /\.mjs/, /node:/, /TypeError/, /\/Users\//, /istem hatasi:/];

const DURUMLAR = [
  ["gecerli istek", "/odunc", "Uye U-4711", '{"isbn":"978-0201835953"}', 201],
  ["kimliksiz istek", "/odunc", null, '{"isbn":"978-0201835953"}', 401],
  ["bozuk govde", "/odunc", "Uye U-4711", "{isbn", 400],
  ["eksik alan", "/odunc", "Uye U-4711", "{}", 400],
  ["katalogda yok", "/odunc", "Uye U-4711", '{"isbn":"978-0000000000"}', 404],
  ["raf bos", "/odunc", "Uye U-4711", '{"isbn":"978-0262033848"}', 409],
  ["programci hatasi", "/rapor", "Uye U-4711", null, 500],
];

const cocuk = spawn("node", ["kaynak/http/sunucu.mjs"]);
let kutuk = "";
cocuk.stdout.on("data", (p) => (kutuk += p));
cocuk.stderr.on("data", (p) => (kutuk += p));
await bekle(700);

const kusurlar = [];
console.log(`${"durum".padEnd(6)} | ${"senaryo".padEnd(18)} | yanit govdesi`);
for (const [ad, yol, kimlik, govde, beklenen] of DURUMLAR) {
  const yanit = await fetch(`http://127.0.0.1:8439${yol}`, {
    method: govde === null ? "GET" : "POST",
    headers: { ...(kimlik ? { authorization: kimlik } : {}),
      ...(govde === null ? {} : { "content-type": "application/json" }) },
    body: govde,
  });
  const metin = await yanit.text();
  console.log(`${String(yanit.status).padEnd(6)} | ${ad.padEnd(18)} | ${metin}`);
  if (yanit.status !== beklenen) kusurlar.push(`${ad}: ${beklenen} bekleniyordu, ${yanit.status} geldi`);
  for (const kalip of YASAK) {
    if (kalip.test(metin)) kusurlar.push(`${ad}: yanit govdesinde yasak kalip ${kalip}`);
  }
}

await bekle(200);
cocuk.kill();
await bekle(300);

console.log("--- kutuk ---");
console.log(kutuk.trimEnd().split("\n").map((s) => "  " + s).join("\n"));

if (!/programci_hatasi/.test(kutuk)) kusurlar.push("kutukte programci hatasi kaydi yok");
if (!/TypeError/.test(kutuk)) kusurlar.push("kutukte ic hata turu yok");

console.log("--- denetim ---");
for (const kusur of kusurlar) console.log("  KUSUR  " + kusur);
console.log(kusurlar.length === 0
  ? "  yedi yanit da bicime uygun; ic ayrinti yalniz kutukte"
  : `  ${kusurlar.length} kusur`);
process.exit(kusurlar.length === 0 ? 0 : 1);
```

```
durum  | senaryo            | yanit govdesi
201    | gecerli istek      | {"odunc":{"isbn":"978-0201835953","uye":"U-4711"},"kalan":2}
401    | kimliksiz istek    | {"hata":{"kod":"kimlik_yok","ileti":"Gecerli bir uye kimligi gerekiyor."},"istekKimligi":"i-2"}
400    | bozuk govde        | {"hata":{"kod":"govde_bicimsiz","ileti":"Istek govdesi JSON olarak cozulemedi."},"istekKimligi":"i-3"}
400    | eksik alan         | {"hata":{"kod":"alan_eksik","ileti":"Istek zorunlu alanlari tasimiyor.","ayrintilar":{"eksikAlanlar":["isbn"]}},"istekKimligi":"i-4"}
404    | katalogda yok      | {"hata":{"kod":"kitap_yok","ileti":"Katalogda bu ISBN bulunmuyor.","ayrintilar":{"isbn":"978-0000000000"}},"istekKimligi":"i-5"}
409    | raf bos            | {"hata":{"kod":"raf_bos","ileti":"Kitabin rafta kopyasi kalmadi.","ayrintilar":{"isbn":"978-0262033848"}},"istekKimligi":"i-6"}
500    | programci hatasi   | {"hata":{"kod":"ic_hata","ileti":"Istek islenemedi."},"istekKimligi":"i-7"}
--- kutuk ---
  {"olay":"acildi","baglantiNoktasi":8439}
  {"duzey":"uyari","istekKimligi":"i-2","tur":"istem_hatasi","ad":"IstemHatasi","ileti":"istem hatasi: kimlik_yok","yigit":null}
  {"duzey":"uyari","istekKimligi":"i-3","tur":"istem_hatasi","ad":"IstemHatasi","ileti":"istem hatasi: govde_bicimsiz","yigit":null}
  {"duzey":"uyari","istekKimligi":"i-4","tur":"istem_hatasi","ad":"IstemHatasi","ileti":"istem hatasi: alan_eksik","yigit":null}
  {"duzey":"uyari","istekKimligi":"i-5","tur":"istem_hatasi","ad":"IstemHatasi","ileti":"istem hatasi: kitap_yok","yigit":null}
  {"duzey":"uyari","istekKimligi":"i-6","tur":"istem_hatasi","ad":"IstemHatasi","ileti":"istem hatasi: raf_bos","yigit":null}
  {"duzey":"hata","istekKimligi":"i-7","tur":"programci_hatasi","ad":"TypeError","ileti":"KATALOG.gec is not a function","yigit":"at Server.<anonymous> (sunucu.mjs:44:61)"}
--- denetim ---
  yedi yanit da bicime uygun; ic ayrinti yalniz kutukte
```

Sınama sıfır çıkış kodu üretir. Yığıt izindeki satır ve sütun numarası kodun her
değişikliğinde kayar.

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

**Altı hata yanıtı aynı biçimdedir.** Hepsinde `hata.kod`, `hata.ileti` ve `istekKimligi`
vardır; ikisinde ek olarak `ayrintilar` bulunur. İstemci tarafında tek bir çözümleyici bütün
hataları işleyebilir ve dallanmayı `kod` alanına dayandırır. İletinin kendisi görüntülemek
içindir; karar `kod` ile verilir, çünkü ileti değişebilir, kod değişmez.

**Ayrıntılar yalnız yararlı olduğunda vardır.** `alan_eksik` yanıtı eksik alanların adını
verir, `kitap_yok` yanıtı sorgulanan ISBN'i döndürür. Bunlar istemcinin zaten bildiği ya da
düzeltmesi gereken değerlerdir. Kimlik hatası yanıtında hiçbir ayrıntı yoktur; hangi kimliğin
neden geçersiz olduğunu söylemek, kimlik denemesi yapan bir tarafa geri bildirim vermek
demektir.

**Beşinci senaryo bir ayrımı görünür kılar.** Katalogda bulunmayan ISBN `404`, rafta kopyası
kalmayan kitap `409` alır. İkisi de "veremiyorum" der, ama biri kaynağın hiç olmadığını,
diğeri kaynağın var olup şu an uygun olmadığını bildirir. İstemci ilkinde girdiyi düzeltir,
ikincisinde bekler.

**Yedinci senaryo ayrımın asıl noktasıdır.** İstemciye giden gövde yalnız `ic_hata` kodunu,
sabit tablodan gelen iletiyi ve `i-7` kimliğini taşır. Kütükte ise aynı isteğin `TypeError` olduğu,
iletisinin `KATALOG.gec is not a function` olduğu ve yığıt izinin ilk satırı bulunur. Aynı
olay iki yerde iki farklı ayrıntı düzeyiyle kayıtlıdır ve `i-7` kimliğiyle eşleşir.

**Sınama iki yönlüdür.** Yanıtlarda yasaklı kalıp bulunmaması tek başına yetmez; kütükte
`programci_hatasi` ve `TypeError` kayıtlarının bulunması da sınanır. Hataları sessizce yutan
bir uygulama ilk koşulu geçer, ikincisinde kalır.

#### Özet

- Hatalar iki sınıfa ayrılır: istem hatası uygulamanın öngördüğü bir durumdur ve `4xx`
  üretir, programcı hatası uygulamanın kusurudur ve `5xx` üretir.
- İstemciye giden ileti hata nesnesinden değil, sabit bir tablodan gelir; tabloda karşılığı
  olmayan her hata `ic_hata` sayılır, yani varsayılan sızdırmama yönündedir.
- İstemciye gösterilebilir alanlar `ayrintilar` başlığı altında açıkça işaretlenir; kimlik
  hatasında ayrıntı verilmez.
- Bütün işleyiciler hata fırlatır, yanıtı tek bir katman yazar; kütük kaydı istem hatalarını
  `uyari`, programcı hatalarını `hata` düzeyinde tutar ve yığıt izini yalnız ikincisine ekler.
- Sınama yedi senaryonun yanıt gövdesinde yasaklı kalıp aramakla kalmaz, iç ayrıntının kütükte
  bulunduğunu da doğrular; iki yön birlikte sınandığında hataları yutan bir uygulama sınamayı
  geçemez.

#### Sonraki Adım

Bu bölümde kurulan parçalar — katmanlı dizin düzeni, ortamdan okunan ve açılışta doğrulanan
yapılandırma, gizli değerlerin ayrılması, standart çıktıya yazılan yapılandırılmış kütük ve
ortak hata yanıtı — birbirinden bağımsız kararlar gibi görünür. Aralarında bir bağ vardır ve
bu bağ tek bir hedefte toplanır: uygulamanın çalıştığı makineden bağımsız olması. Sonraki ders
bu kararları adlandırılmış bir ilkeler kümesi olarak toplar, her ilkenin bu bölümde hangi
ölçümle karşılandığını gösterir ve karşılanmayanları işaret eder.

### On İki Etmen Uygulama İlkeleri

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/on-iki-etmen-uygulama-ilkeleri

Bu bölümde alınan kararlar taşınabilirlik sözleşmesi olarak adlandırılmış on iki ilkeyle eşleştirilir; tek yapının iki ortamda değişmeden çalıştığı dosya özetiyle, hızlı açılış ve zarif kapanma ise uçuştaki isteğin tamamlanmasıyla ölçülür.

Bu bölümde alınan kararlar birbirinden bağımsız gibi görünür: dizinleri katmanlara bölmek,
yapılandırmayı ortamdan okumak, gizli değerleri maskelemek, kütüğü standart çıktıya yazmak,
hataları tek bir katmandan geçirmek. Aralarında bir bağ vardır ve bağ tek bir hedefte
toplanır: uygulamanın **çalıştığı makineden bağımsız** olması.

Bu hedefi adlandırılmış kurallar hâline getiren bir yöntem vardır: **on iki etmen uygulama
ilkeleri** (twelve-factor app). İlkeler bir çatı ya da araç değil, bir sözleşmedir; uygulamayı
yazan ile onu çalıştıran arasındaki arayüzü tanımlar. Bu ders ilkeleri sayar, bu bölümde
hangilerinin karşılandığını ölçümlerle eşleştirir ve karşılanmayanları işaret eder.

#### Sözleşmenin Konusu

İlkelerin ortak varsayımı şudur: uygulamayı çalıştıran taraf, uygulamanın içini bilmeden onu
kurabilmeli, ayarlayabilmeli, çoğaltabilmeli, izleyebilmeli ve durdurabilmelidir. Bu
sağlandığında uygulama bir makineye değil, bir sözleşmeye bağlanır.

Kütüphane ödünç servisi açısından bunun karşılığı somuttur. Servisi bir dizüstü bilgisayarda
çalıştıran geliştirici ile bir sunucuda çalıştıran işletmeci aynı dosyayı kullanır; aralarındaki
tek fark verdikleri ortam değişkenleridir. Kod, hangi makinede olduğunu bilmez.

On iki ilke ve bu bölümdeki karşılıkları aşağıdaki gibidir.

| İlke | İçerik | Bu bölümdeki karşılığı |
|---|---|---|
| Kod tabanı | Tek kod tabanı, çok dağıtım | Sürüm Kontrolüne Giriş kursu |
| Bağımlılıklar | Açıkça bildirilir ve yalıtılır | Modüller, Araçlar ve Ekosistem kursu |
| Yapılandırma | Ortamda saklanır | Yapılandırma Yönetimi dersi |
| Destekleyici servisler | Bağlı kaynak sayılır | Yerel Geliştirme Ortamı dersi |
| Derle, yayımla, çalıştır | Aşamalar kesin ayrılır | Oluşturma Stratejileri ve Altyapı kursu |
| Süreçler | Durumsuz ve paylaşımsız | Uygulama Çalışma Zamanı dersi |
| Bağlantı noktası bağlama | Hizmet kendi bağlar | Web Sunucusu Kavramı dersi |
| Eşzamanlılık | Süreç modeliyle ölçeklenir | Uygulama Çalışma Zamanı dersi |
| Tek kullanımlık olma | Hızlı açılış, zarif kapanma | Bu ders |
| Geliştirme/üretim eşliği | Ortamlar birbirine yakın tutulur | Bu ders |
| Kütükler | Olay akışı sayılır | Günlükleme Kurulumu dersi |
| Yönetim süreçleri | Tek seferlik iş olarak çalıştırılır | Backend Geliştirme müfredatının ileri kursları |

Tabloda dört ilke bu kursun dışına düşer; ikisi bu dersin ölçümleridir.

#### Ölçüm: Tek Yapı, İki Ortam

**Yapılandırma** ilkesinin sınanabilir biçimi şudur: dosyanın özeti değişmeden, davranışı
ortam değişkenleriyle değişmelidir. Aynı ölçüm **geliştirme/üretim eşliğinin** de bir
parçasını gösterir — iki ortamda koşan şey aynı yapıdır — ama eşlik ilkesi daha geniştir:
ortamlar arasındaki zaman, kişi ve araç açıklığının küçük tutulmasını, özellikle de aynı
destekleyici servislerin kullanılmasını ister; bu ders o tarafı ölçmez.

```js
// kaynak/http/sunucu.mjs — ayni dosya her ortamda; baglanti noktasini kendi baglar,
// kutugu standart cikisa yazar, SIGTERM aldiginda ucustaki istegi bitirip iner.
import { createServer } from "node:http";
import { setTimeout as bekle } from "node:timers/promises";

const BAGLANTI_NOKTASI = Number(process.env.KUTUPHANE_BAGLANTI_NOKTASI ?? 8440);
const ORTAM = process.env.KUTUPHANE_ORTAM_ETIKETI ?? "yerel";
const ODUNC_GUN = Number(process.env.KUTUPHANE_ODUNC_GUN ?? 14);

const kutuk = (kayit) => console.log(JSON.stringify({ an: new Date().toISOString(), ...kayit }));

let acikIstek = 0;

const sunucu = createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  acikIstek++;
  const yol = new URL(istek.url, "http://yerel").pathname;
  if (yol === "/odunc") await bekle(1500);            // suren bir odunc islemi
  const metin = JSON.stringify({ ortam: ORTAM, oduncGun: ODUNC_GUN, yol });
  yanit.writeHead(200, { "content-type": "application/json; charset=utf-8",
    "content-length": Buffer.byteLength(metin) });
  yanit.end(metin);
  acikIstek--;
});

sunucu.listen(BAGLANTI_NOKTASI, "127.0.0.1", () => kutuk({
  olay: "acildi", baglantiNoktasi: BAGLANTI_NOKTASI, ortam: ORTAM, oduncGun: ODUNC_GUN,
  acilisMs: Number(performance.now().toFixed(1)),      // surec baslangicindan bu yana
}));

process.on("SIGTERM", () => {
  kutuk({ olay: "sigterm_alindi", acikIstek });
  sunucu.close(() => {                                // yeni baglanti yok, acik olanlar biter
    kutuk({ olay: "kapandi", acikIstek });
    process.exit(0);
  });
  sunucu.closeIdleConnections();                      // bos bekleyen baglantilar hemen kapanir
  setTimeout(() => { kutuk({ olay: "zorla_kapandi" }); process.exit(1); }, 5000).unref();
});
```

```bash
#!/usr/bin/env bash
# Ayni dosyayi iki ortamla calistirir; dosyanin ozeti degismeden davranisin degistigini gosterir.
echo "dosya ozeti: $(shasum -a 256 kaynak/http/sunucu.mjs | cut -c1-16)…"

dene() {  # $1 = ortam etiketi, $2 = odunc gunu, $3 = baglanti noktasi
  KUTUPHANE_ORTAM_ETIKETI="$1" KUTUPHANE_ODUNC_GUN="$2" KUTUPHANE_BAGLANTI_NOKTASI="$3" \
    node kaynak/http/sunucu.mjs > "kutuk-$1.jsonl" 2>&1 & s=$!
  sleep 0.7
  printf '%-8s -> ' "$1"
  curl -sS "http://127.0.0.1:$3/kitaplar"; echo
  kill -TERM "$s"; wait "$s" 2>/dev/null
  grep -o '"acilisMs":[0-9.]*' "kutuk-$1.jsonl"
  rm -f "kutuk-$1.jsonl"
}

dene yerel 14 8441
dene yayin 21 8442
```

```
dosya ozeti: cc28586133fd7c6a…
yerel    -> {"ortam":"yerel","oduncGun":14,"yol":"/kitaplar"}
"acilisMs":28.5
yayin    -> {"ortam":"yayin","oduncGun":21,"yol":"/kitaplar"}
"acilisMs":28.2
```

Dosya özeti ve açılış süreleri her makinede ve her düzenlemeden sonra değişir; özet, dosyanın
iki çalıştırma arasında değişmediğini göstermek için buradadır.

Aynı dosya iki ortamda iki farklı yanıt üretmiştir: ödünç süresi bir ortamda on dört, diğerinde
yirmi bir gündür. Kodda hiçbir dallanma yoktur; ortamın adını taşıyan bir koşul, bir
`if (ORTAM === "yayin")` satırı bulunmaz. Böyle bir satır eklendiği anda ilke kırılır, çünkü
uygulama artık nerede çalıştığını bilir ve yerelde sınanan yol ile yayında çalışan yol
ayrışır.

Aynı çıktı bir başka ilkeyi de doğrular. **Bağlantı noktası bağlama** ilkesi, hizmetin dış bir
bileşene ihtiyaç duymadan kendi bağlantı noktasını açmasını ister. `curl` çağrısı doğrudan
sürece gitmiştir; arada hiçbir bileşen yoktur. Önceki bölümde kurulan ters vekil bir seçenektir,
bir zorunluluk değil — ve tam olarak bu yüzden yerel geliştirme ile yayın aynı kodu çalıştırır.

#### Ölçüm: Tek Kullanımlık Olma

**Tek kullanımlık olma** (disposability) ilkesi süreçlerin hızlı açılmasını ve düzgün
inmesini ister. İkisi de aynı gerekçeye dayanır: süreçler sık başlatılır ve sık durdurulur.
Yayın, ölçek değişikliği, makine değişimi ve çökme sonrası yeniden başlatma bunu zorunlu kılar.

Açılış süresi yukarıdaki çıktıda görülür: süreç başlangıcından dinlemeye geçişe kadar otuz
milisaniyenin altı. Asıl sınanması gereken ise inişin nasıl olduğudur. Aşağıdaki ölçüm,
uçuşta bir istek varken sürece `SIGTERM` gönderir.

```bash
#!/usr/bin/env bash
# Ucusta bir istek varken SIGTERM gonderir; istegin bitip yeni baglantinin reddedildigini olcer.
node kaynak/http/sunucu.mjs > kutuk.jsonl 2>&1 & surec=$!
sleep 0.7

curl -sS -o /dev/null -w 'ucustaki istek : %{http_code}  %{time_total} s\n' \
  http://127.0.0.1:8440/odunc > ucus.txt & istek=$!
sleep 0.4
echo "SIGTERM gonderildi"
kill -TERM "$surec"
sleep 0.4

printf 'kapanma sirasinda yeni istek : '
if curl -s -o /dev/null --max-time 2 http://127.0.0.1:8440/kitaplar; then
  echo "yanit alindi"
else
  echo "baglanti kurulamadi (curl cikis kodu=$?)"
fi

wait "$istek"; cat ucus.txt
wait "$surec"; echo "surec cikis kodu=$?"
echo "--- kutuk ---"
cat kutuk.jsonl
rm -f kutuk.jsonl ucus.txt
```

```
SIGTERM gonderildi
kapanma sirasinda yeni istek : baglanti kurulamadi (curl cikis kodu=7)
ucustaki istek : 200  1.505712 s
surec cikis kodu=0
--- kutuk ---
{"an":"2026-07-28T20:20:44.905Z","olay":"acildi","baglantiNoktasi":8440,"ortam":"yerel","oduncGun":14,"acilisMs":25.9}
{"an":"2026-07-28T20:20:45.986Z","olay":"sigterm_alindi","acikIstek":1}
{"an":"2026-07-28T20:20:47.101Z","olay":"kapandi","acikIstek":0}
```

Zaman damgaları ve süreler her çalıştırmada değişir.

Üç gözlem sabittir. **Uçuştaki istek tamamlanmıştır**: sinyal isteğin ortasında gelmiş,
buna rağmen yanıt `200` olarak dönmüş ve bir buçuk saniyelik iş kesilmemiştir. Kütükteki
`sigterm_alindi` satırı o anda bir açık istek bulunduğunu, `kapandi` satırı ise kapanma
tamamlandığında sıfır açık istek kaldığını yazar.

**Yeni bağlantı reddedilmiştir.** Sinyalden sonra yapılan istek bağlantı bile kuramamıştır;
`sunucu.close` çağrısı dinleyen soketi kapatmış, ama açık bağlantıları kesmemiştir. Kapanan
bir süreç için doğru davranış budur: yeni iş alma, aldığın işi bitir.

**Çıkış kodu sıfırdır.** Bir denetçi bu kodu gördüğünde sürecin isteyerek indiğini bilir;
sıfırdan farklı bir kod ise beklenmedik bir sonlanmayı bildirir ve farklı bir tepki gerektirir.

Zorla kapanma zamanlayıcısı bu düzenin kaçınılmaz tamamlayıcısıdır. Bitmeyen bir istek
süreci sonsuza kadar bekletebilir; beş saniyelik sınır dolduğunda süreç sıfırdan farklı bir
kodla iner. `unref` çağrısı bu zamanlayıcının, kapanma erken tamamlandığında sürecin ömrünü
uzatmasını engeller.

#### Karşılanmayanlar

İlkeler listesinin dürüst okunması, karşılanmamış olanları da saymayı gerektirir.

**Destekleyici servisler** ilkesi henüz uygulanmadı. Bu bölümdeki depolar süreç belleğindeydi;
bir veritabanının ya da dış servisin yalnız bir adresle tanımlanan, değiştirilebilir bir bağlı
kaynak olarak ele alınması, bu kursun Yerel Geliştirme Ortamı dersinde kurulur.

**Yönetim süreçleri** ilkesi de açıktır. Veri göçü, toplu düzeltme, rapor üretimi gibi tek
seferlik işlerin uygulamanın kendisiyle aynı kod ve aynı ortamla çalıştırılması gerekir; ayrı
bir betikle yapıldığında iki farklı gerçeklik oluşur.

**Derle, yayımla, çalıştır** ayrımı bu bölümde ölçülmedi; yapı çıktısının ortam
değişkenlerinden ayrı tutulması ve her yayının geri alınabilir bir kimlik taşıması Oluşturma
Stratejileri ve Altyapı kursunda ele alınmıştı ve sunucu tarafındaki karşılığı ileri
kurslardadır.

Bir ilkeyi karşılamamak kusur değildir; karşılamadığını bilmemek kusurdur. Listenin işlevi
budur — kararların hangisinin alınmış, hangisinin ertelenmiş olduğunu ada bağlar.

#### Özet

- On iki etmen ilkeleri bir araç değil, uygulamayı yazan ile çalıştıran arasındaki
  sözleşmedir; ortak hedefleri uygulamayı çalıştığı makineden bağımsız kılmaktır.
- Yapılandırmanın ortamda saklanması, dosya özeti değişmeden davranışın ortam
  değişkenleriyle değişmesiyle ölçüldü; kodda ortam adına bakan hiçbir dallanma yoktur.
  Geliştirme/üretim eşliğinin ölçülmeyen tarafı, iki ortamın aynı destekleyici servisleri
  kullanmasıdır.
- Bağlantı noktası bağlama ilkesi gereği hizmet kendi soketini açar; ters vekil bir seçenektir,
  önkoşul değildir.
- Tek kullanımlık olma ölçümünde `SIGTERM` sonrası uçuştaki istek `200` ile tamamlanmış, yeni
  bağlantı reddedilmiş ve süreç sıfır çıkış koduyla inmiştir; zorla kapanma zamanlayıcısı
  bitmeyen isteğe karşı sınırı korur.
- Destekleyici servisler, yönetim süreçleri ve derle-yayımla-çalıştır ayrımı bu bölümde
  karşılanmadı; listenin işlevi ertelenmiş kararları adlandırmaktır.

#### Sonraki Adım

İskelet artık ayakta: yapılandırma ortamdan okunuyor, gizli değerler ayrılmış, kütük standart
çıktıya yazılıyor, hatalar ortak bir biçimde yanıta dönüşüyor ve süreç düzgün inip kalkıyor.
Buna karşılık uygulama dışarıya hâlâ hiçbir gerçek içerik vermiyor; verdiği tek şey kendi
durumu. Sonraki bölüm bu boşluğu doldurmaya en yalın olandan başlar: diskte duran bir dosyayı
ağ üzerinden vermek. Sonraki ders o dosyanın hangi koşulla okunmasına izin verildiğini,
alıcının içeriği nasıl yorumlayacağını, aynı dosya ikinci kez istendiğinde ne olacağını ve bu
yükün sunucuda kalıp kalmayacağını kurar.

## Statik ve Dinamik Sunum

Kurulmuş uygulama iskeletinin dışarıya ne verdiği; diskteki dosyanın doğrulayıcı ve saklama yönergesiyle sunulması, görünümün sunucuda şablondan üretilmesi, dışarıdan gelen dosyanın sınırlarla kabul edilmesi ve bağımlı servislerin yerelde ayağa kaldırılması.

### Statik Dosya Sunumu

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/statik-dosya-sunumu

Diskteki bir dosyanın HTTP yanıtına dönüşmesi: kök dizin ve yol çözümü, içerik türü ve uzunluğu, varlık etiketiyle koşullu istek, saklama yönergesinin iki sınıfı ve yükün içerik dağıtımına devredilmesi kararı.

Önceki bölüm uygulamanın iskeletini kurdu: yapılandırma ortamdan okunuyor, kütük yazılıyor,
hatalar ortak bir biçimde yanıta dönüşüyor. İskelet ayakta ama dışarıya henüz hiçbir şey
vermiyor. Kütüphane ödünç servisinin ilk vereceği şey de en yalın olan: diskte duran bir
dosya. Katalog sayfasının belgesi, biçem dosyası, betiği.

Bu ders o dosyayı ağ üzerinden vermenin neyi gerektirdiğini kurar. Dosyayı okuyup yanıta
yazmak işin küçük parçasıdır; asıl kararlar hangi dosyanın okunacağına izin verildiği,
alıcının içeriği nasıl yorumlayacağı, aynı dosyanın ikinci kez istendiğinde ne olacağı ve
bu yükün sunucuda kalıp kalmayacağıdır.

#### Kök Dizin ve Yol Çözümü

Sunulacak dosyalar tek bir dizinin altında toplanır. Bu dizine **kök dizin** (document
root) denir ve sunucunun dosya sisteminde erişmeye yetkili olduğu tek bölgedir. İstek
yolunun kök dizinle birleştirilmesine **yol çözümü** (path resolution) denir.

Yol çözümünün tek başına yeterli olmadığı yer şurasıdır: istek yolu kök dizinin dışını
gösteren bileşenler taşıyabilir. Birleştirme sonrası ortaya çıkan mutlak yol, hâlâ kök
dizinin altında mı diye sınanmalıdır. Sınamayı birleştirmeden **sonra** yapmak
zorunludur, çünkü birleştirme işlemi yolu sadeleştirir ve sadeleştirmeden önce yapılan
bir denetim yanıltıcı olur.

```js
// sunucu.mjs — kutuphane katalogunun statik varliklarini sunan dosya sunucusu
import { createServer } from "node:http";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { extname, join, sep } from "node:path";

const KOK = join(process.cwd(), "genel");

const TIPLER = {
  ".html": "text/html; charset=utf-8",
  ".css": "text/css; charset=utf-8",
  ".js": "text/javascript; charset=utf-8",
  ".svg": "image/svg+xml",
  ".json": "application/json; charset=utf-8",
};

// Icerik karmasi tasiyan ad (katalog.7f3a91c2.css) sonsuza kadar saklanabilir;
// belge her istekte dogrulanmalidir.
const KARMALI = /\.[0-9a-f]{8}\.(css|js)$/;
const saklama = (yol) =>
  KARMALI.test(yol) ? "public, max-age=31536000, immutable" : "no-cache";

// join() yolu birlestirip sadelestirir; sonuc kokun altinda degilse istek reddedilir.
const guvenliYol = (istekYolu) => {
  const tam = join(KOK, decodeURIComponent(istekYolu.split("?")[0]));
  return tam === KOK || tam.startsWith(KOK + sep) ? tam : null;
};

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const yol = guvenliYol(istek.url === "/" ? "/index.html" : istek.url);
  if (!yol) return yanit.writeHead(403).end();

  let govde;
  try {
    govde = await readFile(yol);
  } catch {
    return yanit.writeHead(404).end();
  }

  const etiket = `"${createHash("sha256").update(govde).digest("hex").slice(0, 16)}"`;
  yanit.setHeader("ETag", etiket);
  yanit.setHeader("Cache-Control", saklama(yol));
  yanit.setHeader("Content-Type", TIPLER[extname(yol)] ?? "application/octet-stream");

  if (istek.headers["if-none-match"] === etiket) return yanit.writeHead(304).end();

  yanit.setHeader("Content-Length", govde.length);
  yanit.writeHead(200).end(govde);
}).listen(8310, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8310"));
```

Sunucunun beklediği dosyalar `genel/` dizininde durur. Aşağıdaki blok bu dizini üretir;
sunucu dosyasıyla aynı yerde çalıştırılır.

```bash
#!/usr/bin/env bash
# genel/ dizinini ve sunucunun bekledigi dort varligi olusturur.
mkdir -p genel
cat > genel/index.html <<'SON'
<!doctype html>
<html lang="tr"><head><meta charset="utf-8"><title>Kutuphane Katalogu</title>
<link rel="stylesheet" href="/katalog.7f3a91c2.css">
<script src="/katalog.4b8e0d15.js" defer></script></head>
<body><h1>Kutuphane Katalogu</h1><div id="sonuc"></div></body></html>
SON
cat > genel/katalog.7f3a91c2.css <<'SON'
body { font-family: system-ui, sans-serif; margin: 2rem auto; max-width: 46rem; }
h1 { font-size: 1.5rem; }
.kitap { border-bottom: 1px solid #ddd; padding: .5rem 0; }
.kitap .yazar { color: #555; }
SON
cat > genel/katalog.4b8e0d15.js <<'SON'
const kok = document.getElementById("sonuc");
const kitaplar = await (await fetch("/kitaplar.json")).json();
kok.innerHTML = kitaplar.map((k) =>
  `<div class="kitap">${k.baslik} <span class="yazar">${k.yazar}</span></div>`).join("");
SON
cat > genel/kitaplar.json <<'SON'
[{"isbn":"9789750718533","baslik":"Kayip Zamanin Izinde","yazar":"Marcel Proust"},
 {"isbn":"9789944885300","baslik":"Tutunamayanlar","yazar":"Oguz Atay"},
 {"isbn":"9789750726439","baslik":"Kar","yazar":"Orhan Pamuk"}]
SON
```

#### İçerik Türü ve Uzunluk

Ağ üzerinden giden şey bayt dizisidir; baytın kendisi ne olduğunu söylemez. Alıcının
içeriği ne olarak işleyeceğini **içerik türü** (content type) başlığı belirler. Dosya
uzantısından türe eşleme, sunucunun bildiği tür kümesiyle sınırlı tutulur ve bilinmeyen
uzantı için genel ikili tür verilir. Metin türlerinde karakter kodlaması da başlıkta
bildirilir; bildirilmediğinde alıcı tahmine düşer ve tahmin dosyanın kendi kodlamasıyla
çakışabilir.

**İçerik uzunluğu** (content length) başlığı gövdenin bayt sayısını verir. Alıcı böylece
yanıtın nerede bittiğini bağlantıyı beklemeden bilir ve indirme ilerlemesini
hesaplayabilir. Bu başlık ancak gövdenin tamamı elde hazırken yazılabilir: dosya belleğe
okunduğu için burada mümkündür, gövde parça parça üretiliyorsa yerini parçalı aktarım
alır.

Uzunluk başlığının 304 yanıtında yazılmadığına dikkat edin. O yanıtın gövdesi yoktur.
HTTP bu başlığı 304 yanıtında yasaklamaz, ama izin verdiği tek değer gövdenin değil,
aynı isteğe verilecek 200 yanıtının uzunluğudur; bu sunucu o ayrımı taşımamak için
başlığı hiç yazmaz.

#### Doğrulayıcı ve Koşullu İstek

Aynı dosya ikinci kez istendiğinde, içerik değişmemişse baytları yeniden taşımak boş
iştir. Bunu saptamanın yolu **doğrulayıcıdır** (validator): sunucunun içerikten türettiği
ve içerik değişince değişen kısa bir değer. HTTP'de bu değer **varlık etiketi** (entity
tag) başlığıyla taşınır.

Yukarıdaki sunucu etiketi gövdenin özetinden üretir. Değiştirilme zamanından üretmek de
mümkündür ama zaman damgası saniye çözünürlüğündedir ve dosya kopyalandığında değişir;
içerikten türetilen etiket bu iki sorunu da taşımaz.

İstemci elindeki etiketi bir sonraki istekte geri gönderir. Sunucu etiketleri
karşılaştırır: aynıysa gövdesiz bir 304 yanıtı döner. Bu alışverişe **koşullu istek**
(conditional request) denir ve İnternet Nasıl Çalışır kursunda tanıtılan mekanizmanın sunucu tarafındaki
karşılığıdır.

#### Saklama Yönergesinin İki Sınıfı

Koşullu istek gövdeyi taşımaz ama isteğin kendisini yine de sunucuya kadar getirir.
İsteği hiç yapmamak, **saklama yönergesiyle** (cache directive) sağlanır: sunucu, yanıtın
ne kadar süre yeniden sorulmadan kullanılabileceğini bildirir.

Karar, dosyanın adının içeriğine bağlı olup olmadığıyla belirlenir. Sunucudaki
`katalog.7f3a91c2.css` adı bir **içerik karması** (content hash) taşır: içerik
değiştiğinde ad da değişir, dolayısıyla bu ad altındaki bayt dizisi asla değişmez. Böyle
bir varlık uzun ömürle ve değişmeyeceği bildirilerek saklanabilir. Belgenin adı ise
sabittir ve içeriği değişebilir; onun için saklama izni verilir ama her kullanımda
doğrulama istenir.

Bu ayrım, statik sunumun tek gerçek tasarım kararıdır. Adı sabit olan bir dosyaya uzun
ömür verilirse eski içerik günlerce sunulur; adı karmalı olan bir dosyaya kısa ömür
verilirse hiç gerekmeyen doğrulama istekleri üretilir.

#### Ölçüm

Aşağıdaki betik sunucuyu ayağa kaldırır, üç davranışı sınar ve durdurur. Bağlantı noktası
8310 keyfîdir ve boş olmalıdır.

```bash
#!/usr/bin/env bash
# sunucu.mjs'yi baslatir, saklama ve kosullu istek davranisini olcer, sonra durdurur.
node sunucu.mjs > /dev/null &
sunucu=$!
sleep 1
A=http://127.0.0.1:8310

echo "--- ilk istek: karmali varlik ---"
curl -sS -D - -o /dev/null "$A/katalog.7f3a91c2.css"

echo "--- ilk istek: belge ---"
curl -sS -D - -o /dev/null "$A/index.html"

ETIKET=$(curl -sS -D - -o /dev/null "$A/index.html" | awk '/^ETag:/ {print $2}' | tr -d '\r')
echo "--- kosullu istek (If-None-Match: $ETIKET) ---"
curl -sS -D - -o /dev/null -H "If-None-Match: $ETIKET" "$A/index.html"

echo "--- indirilen bayt: kosulsuz / kosullu ---"
printf 'kosulsuz  %s B\n' "$(curl -sS -o /dev/null -w '%{size_download}' "$A/index.html")"
printf 'kosullu   %s B\n' "$(curl -sS -o /dev/null -w '%{size_download}' -H "If-None-Match: $ETIKET" "$A/index.html")"

echo "--- kok disina cikma denemesi ---"
curl -sS -o /dev/null -w 'GET /../sunucu.mjs -> %{http_code}\n' --path-as-is "$A/../sunucu.mjs"

kill "$sunucu"
```

```
--- ilk istek: karmali varlik ---
HTTP/1.1 200 OK
ETag: "6e73c0c61b184ac3"
Cache-Control: public, max-age=31536000, immutable
Content-Type: text/css; charset=utf-8
Content-Length: 199
Connection: keep-alive
Keep-Alive: timeout=5

--- ilk istek: belge ---
HTTP/1.1 200 OK
ETag: "69db6e7560de7215"
Cache-Control: no-cache
Content-Type: text/html; charset=utf-8
Content-Length: 275
Connection: keep-alive
Keep-Alive: timeout=5

--- kosullu istek (If-None-Match: "69db6e7560de7215") ---
HTTP/1.1 304 Not Modified
ETag: "69db6e7560de7215"
Cache-Control: no-cache
Content-Type: text/html; charset=utf-8
Connection: keep-alive
Keep-Alive: timeout=5

--- indirilen bayt: kosulsuz / kosullu ---
kosulsuz  275 B
kosullu   0 B
--- kok disina cikma denemesi ---
GET /../sunucu.mjs -> 403
```

Etiket değerleri dosya içeriğinden türetilir; dosyaların birebir yukarıdaki gibi
üretilmediği bir dizinde farklı çıkar. Bağlantı başlıkları çalışma zamanının varsayılan
ayarına bağlıdır.

Üç sonuç okunur. Karmalı varlık ile belge **farklı saklama yönergeleri** almıştır.
Koşullu istek **275 bayt yerine 0 bayt** taşımıştır; kazanç, dosya büyüdükçe oransal
olarak aynı kalır çünkü 304 yanıtının gövdesi her boyutta boştur. Kök dizinin dışını
gösteren istek **403** ile karşılanmıştır: yol denetimi birleştirme sonrasında yapıldığı
için sadeleştirme sonucu dışarı çıkan yol yakalanmıştır.

#### İçerik Dağıtımına Devretme Kararı

Statik varlıkların sunumu bir noktadan sonra uygulama sunucusunun işi olmaktan çıkar.
Aynı baytları binlerce kez göndermek uygulama mantığı gerektirmez; bunu kullanıcıya daha
yakın duran bir katman yapabilir. İnternet Nasıl Çalışır kursunda tanıtılan **içerik dağıtım ağı**
(content delivery network) tam olarak bu katmandır ve uygulama sunucusu bu düzende
**kaynak sunucu** (origin server) adını alır.

Devretme kararı iki oranla verilir. Aşağıdaki betik önce her varlığın gerçek boyutunu ve
saklama yönergesini kaynak sunucudan öğrenir, sonra aynı iş yükünü önünde saklama katmanı
varmış gibi hesaplar.

```js
// devretme.mjs — ayni is yukunu once kaynak sunucuda olcer, sonra onunde
// paylasimli bir saklama katmani varsayarak kaynaga kalan yuku hesaplar.
const A = "http://127.0.0.1:8310";
const SAYFA = ["/index.html", "/katalog.7f3a91c2.css", "/katalog.4b8e0d15.js", "/kitaplar.json"];
const ZIYARET = 500; // ayni sayfayi acan ziyaretci sayisi

// 1) Gercek boyut ve saklama yonergesini kaynaktan ogren.
const varlik = new Map();
for (const yol of SAYFA) {
  const y = await fetch(A + yol);
  const govde = await y.arrayBuffer();
  varlik.set(yol, {
    bayt: govde.byteLength,
    saklanabilir: (y.headers.get("cache-control") ?? "").includes("max-age"),
  });
}

// 2) Kaynak sunucu yalniz basina: her ziyaret her varligi ondan ceker.
const kaynakIstek = ZIYARET * SAYFA.length;
const kaynakBayt = ZIYARET * SAYFA.reduce((t, y) => t + varlik.get(y).bayt, 0);

// 3) Onunde saklama katmani: saklanabilir varlik bir kez cekilir, gerisi kenardan
//    karsilanir. Saklanamayan belge her ziyarette dogrulanir; yanit 304 oldugu icin
//    istek kaynaga ulasir ama govde tasinmaz.
let kenarIstek = 0, kenarBayt = 0;
for (const yol of SAYFA) {
  const v = varlik.get(yol);
  if (v.saklanabilir) { kenarIstek += 1; kenarBayt += v.bayt; }
  else { kenarIstek += ZIYARET; kenarBayt += v.bayt; }
}

const oran = (a, b) => ((1 - a / b) * 100).toFixed(1);
console.log(`is yuku: ${ZIYARET} ziyaret x ${SAYFA.length} varlik`);
console.log(`kaynak sunucu yalniz : ${kaynakIstek} istek, ${kaynakBayt} B`);
console.log(`saklama katmani onde : ${kenarIstek} istek, ${kenarBayt} B`);
console.log(`istek isabet orani   : %${oran(kenarIstek, kaynakIstek)}`);
console.log(`bayt isabet orani    : %${oran(kenarBayt, kaynakBayt)}`);
```

```
is yuku: 500 ziyaret x 4 varlik
kaynak sunucu yalniz : 2000 istek, 464500 B
saklama katmani onde : 1002 istek, 929 B
istek isabet orani   : %49.9
bayt isabet orani    : %99.8
```

İki oran birbirinden çok uzaktır ve bu ayrım kararın kendisidir. **İstek isabet oranı**
(request hit ratio) kaynağa hiç ulaşmayan isteklerin payıdır; burada yaklaşık yarıdır,
çünkü saklanamayan iki varlık her ziyarette doğrulanır. **Bayt isabet oranı** (byte hit
ratio) kaynaktan hiç çıkmayan gövdelerin payıdır; burada neredeyse tamdır, çünkü
doğrulama yanıtları gövdesizdir.

Karar bu yüzden sunucuda hangi kaynağın kıt olduğuna bakar. Darboğaz bant genişliğiyse
devretme yükü neredeyse sıfıra indirir. Darboğaz istek başına ödenen bağlantı ve işlem
maliyetiyse, kazanç ancak saklanamayan varlıkların payı kadar sınırlıdır ve iyileştirme
saklama yönergelerini gözden geçirmekten geçer. Katalog verisinin de kısa ömürlü bir
saklama yönergesiyle sunulması, ikinci oranı doğrudan yukarı çeker.

#### Özet

- Statik sunum, istek yolunun kök dizinle birleştirilmesi ve sonucun hâlâ kök dizinin
  altında olduğunun birleştirmeden sonra sınanmasıyla başlar.
- İçerik türü alıcının baytları nasıl yorumlayacağını, içerik uzunluğu gövdenin nerede
  bittiğini bildirir; bu sunucu gövdesiz 304 yanıtında uzunluk başlığını hiç yazmaz.
- Varlık etiketi içerikten türetilen bir doğrulayıcıdır; koşullu istek eşleşince yanıt
  gövdesiz döner ve ölçümde 275 bayt yerine 0 bayt taşınır.
- Saklama yönergesi iki sınıfa ayrılır: adı içerik karması taşıyan varlık uzun ömürle
  saklanır, adı sabit olan belge her kullanımda doğrulanır.
- İçerik dağıtımına devretme kararı istek isabet oranı ile bayt isabet oranına bakılarak
  verilir; ölçümde bu iki oran %49,9 ve %99,8 çıkmış, hangi kaynağın kıt olduğuna göre
  farklı sonuçlara işaret etmiştir.

#### Sonraki Adım

Buraya kadar sunulan her şey diskte hazır duruyordu: dosya okundu, başlıklar yazıldı,
baytlar gönderildi. Katalog sayfası ise içeriğini betiğe yaptırıyor — belge boş geliyor,
kitap listesi tarayıcıda kuruluyor. Aynı listeyi sunucunun kendisi belgeye yazabilir.
Bunun için veri ile işaretlemeyi birleştiren bir mekanizma gerekir ve bu mekanizmanın
merkezinde tek bir soru vardır: veriden gelen bir metin işaretlemenin içine konurken ne
olur? Sonraki ders bu birleştirmeyi kendi şablon değerlendiricisini yazarak kurar ve
kaçışlamanın çıktı üzerindeki etkisini ölçer.

### Sunucuda Şablon Oluşturma

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/sunucuda-sablon-olusturma

Görünümün sunucuda veriden üretilmesi: şablon değerlendiricisinin çözümleme ve üretim adımları, kaçışlamanın öntanımlı olması, ham çıktının açık işaretle istenmesi, öznitelik bağlamının farkı ve düzen ile sayfa şablonunun ayrılması.

Önceki ders diskteki dosyaları sundu. Katalog sayfası da o dosyalardan biriydi ama
içeriğini kendisi taşımıyordu: belge boş bir kap olarak gidiyor, kitap listesi tarayıcıda
betikle kuruluyordu. Sunucu kitap kayıtlarını zaten okuyabildiğine göre listeyi belgenin
içine kendisi yazabilir.

Bu birleştirmeyi yapan mekanizmaya **şablon** (template) denir: içine değer konacak yerleri
işaretlenmiş bir metin. Şablonu veriyle birleştiren programa **şablon değerlendiricisi**
(template engine) denir. Bu dersin merkezinde tek bir soru vardır: veriden gelen bir metin
işaretlemenin içine konurken ne olur?

#### Şablonun İki Adımı

Değerlendirici işini iki adımda yapar. Önce şablon metni **çözümlenir**: düz metin
parçaları ile yer tutucular birbirinden ayrılır ve blok etiketleri iç içe bir yapı kurar.
Sonra bu yapı veriyle gezilerek **üretilir**.

İki adımın ayrı olması bir gerekliliktir. Çözümleme şablona bakar ve veriye bakmaz;
üretim veriye bakar ve şablon metnini bir daha okumaz. Böylece aynı şablon bir kez
çözümlenip yüz bin kez üretilebilir ve daha önemlisi, **verinin içeriği hiçbir koşulda
şablon yapısını değiştiremez**. Veriden gelen bir metin bir blok etiketine benzese bile
üretim adımında etiket olarak yorumlanmaz, çünkü çözümleme çoktan bitmiştir.

Aşağıdaki değerlendirici dört etiket tanır: `{{ ad }}` değeri kaçışlayarak yazar,
`{{! ad }}` ham yazar, `{{# ad }}` ile `{{/ ad }}` arası bir dizi üzerinde yinelenir,
`{{^ ad }}` ile `{{/ ad }}` arası ise değer boşken çalışır.

```js
// sablon.mjs — kacislamayi ontanimli yapan kucuk bir sablon degerlendiricisi
const KACIS = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
export const kacisla = (deger) => String(deger).replace(/[&<>"']/g, (k) => KACIS[k]);

// {{ ad }} kacislar, {{! ad }} kacislamaz, {{# ad }}..{{/ ad }} yineler,
// {{^ ad }}..{{/ ad }} bos oldugunda calisir.
const ETIKET = /\{\{\s*([#^/!]?)\s*([\w.]+)\s*\}\}/g;

const coz = (yigit, ad) => {
  if (ad === ".") return yigit[yigit.length - 1];
  const parcalar = ad.split(".");
  for (let i = yigit.length - 1; i >= 0; i--) {
    let deger = yigit[i];
    if (deger === null || typeof deger !== "object") continue;
    if (!(parcalar[0] in deger)) continue;
    for (const p of parcalar) deger = deger?.[p];
    return deger;
  }
  return undefined;
};

// Sablonu tek geciste agaca cevirir: dizgi parcalari ve blok dugumleri.
const ayristir = (metin) => {
  const kok = { cocuk: [] };
  const yigit = [kok];
  let son = 0;
  for (const e of metin.matchAll(ETIKET)) {
    const [tam, tur, ad] = e;
    const ust = yigit[yigit.length - 1];
    if (e.index > son) ust.cocuk.push(metin.slice(son, e.index));
    son = e.index + tam.length;
    if (tur === "#" || tur === "^") {
      const dugum = { tur, ad, cocuk: [] };
      ust.cocuk.push(dugum);
      yigit.push(dugum);
    } else if (tur === "/") {
      if (yigit.length === 1 || yigit[yigit.length - 1].ad !== ad)
        throw new Error(`kapanmayan blok: ${ad}`);
      yigit.pop();
    } else {
      ust.cocuk.push({ tur, ad });
    }
  }
  if (yigit.length !== 1) throw new Error(`kapanmayan blok: ${yigit[yigit.length - 1].ad}`);
  kok.cocuk.push(metin.slice(son));
  return kok;
};

const uret = (dugum, yigit) =>
  dugum.cocuk.map((c) => {
    if (typeof c === "string") return c;
    const deger = coz(yigit, c.ad);
    if (c.tur === "#") {
      const liste = Array.isArray(deger) ? deger : deger ? [deger] : [];
      return liste.map((oge) => uret(c, [...yigit, oge])).join("");
    }
    if (c.tur === "^") {
      const bos = Array.isArray(deger) ? deger.length === 0 : !deger;
      return bos ? uret(c, yigit) : "";
    }
    if (deger === undefined || deger === null) return "";
    return c.tur === "!" ? String(deger) : kacisla(deger);
  }).join("");

export const olustur = (metin, veri) => uret(ayristir(metin), [veri]);
```

Ad çözümü bir kapsam yığıtı üzerinden yapılır: blok içine girildiğinde o turun ögesi
yığıta eklenir, ad önce en içteki kapsamda aranır, bulunamazsa dışarı doğru gidilir. Bu
sayede yineleme bloğunun içinden dıştaki alanlara da erişilebilir.

#### Kaçışlamanın Öntanımlı Olması

**Kaçışlama** (escaping), işaretleme dilinde yapısal anlamı olan karakterlerin
**karakter varlığı** (character entity) biçimine çevrilmesidir. Bu dönüşüm yapılırsa
veriden gelen metin çıktıda metin olarak kalır; yapılmazsa işaretlemenin parçası olur.

Değerlendiricide kaçışlama öntanımlıdır ve ham çıktı ancak `{{! ad }}` yazılarak, yani
şablonu yazan kişinin açık kararıyla elde edilir. Sıralamanın tersi de yazılabilirdi;
farkı, hangisinin unutulduğunda ne olduğudur. Kaçışlama öntanımlıysa unutmanın bedeli
görünürde bir tuhaflıktır. Ham çıktı öntanımlıysa unutmanın bedeli, veriden gelen metnin
belge yapısına karışmasıdır.

Aşağıdaki blok aynı veriyi iki yer tutucuya birden verir ve farkı gösterir. İşaret metni
zararsızdır: bir vurgulama ögesi.

```js
// dene.mjs — ayni veriyle kacislamali ve kacislamasiz uretimi karsilastirir
import { olustur } from "./sablon.mjs";

const SABLON = `<ul class="katalog">
{{# kitaplar }}  <li>{{ baslik }} — <span class="yazar">{{ yazar }}</span></li>
{{/ kitaplar }}{{^ kitaplar }}  <li class="bos">Sonuc yok</li>
{{/ kitaplar }}</ul>
<p class="not">{{ not }}</p>
<p class="not-ham">{{! not }}</p>`;

const VERI = {
  kitaplar: [
    { baslik: "Kayip Zamanin Izinde", yazar: "Marcel Proust" },
    { baslik: "Kar & Buz <arsiv>", yazar: "Sube 3" },
  ],
  not: "<mark>ayrilmis</mark>",
};

const cikti = olustur(SABLON, VERI);
console.log(cikti);
console.log("---");
console.log("cikti bayt:", Buffer.byteLength(cikti));
console.log("mark ogesi sayisi:", (cikti.match(/<mark>/g) ?? []).length);
console.log("--- bos liste ---");
console.log(olustur(SABLON, { kitaplar: [], not: "yok" }));
```

```
<ul class="katalog">
  <li>Kayip Zamanin Izinde — <span class="yazar">Marcel Proust</span></li>
  <li>Kar &amp; Buz &lt;arsiv&gt; — <span class="yazar">Sube 3</span></li>
</ul>
<p class="not">&lt;mark&gt;ayrilmis&lt;/mark&gt;</p>
<p class="not-ham"><mark>ayrilmis</mark></p>
---
cikti bayt: 278
mark ogesi sayisi: 1
--- bos liste ---
<ul class="katalog">
  <li class="bos">Sonuc yok</li>
</ul>
<p class="not">yok</p>
<p class="not-ham">yok</p>
```

Aynı veri iki farklı sonuç vermiştir. Kaçışlanan yer tutucuda işaret metni **ekranda
okunacak bir metindir**; ham yer tutucuda **belgeye eklenmiş bir ögedir**. Sayım bunu
doğrular: çıktıda bir tek vurgulama ögesi vardır ve o da ham yazılan yerden gelmiştir.

Kitap başlığındaki `&` ve açılı ayraçlar da kaçışlanmıştır. Bu, güvenlikten önce bir
doğruluk meselesidir: kaçışlanmasaydı başlığın bir bölümü ekranda hiç görünmezdi, çünkü
işaretleme çözümleyicisi onu etiket başlangıcı sayardı.

#### Metin Bağlamı ile Öznitelik Bağlamı

Tek bir kaçışlama işlevi, değerin belgenin neresine konduğunu bilmez. Metin içine konan
bir değerde açılı ayraçlar önemlidir; öznitelik değerine konan bir değerde **tırnak**
önemlidir. Yukarıdaki işlev her ikisini de dönüştürdüğü için iki bağlamda da doğru sonuç
verir, ama bunun neden gerekli olduğu ayrıca gösterilmelidir.

```js
// oznitelik.mjs — kacislamanin oznitelik degeri uzerindeki etkisi
import { olustur } from "./sablon.mjs";

const KACISLAMALI = `<a class="sube" title="{{ ad }}" href="/sube/{{ kod }}">Sube</a>`;
const HAM = `<a class="sube" title="{{! ad }}" href="/sube/{{ kod }}">Sube</a>`;
const VERI = { ad: 'Merkez "Eski Kitaplar" Salonu', kod: "s3" };

// Cozumleyici oznitelik degerini ilk kapanis tirnaginda bitirir; asagidaki desen
// ayni kurali uygular ve degerin nerede kesildigini gosterir.
const baslikDegeri = (metin) => metin.match(/title="([^"]*)"/)[1];

for (const [ad, sablon] of [["kacislamali", KACISLAMALI], ["ham", HAM]]) {
  const cikti = olustur(sablon, VERI);
  console.log(ad);
  console.log("  cikti :", cikti);
  console.log("  title :", JSON.stringify(baslikDegeri(cikti)));
}
```

```
kacislamali
  cikti : <a class="sube" title="Merkez &quot;Eski Kitaplar&quot; Salonu" href="/sube/s3">Sube</a>
  title : "Merkez &quot;Eski Kitaplar&quot; Salonu"
ham
  cikti : <a class="sube" title="Merkez "Eski Kitaplar" Salonu" href="/sube/s3">Sube</a>
  title : "Merkez "
```

Ham çıktıda öznitelik değeri **"Merkez "** ile bitmiştir: şube adındaki tırnak,
özniteliği kapatmıştır. Geri kalan sözcükler artık değerin parçası değil, etiketin
gövdesindeki başka bir şeydir. Kaçışlanan çıktıda ise değer bütün olarak durur ve
çözümleyici karakter varlıklarını okurken şube adını eksiksiz geri verir.

Buradan çıkan kural şudur: öznitelik değerleri **her zaman tırnak içine alınır** ve
şablona konan değer bu tırnağı kapatabilecek karakterlerden arındırılır. Tırnaksız
öznitelik yazımı, kaçışlama işlevinin dönüştürmediği boşluk gibi karakterleri de
yapısal hâle getirdiği için bu düzende hiç kullanılmaz.

#### Düzen ve Sayfa

Her sayfanın belge iskeleti aynıdır: aynı dil bildirimi, aynı başlık ögesi, aynı gövde
çerçevesi. Bu ortak kısmı her şablona kopyalamak yerine bir **düzen şablonu** yazılır ve
sayfanın kendi çıktısı ona yerleştirilir. Yerleştirme, düzenin içindeki tek ham yer
tutucuyla yapılır — çünkü oraya konan şey veri değil, zaten kaçışlanmış bir üretim
çıktısıdır.

Şablonun gördüğü veri yapısına **görünüm modeli** (view model) denir. Görünüm modeli, veri
kaynağıyla şablon arasındaki sözleşmedir: şablon kayıtların nereden geldiğini bilmez,
kayıtları okuyan kod da işaretlemeyi bilmez.

```js
// gorunum.mjs — katalog sayfasini sunucuda uretip sunar (sablon.mjs ile ayni dizinde)
import { createServer } from "node:http";
import { olustur } from "./sablon.mjs";

const KITAPLAR = [
  { baslik: "Kayip Zamanin Izinde", yazar: "Marcel Proust", sube: "Merkez" },
  { baslik: "Tutunamayanlar", yazar: "Oguz Atay", sube: "Sube 3" },
  { baslik: "Kar & Buz <arsiv>", yazar: "Derleme", sube: "Sube 3" },
];

const DUZEN = `<!doctype html>
<html lang="tr"><head><meta charset="utf-8"><title>{{ baslik }}</title></head>
<body><h1>{{ baslik }}</h1>{{! icerik }}</body></html>
`;

const KATALOG = `<p class="ozet">{{ sayi }} kayit</p>
<ul class="katalog">
{{# kitaplar }}  <li>{{ baslik }} — <span class="yazar">{{ yazar }}</span> ({{ sube }})</li>
{{/ kitaplar }}{{^ kitaplar }}  <li class="bos">Sonuc yok</li>
{{/ kitaplar }}</ul>`;

// Gorunum modeli: sablonun gordugu tek veri yapisi. Sorgu ve kayitlar burada
// birlestirilir; sablonun kendisi veri kaynagini hic bilmez.
const gorunumModeli = (sorgu) => {
  const q = (sorgu ?? "").toLocaleLowerCase("tr");
  const kitaplar = q ? KITAPLAR.filter((k) => k.baslik.toLocaleLowerCase("tr").includes(q)) : KITAPLAR;
  return { kitaplar, sayi: kitaplar.length };
};

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");
  if (adres.pathname !== "/katalog") return yanit.writeHead(404).end();

  const icerik = olustur(KATALOG, gorunumModeli(adres.searchParams.get("q")));
  const belge = olustur(DUZEN, { baslik: "Kutuphane Katalogu", icerik });

  yanit.setHeader("Content-Type", "text/html; charset=utf-8");
  yanit.setHeader("Content-Length", Buffer.byteLength(belge));
  yanit.writeHead(200).end(belge);
}).listen(8311, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8311"));
```

```bash
#!/usr/bin/env bash
# gorunum.mjs'yi baslatir, uretilen belgeyi ve suzulmus sonucu gosterir, durdurur.
node gorunum.mjs > /dev/null &
sunucu=$!
sleep 1
A=http://127.0.0.1:8311

echo "--- tum katalog ---"
curl -sS "$A/katalog"
echo "--- suzgec: q=kar ---"
curl -sS "$A/katalog?q=kar"
echo "--- eslesmeyen sorgu ---"
curl -sS "$A/katalog?q=zzz" | grep -c 'class="bos"'

kill "$sunucu"
```

```
--- tum katalog ---
<!doctype html>
<html lang="tr"><head><meta charset="utf-8"><title>Kutuphane Katalogu</title></head>
<body><h1>Kutuphane Katalogu</h1><p class="ozet">3 kayit</p>
<ul class="katalog">
  <li>Kayip Zamanin Izinde — <span class="yazar">Marcel Proust</span> (Merkez)</li>
  <li>Tutunamayanlar — <span class="yazar">Oguz Atay</span> (Sube 3)</li>
  <li>Kar &amp; Buz &lt;arsiv&gt; — <span class="yazar">Derleme</span> (Sube 3)</li>
</ul></body></html>
--- suzgec: q=kar ---
<!doctype html>
<html lang="tr"><head><meta charset="utf-8"><title>Kutuphane Katalogu</title></head>
<body><h1>Kutuphane Katalogu</h1><p class="ozet">1 kayit</p>
<ul class="katalog">
  <li>Kar &amp; Buz &lt;arsiv&gt; — <span class="yazar">Derleme</span> (Sube 3)</li>
</ul></body></html>
```

Boş sonuç dalı ayrıca sınanmıştır: eşleşmeyen sorguda `class="bos"` geçen satır sayısı
birdir. Bu, boş listenin şablonda ele alınmış olduğunu doğrular; ele alınmasaydı çıktıda
gövdesiz bir liste ögesi kalırdı.

Bağlantı noktası 8311 keyfîdir ve boş olmalıdır.

#### Şablonun Taşıyamayacağı Yük

Şablon bir görünüm aracıdır ve yükü arttıkça bozulur. Üç sınır pratikte belirleyicidir.

**Hesap şablonda yapılmaz.** Gecikmiş ödünç işleminin gün sayısı, ceza tutarı ya da
sıralama ölçütü görünüm modelinde hesaplanır. Şablonda hesap yapılırsa aynı kural iki
sayfada iki farklı sonuç vermeye başlar ve sınanamaz hâle gelir.

**Veri erişimi şablonda yapılmaz.** Yineleme bloğunun içinden bir sorgu çalıştırılabilseydi
sayfadaki satır sayısı kadar sorgu doğardı. Görünüm modeli, üretim başlamadan önce ihtiyaç
duyulan her şeyi hazır getirir.

**Ham çıktı sayılabilir olmalıdır.** Ham yer tutucu, sistemde denetlenmesi gereken tek
noktadır. Sayısı azaldıkça ve yalnızca üretim çıktısı yerleştirilen yerlerde kaldıkça
şablon katmanının doğruluğu gözle sınanabilir kalır.

#### Özet

- Şablon değerlendiricisi önce şablonu çözümler, sonra veriyle üretir; verinin içeriği
  çözümleme bittikten sonra geldiği için şablon yapısını değiştiremez.
- Kaçışlama, işaretlemede yapısal anlamı olan karakterleri karakter varlığına çevirir ve
  öntanımlı olmalıdır; ham çıktı şablonu yazanın açık kararıyla istenir.
- Ölçüm iki bağlamı ayırır: metin bağlamında kaçışlanmayan değer belgeye öge ekler,
  öznitelik bağlamında ise değeri ilk tırnakta keser.
- Düzen şablonu ortak belge iskeletini taşır; sayfa çıktısı oraya tek bir ham yer
  tutucuyla yerleşir, çünkü yerleşen şey veri değil zaten üretilmiş çıktıdır.
- Görünüm modeli şablon ile veri kaynağı arasındaki sözleşmedir; hesap ve veri erişimi
  şablonda değil, görünüm modelini kuran kodda yapılır.

#### Sonraki Adım

Şimdiye kadar veri hep içeriden geldi: diskteki dosyalar, bellekteki kayıtlar. Kütüphane
servisinin gerçek işleyişinde dışarıdan da veri gelir — üye fotoğrafı, bağış yapılan
kitabın kapak görüntüsü, şubeden yüklenen sayım dosyası. Bu içerik sunucuya gelirken
boyutu bilinmez, türü bildirildiği gibi olmayabilir ve nereye yazılacağı bir karardır.
Sonraki ders yüklemeyi bu üç soruyla ele alır: sunucu sınırı aşan bir yükü ne zaman ve
nasıl reddeder, dosyanın türüne uzantısına bakmadan nasıl karar verir ve yazılan dosya
sunulan dizinden neden ayrı tutulur?

### Dosya Yükleme ve Depolama

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/dosya-yukleme-ve-depolama

Dışarıdan gelen dosyanın sınırlarla kabul edilmesi: bildirilen uzunluk ile akış sayacının iki katmanlı boyut sınırı, reddin karşı tarafa ulaştırılması, türün uzantı yerine içerikten saptanması, depo dizininin sunulan dizinden ayrılması ve dosya adının sunucuda üretilmesi.

Önceki iki ders sunucudan dışarı akan veriyi ele aldı: diskteki dosya, şablondan üretilen
belge. Kütüphane servisinin işleyişinde ters yönde de veri akar. Bağışlanan kitabın kapak
görüntüsü, üye kaydına eklenen fotoğraf, şubeden gelen sayım dosyası sunucuya dışarıdan
gelir.

Dışarıdan gelen içeriğin üç özelliği, sunucudan çıkan içerikten ayrılır: boyutu önceden
bilinmez, türü bildirildiği gibi olmayabilir ve nereye yazılacağı sunucunun kararıdır. Bu
ders yükleme ucunu bu üç sorunun cevabı olarak kurar. Ele alınan her kalem bir
**savunmadır**: sunucunun kabul etmeden önce uyguladığı bir kural.

#### Boyut Sınırı İki Katmanlıdır

Bir yükleme gövdesinin boyutu iki ayrı yerden öğrenilebilir ve ikisi de tek başına
yetmez.

Birincisi **bildirilen uzunluktur**: istek başlığındaki içerik uzunluğu değeri. Bu değer
gövde hiç okunmadan görülür, dolayısıyla sınırın üstündeki bir yükleme daha ilk baytı
belleğe alınmadan reddedilebilir. Ucuz ve erken bir savunmadır.

İkincisi **akış sayacıdır**: gövde okunurken gerçekte gelen baytların sayılması.
Bildirilen uzunluk isteğin bir parçasıdır ve isteği kuran taraf onu göndermeyebilir ya da
yanlış gönderebilir; parçalı aktarımda başlık hiç bulunmaz. Sayaç, bildirime bakmaksızın
sınırı uygular.

İki katman birlikte çalışır: bildirim erken reddi mümkün kılar, sayaç reddin
atlanamamasını sağlar. Yalnız birincisi varsa sınır bildirilmediğinde kaybolur; yalnız
ikincisi varsa büyük bir yük her seferinde sınıra kadar okunur.

#### Reddi Karşı Tarafa Ulaştırmak

Sınırı aşan bir isteğin bağlantısını anında kapatmak akla yakın gelir ama sonucu
beklenmediktir: karşı taraf gövdeyi yazmayı sürdürdüğü için bağlantı hata ile kopar ve
sunucunun yazdığı yanıt hiç okunmaz. Gönderen tarafta görünen şey "bağlantı sıfırlandı"
olur; sınırın aşıldığı, sınırın ne olduğu ve isteğin nasıl düzeltileceği kaybolur.

Reddin okunabilmesi için sunucunun gelen baytları **boşaltmaya** (draining) devam etmesi
gerekir: biriktirmeden, ama okuyarak. Boşaltmanın da bir üst sınırı olur; o sınır da
aşıldığında bağlantı düşürülür, çünkü sonsuza kadar okumak reddin anlamını ortadan
kaldırır.

Bu, yükleme ucunun en kolay atlanan ayrıntısıdır ve ölçümde doğrudan görünür: boşaltma
yoksa istemci 413 yanıtı yerine bir bağlantı hatası görür.

#### Tür Uzantıdan Değil İçerikten Saptanır

Yüklenen dosyanın türü hakkında sunucuya iki bilgi ulaşır: istekteki içerik türü başlığı
ve dosya adının uzantısı. İkisi de gönderen tarafın yazdığı metinlerdir; içerikle
uyuşmaları zorunlu değildir. Uyuşmadıklarında ortaya çıkan durum bir saldırıdan önce bir
tutarsızlıktır: kayıt görüntü olarak işaretlenir, görüntü olarak sunulur, görüntü
değildir.

Güvenilir kaynak içeriğin kendisidir. Yaygın ikili biçimler dosyanın başında sabit bir
bayt dizisi taşır; bu diziye **imza** (magic number) denir. Sunucu kabul ettiği türlerin
imzalarını bilir ve gövdenin ilk baytlarını bu listeyle karşılaştırır. Eşleşme yoksa
yükleme reddedilir.

İmza denetimi bir izin listesidir: sunucu neyi kabul ettiğini sayar, neyi reddettiğini
değil. Liste dışındaki her şey, tanınmadığı için reddedilir. Ters yönde kurulan bir
kural — belli türleri yasaklamak — listeye girmeyen her yeni biçimi sessizce kabul eder.

İmza denetiminin sınırı da açıkça bilinmelidir: imza dosyanın biçimini gösterir, geri
kalanının geçerli olduğunu göstermez. Görüntülerin boyutu ve içeriği ayrıca sınanacaksa
bu iş biçimi çözen bir adımın işidir.

#### Depolama Yeri ve Üretilen Ad

Yüklenen dosya nereye yazılır sorusunun cevabı, ilk dersteki kök dizin kararının
devamıdır. Statik sunucu kök dizinin altındaki her dosyayı sunar. Yüklenen içerik o
dizine yazılırsa, dışarıdan gelen bayt dizileri sunucunun sunum alanına doğrudan eklenmiş
olur. Bu yüzden **depo dizini sunulan dizinden ayrı tutulur** ve yüklenen içerik ancak
uygulamanın kendi ucundan, kaydı bulunarak sunulur.

Dosya adı da dışarıdan alınmaz. Gönderilen ad yalnızca kullanıcıya gösterilecek bir üstveri
olarak saklanır; diske yazılan ad sunucuda **üretilir**. Üretilen adın üç özelliği vardır:
çakışmaz, dizin yapısında anlam taşımaz ve uzantısı gönderilen addan değil **saptanan
türden** gelir. Böylece hem eşzamanlı iki yüklemenin birbirini ezmesi, hem de adın dosya
yolu olarak yorumlanması engellenmiş olur.

Sunum tarafında iki başlık daha eklenir. İçerik türü kayıttan okunur, bildirilenden değil;
ve tür tahmini kapatılır, böylece alıcı taraf bildirilen türün dışına çıkarak içeriği
başka bir şey sanmaz.

#### Uç Noktanın Tamamı

Aşağıdaki uç, tarayıcı formlarının kullandığı **çok parçalı form verisi**
(multipart/form-data) kodlamasını çözmek yerine gövdeyi doğrudan dosya baytları olarak
alır. Bu, sınır mantığını parça ayrıştırmasının arkasına saklamamak içindir; çok parçalı
kodlamada aynı üç savunma her parça için ayrı ayrı uygulanır.

```js
// yukleme.mjs — kapak gorseli yukleme ucu: boyut siniri, icerik denetimi, ayri depo
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
import { mkdir, writeFile, readFile } from "node:fs/promises";
import { join } from "node:path";

const SINIR = 64 * 1024;          // kabul edilen en buyuk kapak gorseli
const BOSALTMA = 1024 * 1024;     // reddedilen govdeden okunmaya devam edilecek en fazla bayt
const DEPO = join(process.cwd(), "veri", "kapaklar"); // sunulan dizinin disinda
const kayitlar = new Map();       // uretilen ad -> { tur, bayt }

// Tur, bildirilen basliktan ya da dosya adindan degil, govdenin ilk baytlarindan belirlenir.
const IMZALAR = [
  { uzanti: "png", tur: "image/png", imza: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },
  { uzanti: "jpg", tur: "image/jpeg", imza: [0xff, 0xd8, 0xff] },
  { uzanti: "gif", tur: "image/gif", imza: [0x47, 0x49, 0x46, 0x38] },
];
const turBelirle = (govde) =>
  IMZALAR.find((k) => k.imza.every((b, i) => govde[i] === b)) ?? null;

// Govdeyi okur. Sinir asilirsa baytlar biriktirilmez ama akis bosaltma sinirina kadar
// okunmaya devam eder: bosaltilmadan yazilan yanit istemciye ulasmadan baglanti kirilir.
const govdeyiOku = (istek, pesinenRed) =>
  new Promise((coz) => {
    const parcalar = [];
    let toplam = 0;
    let asildi = pesinenRed;
    istek.on("data", (parca) => {
      toplam += parca.length;
      if (!asildi && toplam > SINIR) { asildi = true; parcalar.length = 0; }
      if (asildi) { if (toplam > BOSALTMA) istek.destroy(); return; }
      parcalar.push(parca);
    });
    istek.on("end", () => coz(asildi ? null : Buffer.concat(parcalar)));
    istek.on("error", () => coz(null));
    istek.on("close", () => coz(null));
  });

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);
};

await mkdir(DEPO, { recursive: true });

createServer(async (istek, yanit) => {
  yanit.sendDate = false;

  if (istek.method === "POST" && istek.url === "/kapak") {
    // 1. Savunma: bildirilen uzunluk sinirin ustundeyse govde hic biriktirilmez.
    // 2. Savunma: gercekte gelen bayt sayilir; bildirim yanlissa da sinir uygulanir.
    const bildirilen = Number(istek.headers["content-length"] ?? 0);
    const asama = bildirilen > SINIR ? "bildirilen_uzunluk" : "akis_sayaci";
    const govde = await govdeyiOku(istek, bildirilen > SINIR);
    if (govde === null)
      return json(yanit, 413, { hata: "sinir_asildi", asama, sinir: SINIR });

    // 3. Savunma: tur, gonderilen baslik ve dosya adi degil, icerik belirler.
    const tur = turBelirle(govde);
    if (!tur)
      return json(yanit, 415, {
        hata: "tur_kabul_edilmiyor",
        bildirilen: istek.headers["content-type"] ?? null,
      });

    // 4. Savunma: ad sunucuda uretilir; uzanti saptanan turden gelir.
    const ad = `${randomUUID()}.${tur.uzanti}`;
    await writeFile(join(DEPO, ad), govde);
    kayitlar.set(ad, { tur: tur.tur, bayt: govde.length });

    yanit.setHeader("Location", `/kapak/${ad}`);
    return json(yanit, 201, { ad, tur: tur.tur, bayt: govde.length });
  }

  // 5. Savunma: sunum kayitli adlar uzerinden yapilir; istek yolu dosya yoluna donusmez.
  if (istek.method === "GET" && istek.url.startsWith("/kapak/")) {
    const ad = istek.url.slice("/kapak/".length);
    const kayit = kayitlar.get(ad);
    if (!kayit) return json(yanit, 404, { hata: "bulunamadi" });
    const govde = await readFile(join(DEPO, ad));
    yanit.setHeader("Content-Type", kayit.tur);
    yanit.setHeader("Content-Length", govde.length);
    yanit.setHeader("Content-Disposition", "inline");
    yanit.setHeader("X-Content-Type-Options", "nosniff");
    return yanit.writeHead(200).end(govde);
  }

  return json(yanit, 404, { hata: "bulunamadi" });
}).listen(8312, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8312"));
```

Kayıtların bellekte tutulması yalnızca bu dersin kapsamı içindir; sunucu yeniden
başladığında kayıtlar kaybolur ve diskteki dosyalar erişilemez kalır. Üstverinin kalıcı
bir depoya yazılması, veri erişimi kursunun konusudur.

#### Ölçüm

Ölçüm üç dosya kullanır: geçerli bir görüntü, adı görüntü uzantısıyla biten bir metin
dosyası ve sınırın üstünde bir dosya. Aşağıdaki blok üçünü de üretir.

```js
// ornek-uret.mjs — deneme icin uc dosya uretir: gecerli PNG, PNG uzantili metin, buyuk PNG
import { crc32, deflateSync } from "node:zlib";
import { statSync, writeFileSync } from "node:fs";

const parca = (tip, veri) => {
  const uzunluk = Buffer.alloc(4);
  uzunluk.writeUInt32BE(veri.length);
  const govde = Buffer.concat([Buffer.from(tip, "ascii"), veri]);
  const sag = Buffer.alloc(4);
  sag.writeUInt32BE(crc32(govde) >>> 0);
  return Buffer.concat([uzunluk, govde, sag]);
};

const png = (en, boy) => {
  const ihdr = Buffer.alloc(13);
  ihdr.writeUInt32BE(en, 0); ihdr.writeUInt32BE(boy, 4);
  ihdr[8] = 8; ihdr[9] = 2; // 8 bit derinlik, dogru renk
  const satir = Buffer.alloc(1 + en * 3); // suzgec bayti + RGB
  const ham = Buffer.concat(Array.from({ length: boy }, () => satir));
  return Buffer.concat([
    Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
    parca("IHDR", ihdr),
    parca("IDAT", deflateSync(ham)),
    parca("IEND", Buffer.alloc(0)),
  ]);
};

writeFileSync("kapak.png", png(8, 8));
writeFileSync("sahte.png", Buffer.from("Bu bir metin dosyasidir, adi .png ile bitiyor.\n"));
writeFileSync("buyuk.png", Buffer.concat([png(1200, 1200), Buffer.alloc(120 * 1024, 0x41)]));
for (const a of ["kapak.png", "sahte.png", "buyuk.png"])
  console.log(a, statSync(a).size, "B");
```

```bash
#!/usr/bin/env bash
# yukleme.mjs'yi baslatir, dort yukleme denemesini sinar, depoyu listeler, durdurur.
rm -rf veri
node ornek-uret.mjs
node yukleme.mjs > /dev/null &
sunucu=$!
sleep 1
A=http://127.0.0.1:8312

echo "--- 1) gecerli kapak (69 B) ---"
AD=$(curl -sS -X POST --data-binary @kapak.png -H 'Content-Type: image/png' "$A/kapak" \
     | tee /dev/stderr | sed 's/.*"ad":"\([^"]*\)".*/\1/')
echo
echo "--- 2) yuklenen kapagin sunulmasi ---"
curl -sS -D - -o /dev/null "$A/kapak/$AD"

echo "--- 3) .png adli metin dosyasi, image/png bildirilerek ---"
curl -sS -o - -w '  <- HTTP %{http_code}\n' -X POST --data-binary @sahte.png \
     -H 'Content-Type: image/png' "$A/kapak"

echo "--- 4) sinirin ustunde, uzunluk bildirerek ---"
curl -sS -o - -w '  <- HTTP %{http_code}\n' -X POST --data-binary @buyuk.png \
     -H 'Content-Type: image/png' "$A/kapak"

echo "--- 5) sinirin ustunde, uzunluk bildirmeden (parcali aktarim) ---"
curl -sS -o - -w '  <- HTTP %{http_code}\n' -X POST --data-binary @buyuk.png \
     -H 'Content-Type: image/png' -H 'Transfer-Encoding: chunked' "$A/kapak"

echo "--- depoya yazilanlar ---"
ls veri/kapaklar | sed 's/^/  /'
echo "  toplam dosya: $(ls veri/kapaklar | wc -l | tr -d ' ')"

kill "$sunucu"
```

```
kapak.png 69 B
sahte.png 47 B
buyuk.png 127159 B
--- 1) gecerli kapak (69 B) ---
{"ad":"158bb07e-446a-49bb-b053-4cd61d4500f6.png","tur":"image/png","bayt":69}
--- 2) yuklenen kapagin sunulmasi ---
HTTP/1.1 200 OK
Content-Type: image/png
Content-Length: 69
Content-Disposition: inline
X-Content-Type-Options: nosniff
Connection: keep-alive
Keep-Alive: timeout=5

--- 3) .png adli metin dosyasi, image/png bildirilerek ---
{"hata":"tur_kabul_edilmiyor","bildirilen":"image/png"}  <- HTTP 415
--- 4) sinirin ustunde, uzunluk bildirerek ---
{"hata":"sinir_asildi","asama":"bildirilen_uzunluk","sinir":65536}  <- HTTP 413
--- 5) sinirin ustunde, uzunluk bildirmeden (parcali aktarim) ---
{"hata":"sinir_asildi","asama":"akis_sayaci","sinir":65536}  <- HTTP 413
--- depoya yazilanlar ---
  158bb07e-446a-49bb-b053-4cd61d4500f6.png
  toplam dosya: 1
```

Üretilen ad her çalıştırmada değişir; bağlantı başlıkları çalışma zamanının varsayılan
ayarına bağlıdır. Bağlantı noktası 8312 keyfîdir ve boş olmalıdır.

Dört sonuç okunur. **Tür denetimi bildirime bakmamıştır**: üçüncü denemede istek türü
görüntü olarak bildirilmiş, içerik metin olduğu için yükleme 415 ile reddedilmiştir.
**İki sınır katmanı da ayrı ayrı çalışmıştır**: dördüncü denemede ret bildirilen
uzunlukta, beşinci denemede — uzunluk hiç bildirilmediği için — akış sayacında olmuştur.
Yanıttaki aşama alanı hangi katmanın devreye girdiğini söyler. **Reddedilen hiçbir yükleme
diske yazılmamıştır**: iki yüz kilobayta yakın veri gönderilmiş olmasına karşın depoda tek
dosya vardır ve o da geçerli kapaktır.

#### Özet

- Boyut sınırı iki katmandan oluşur: bildirilen uzunluk erken ve ucuz reddi, akış sayacı
  ise bildirimin yanlış ya da eksik olduğu durumda sınırın uygulanmasını sağlar.
- Sınırı aşan istek anında kesilirse yazılan yanıt karşı tarafa ulaşmaz; ret, gelen
  baytlar kendi üst sınırına kadar boşaltılarak bildirilir.
- Dosya türü bildirilen başlıktan ya da uzantıdan değil, gövdenin baş kısmındaki imzadan
  saptanır ve kabul edilen türler bir izin listesiyle sayılır.
- Depo dizini statik sunumun kök dizininden ayrı tutulur; yüklenen içerik ancak kaydı
  bulunarak, uygulamanın kendi ucundan sunulur.
- Diske yazılan ad sunucuda üretilir ve uzantısı saptanan türden gelir; gönderilen ad
  yalnızca gösterilecek bir üstveri olarak saklanır.

#### Sonraki Adım

Bu ders sunucunun diske yazdığı ilk kalıcı veriyi ortaya çıkardı ve bir eksiği de görünür
kıldı: kayıtlar bellekte durduğu için sunucu yeniden başladığında kaybolmakta. Gerçek
kurulumda o kayıtlar bir veritabanında, kapak dosyaları bir nesne deposunda, arama ise
ayrı bir serviste durur. Uygulama artık tek başına çalışan bir süreç değildir; yanında
başka servisler ister. Kursun son dersi bu bağımlılıkları geliştiricinin makinesinde
ayağa kaldırma sorusunu ele alır: servisler hangi sırayla başlar, uygulama onların hazır
olduğunu nasıl anlar ve aynı ortam iki makinede nasıl birebir aynı kurulur?

### Yerel Geliştirme Ortamı

Source: https://academia.sh/tr/kurslar/sunucu-temelleri/yerel-gelistirme-ortami

Bağımlı servislerin geliştiricinin makinesinde ayağa kaldırılması: başlatma sırasının zincir oluşturması, hazır olma denetiminin bağlantı denemesi ve geri çekilmeyle kurulması, sabit beklemenin neden bir tahmin olduğu ve aynı ortamın iki makinede sürüm sabitleme ile tohum verisi kurallarından yeniden üretilmesi.

Önceki ders sunucunun diske yazdığı ilk kalıcı veriyi ortaya çıkardı ve kayıtların bellekte
durmasının bir eksik olduğunu söyledi. Gerçek kurulumda o kayıtlar bir veritabanında,
arama ise ayrı bir serviste durur. Kütüphane ödünç uygulaması artık tek başına çalışan bir
süreç değildir: yanında **bağımlı servisler** (dependent services) ister.

Bu, geliştiricinin makinesinde yeni bir soruna yol açar. Uygulamayı çalıştırmak için önce
başka programları çalıştırmak gerekir, onların da kendi hazırlık işleri vardır ve
uygulamanın onlara ne zaman güvenebileceği belli değildir. Bu ders üç soruyu yanıtlar:
servisler hangi sırayla başlar, bir servisin hazır olduğu nasıl anlaşılır ve aynı ortam iki
ayrı makinede nasıl birebir aynı kurulur?

#### Bağımlılıklar Bir Zincir Kurar

Yerel ortamın kararları tek bir dosyada toplanır. Bu dosya bir **ortam bildirimi**
(environment manifest) olarak çalışır: bağlantı noktaları, şema sürümü, çalışma zamanının
en az sürümü ve tohum verisi kümeleri.

```json
{
  "calisma_zamani": { "ad": "node", "en_az_ana_surum": 22 },
  "sema_surumu": 3,
  "tohum_kumesi": "kucuk",
  "tohum_satir": { "kucuk": 150000, "buyuk": 1000000 },
  "baglanti_noktalari": { "veritabani": 8431, "arama": 8432, "uygulama": 8433 }
}
```

İlk bağımlılık katalog verisini tutan servistir. Davranışındaki ayrıntı bu dersin
merkezidir: bağlantı noktasını hemen açar, ama şema ve tohum verisi yazılmadan sorgu kabul
etmez. Ayrıca kendi durumunu bildiren bir **sağlık ucu** (health endpoint) sunar.

```js
// veritabani.mjs — katalog verisini tutan servis; hazir olmadan sorgu kabul etmez
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";
import { readFileSync, rmSync } from "node:fs";

const ortam = JSON.parse(readFileSync("ortam.json", "utf8"));
const kume = process.env.TOHUM_KUMESI ?? ortam.tohum_kumesi;
const satir = ortam.tohum_satir[kume];
const kapi = ortam.baglanti_noktalari.veritabani;

rmSync("kutuphane.db", { force: true });
const db = new DatabaseSync("kutuphane.db");
let hazir = false;

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);
};

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");
  if (adres.pathname === "/saglik")
    return json(yanit, hazir ? 200 : 503, { hazir, sema: ortam.sema_surumu });
  if (!hazir) return json(yanit, 503, { hata: "hazir_degil" });
  if (adres.pathname === "/kitaplar") {
    const yazar = adres.searchParams.get("yazar") ?? "Yazar 42";
    const satirlar = db.prepare(
      "SELECT isbn, ad, yazar FROM kitap WHERE yazar = ? ORDER BY isbn").all(yazar);
    return json(yanit, 200, { sayi: satirlar.length, kitaplar: satirlar });
  }
  return json(yanit, 404, { hata: "bulunamadi" });
}).listen(kapi, "127.0.0.1", () => {
  console.log(`veritabani :${kapi} acildi (hazir degil)`);
  // Hazirlik isi baglanti noktasi acildiktan sonra, parcalar halinde yurur: boylece
  // hazir olmayan sunucu istekleri yanitsiz birakmaz, durumunu bildiren bir yanit dondurur.
  const basladi = Date.now();
  db.exec("CREATE TABLE kitap (isbn TEXT PRIMARY KEY, ad TEXT NOT NULL, yazar TEXT NOT NULL)");
  const ekle = db.prepare("INSERT INTO kitap (isbn, ad, yazar) VALUES (?, ?, ?)");
  db.exec("BEGIN");
  let yazilan = 0;
  const parcaYaz = () => {
    for (const son = Math.min(yazilan + 10000, satir); yazilan < son; yazilan++)
      ekle.run(`978-${String(yazilan).padStart(10, "0")}`,
               `Kitap ${yazilan}`, `Yazar ${yazilan % 500}`);
    if (yazilan < satir) return setImmediate(parcaYaz);
    db.exec("COMMIT");
    db.exec("CREATE INDEX ix_yazar ON kitap(yazar)");
    hazir = true;
    console.log(`veritabani hazir: ${kume} tohum, ${satir} satir, ${Date.now() - basladi} ms`);
  };
  setImmediate(parcaYaz);
});
```

İkinci bağımlılık arama servisidir ve zinciri kuran şey onun açılışıdır: dizinini
veritabanından okuyarak kurar, bu yüzden veritabanı hazır değilse hiç başlayamaz.

```js
// arama.mjs — ikinci HTTP servisi; dizinini acilista veritabanindan okur
import { createServer } from "node:http";
import { readFileSync } from "node:fs";

const ortam = JSON.parse(readFileSync("ortam.json", "utf8"));
const { veritabani, arama } = ortam.baglanti_noktalari;

const yanit = await fetch(`http://127.0.0.1:${veritabani}/kitaplar?yazar=Yazar%2042`)
  .catch((e) => { console.error(`arama: veritabanina ulasilamadi (${e.cause?.code ?? e.name})`);
                  process.exit(1); });
if (!yanit.ok) { console.error(`arama: veritabani ${yanit.status} verdi, dizin kurulamadi`);
                 process.exit(1); }

const { kitaplar } = await yanit.json();
const dizin = new Map(kitaplar.map((k) => [k.ad.toLowerCase(), k.isbn]));

createServer((istek, cikti) => {
  cikti.sendDate = false;
  const terim = new URL(istek.url, "http://yerel").searchParams.get("q")?.toLowerCase() ?? "";
  const bulunan = [...dizin].filter(([ad]) => ad.includes(terim)).map(([ad, isbn]) => ({ ad, isbn }));
  const govde = JSON.stringify({ dizinBoyu: dizin.size, bulunan });
  cikti.setHeader("Content-Type", "application/json; charset=utf-8");
  cikti.setHeader("Content-Length", Buffer.byteLength(govde));
  cikti.writeHead(200).end(govde);
}).listen(arama, "127.0.0.1", () => console.log(`arama :${arama} hazir, dizin ${dizin.size} kayit`));
```

Zincirin son halkası, ikisini birden kullanan ödünç uygulamasıdır.

```js
// uygulama.mjs — odunc uygulamasi; iki bagimli servisi de kullanir
import { createServer } from "node:http";
import { readFileSync } from "node:fs";

const ortam = JSON.parse(readFileSync("ortam.json", "utf8"));
const { veritabani, arama, uygulama } = ortam.baglanti_noktalari;

createServer(async (istek, cikti) => {
  cikti.sendDate = false;
  const q = new URL(istek.url, "http://yerel").searchParams.get("q") ?? "kitap 42";
  let kod = 200, govde;
  try {
    const a = await fetch(`http://127.0.0.1:${arama}/?q=${encodeURIComponent(q)}`).then((r) => r.json());
    const v = await fetch(`http://127.0.0.1:${veritabani}/saglik`).then((r) => r.json());
    govde = { sorgu: q, eslesme: a.bulunan.length, dizinBoyu: a.dizinBoyu, veritabani: v };
  } catch (e) {
    kod = 503;
    govde = { hata: "bagimli_servis_yok", ayrinti: e.cause?.code ?? e.name };
  }
  const metin = JSON.stringify(govde);
  cikti.setHeader("Content-Type", "application/json; charset=utf-8");
  cikti.setHeader("Content-Length", Buffer.byteLength(metin));
  cikti.writeHead(kod).end(metin);
}).listen(uygulama, "127.0.0.1", () => console.log(`uygulama :${uygulama} hazir`));
```

Katalog verisinin burada kendi yazdığımız bir süreçte durması, dersin konusunu görünür
kılmak içindir; yerini bir veritabanı sunucusu aldığında davranış aynı kalır. Açılışta
yapılan hazırlık işi de aynı yerde durur: dosya biçiminin denetlenmesi, kütüğün onarılması,
dizinlerin açılması.

#### Hazır Olmak, Başlamış Olmaktan Farklıdır

Bir süreci başlatmak ile o sürecin iş görebilir olması iki ayrı olaydır ve aralarında bir
pencere vardır. **Hazır olma denetimi** (readiness check) bu pencereyi bekleyen bir
kapıdır: bağımlı servise denenerek sorulur, olumsuz yanıtta beklenir ve yeniden denenir.
Bekleme süresi her başarısız denemede iki katına çıkar — Uygulama Mimarisi kursunda
tanıtılan **üstel geri çekilme** (exponential backoff) burada da geçerlidir: ilk denemeler
sık, sonrakiler seyrek olur, böylece hızlı açılan bir servis hemen yakalanır, yavaş açılan
bir servis boşuna yoklanmaz.

```js
// bekle.mjs — hazir olma denetimi. Kullanim: node bekle.mjs <kapi|saglik> <baglanti-noktasi> [ad]
import { connect } from "node:net";
const [kip, kapi, ad = "servis"] = process.argv.slice(2);

const kapiAcikMi = (p) => new Promise((coz) => {
  const s = connect({ port: Number(p), host: "127.0.0.1" });
  s.once("connect", () => { s.destroy(); coz(true); });
  s.once("error", () => coz(false));
});
const saglikliMi = async (p) =>
  fetch(`http://127.0.0.1:${p}/saglik`).then((r) => r.status === 200).catch(() => false);

const dene = kip === "kapi" ? kapiAcikMi : saglikliMi;
const basladi = Date.now();
let gecikme = 20;      // ilk bekleme, ms; her basarisiz denemede iki katina cikar
let deneme = 0;
while (Date.now() - basladi < 5000) {
  deneme++;
  if (await dene(kapi)) {
    console.log(`  ${ad}: hazir (${kip} kipi, ${deneme} deneme, ${Date.now() - basladi} ms)`);
    process.exit(0);
  }
  await new Promise((c) => setTimeout(c, gecikme));
  gecikme = Math.min(gecikme * 2, 500);
}
console.error(`  ${ad}: 5 s icinde hazir olmadi`);
process.exit(1);
```

İki kip arasındaki fark ölçümün konusudur. **Bağlantı noktası denetimi** sorunun yalnız bir
kısmını sorar: birisi bu adreste dinliyor mu? **Sağlık denetimi** asıl soruyu sorar: bu
servis iş görebiliyor mu? Başlatma betiği dört ayrı geçidi karşılaştırabilecek biçimde
yazılır.

```bash
#!/usr/bin/env bash
# ayaga-kaldir.sh — bagimli servisleri sirayla ayaga kaldirir.
# Kullanim: ./ayaga-kaldir.sh <yok|sabit|kapi|saglik>;  TOHUM_KUMESI=kucuk|buyuk
kip="${1:-saglik}"; kume="${TOHUM_KUMESI:-kucuk}"
pids=(); temizle() { kill "${pids[@]}" 2>/dev/null; wait 2>/dev/null; }; trap temizle EXIT
echo "== gecit=$kip  tohum=$kume =="

TOHUM_KUMESI="$kume" node veritabani.mjs & pids+=($!)
case "$kip" in
  yok)    ;;                                                 # gecit yok
  sabit)  sleep 0.5 ;;                                       # sabit bekleme: bir tahmin
  kapi)   node bekle.mjs kapi   8431 veritabani || exit 1 ;; # baglanti kabul ediyor mu
  saglik) node bekle.mjs saglik 8431 veritabani || exit 1 ;; # is gorebiliyor mu
esac

node arama.mjs & pids+=($!)
node bekle.mjs kapi 8432 arama || { echo "  -> zincir kirildi: arama ayaga kalkmadi"; exit 1; }

node uygulama.mjs & pids+=($!)
node bekle.mjs kapi 8433 uygulama > /dev/null
printf 'istek -> '
curl -sS --max-time 5 -w ' (HTTP %{http_code})\n' "http://127.0.0.1:8433/?q=kitap%2042"
```

#### Ölçüm: Dört Geçit

```bash
for k in yok sabit kapi saglik; do ./ayaga-kaldir.sh $k; echo; done
```

```
== gecit=yok  tohum=kucuk ==
arama: veritabanina ulasilamadi (ECONNREFUSED)
veritabani :8431 acildi (hazir degil)
veritabani hazir: kucuk tohum, 150000 satir, 128 ms
  arama: 5 s icinde hazir olmadi
  -> zincir kirildi: arama ayaga kalkmadi

== gecit=sabit  tohum=kucuk ==
veritabani :8431 acildi (hazir degil)
veritabani hazir: kucuk tohum, 150000 satir, 136 ms
arama :8432 hazir, dizin 300 kayit
  arama: hazir (kapi kipi, 2 deneme, 23 ms)
uygulama :8433 hazir
istek -> {"sorgu":"kitap 42","eslesme":3,"dizinBoyu":300,"veritabani":{"hazir":true,"sema":3}} (HTTP 200)

== gecit=kapi  tohum=kucuk ==
veritabani :8431 acildi (hazir degil)
  veritabani: hazir (kapi kipi, 2 deneme, 23 ms)
arama: veritabani 503 verdi, dizin kurulamadi
veritabani hazir: kucuk tohum, 150000 satir, 136 ms
  arama: 5 s icinde hazir olmadi
  -> zincir kirildi: arama ayaga kalkmadi

== gecit=saglik  tohum=kucuk ==
veritabani :8431 acildi (hazir degil)
veritabani hazir: kucuk tohum, 150000 satir, 129 ms
  veritabani: hazir (saglik kipi, 4 deneme, 177 ms)
arama :8432 hazir, dizin 300 kayit
  arama: hazir (kapi kipi, 2 deneme, 23 ms)
uygulama :8433 hazir
istek -> {"sorgu":"kitap 42","eslesme":3,"dizinBoyu":300,"veritabani":{"hazir":true,"sema":3}} (HTTP 200)
```

Süreler makineye bağlıdır; arka plandaki üç sürecin yazdığı satırların sırası da her
çalıştırmada değişir. Değişmeyen, hangi geçidin zinciri kurduğudur.

**Geçit yokken** arama servisi veritabanının bağlantı noktası açılmadan istek yapar ve
bağlantı reddedilir. Süreç çıkar, ondan sonrası hiç kurulmaz. Başlatma sırası bir tercih
değil, bir zorunluluktur.

**Bağlantı noktası denetimi** en öğretici satırı üretir. Geçit 23 milisaniyede, ikinci
denemede geçmiştir — çünkü bağlantı noktası gerçekten açıktır. Ama veritabanı o anda hâlâ
tohum verisini yazmaktadır ve aramaya `503` döndürür. Zincir yine kırılır. Dinleyen bir
bağlantı noktası, iş gören bir servis demek değildir.

**Sağlık denetimi** aynı işi 177 milisaniyede ve dört denemede yapar: geçit, servisin kendi
bildirdiği hazır olma durumunu bekler. Zincir kurulur, uygulama iki bağımlılığını da
kullanarak yanıt üretir.

#### Sabit Bekleme Bir Tahmindir

Sabit bekleme küçük tohum kümesinde çalıştı. Bu, doğru olduğunu değil, tahminin tuttuğunu
gösterir. Aynı betik daha büyük bir tohum kümesiyle çalıştırıldığında tahmin bozulur.

```bash
for k in sabit saglik; do TOHUM_KUMESI=buyuk ./ayaga-kaldir.sh $k; echo; done
```

```
== gecit=sabit  tohum=buyuk ==
veritabani :8431 acildi (hazir degil)
arama: veritabani 503 verdi, dizin kurulamadi
veritabani hazir: buyuk tohum, 1000000 satir, 860 ms
  arama: 5 s icinde hazir olmadi
  -> zincir kirildi: arama ayaga kalkmadi

== gecit=saglik  tohum=buyuk ==
veritabani :8431 acildi (hazir degil)
veritabani hazir: buyuk tohum, 1000000 satir, 868 ms
  veritabani: hazir (saglik kipi, 7 deneme, 1179 ms)
arama :8432 hazir, dizin 2000 kayit
  arama: hazir (kapi kipi, 3 deneme, 64 ms)
uygulama :8433 hazir
istek -> {"sorgu":"kitap 42","eslesme":23,"dizinBoyu":2000,"veritabani":{"hazir":true,"sema":3}} (HTTP 200)
```

Yarım saniyelik bekleme, hazırlık 860 milisaniye sürdüğü için yetmez. Sağlık denetimi ise
aynı betikte, hiçbir değişiklik yapılmadan 1179 milisaniye bekler ve geçer. Sabit bekleme
iki yönde de yanılır: kısa olduğunda zinciri kırar, uzun olduğunda her açılışta boşa süre
harcar. Denetim ise beklemeyi ölçüme bağlar.

#### Aynı Ortam İki Makinede

Zincirin kurulması yetmez; aynı zincirin başka bir makinede aynı sonucu vermesi gerekir.
Bunu bozan iki şey vardır: farklı çalışma zamanı sürümleri ve her kurulumda farklı çıkan
tohum verisi.

Birincisi **sürüm sabitlemesiyle** karşılanır: bildirimde yazan en az sürüm, açılışta
çalışan sürümle karşılaştırılır ve uyuşmazlık sessizce geçilmez. İkincisi bir kuralla
karşılanır: **tohum verisi girdidir, üretilen bir şey değildir**. Kimlikler, tarihler ve
ilişkiler tohumun parçası olarak yazılır; çalışma anından ya da rastgelelikten alınmaz.

```js
// tohum.mjs — tohum verisini yazar. Kullanim: node tohum.mjs <dosya> <sabit|serbest>
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";

const [dosya, kural = "sabit"] = process.argv.slice(2);
rmSync(dosya, { force: true });
const db = new DatabaseSync(dosya);
db.exec(`CREATE TABLE uye (id INTEGER PRIMARY KEY, ad TEXT NOT NULL, kayit TEXT NOT NULL);
         CREATE TABLE odunc (id INTEGER PRIMARY KEY, uye INTEGER NOT NULL, isbn TEXT NOT NULL)`);

const UYELER = ["Ayse Kaya", "Baris Demir", "Ceren Yildiz", "Deniz Aksoy"];
const ISBNLER = ["978-0000000001", "978-0000000002", "978-0000000003"];
const uyeEkle = db.prepare("INSERT INTO uye (id, ad, kayit) VALUES (?, ?, ?)");
const oduncEkle = db.prepare("INSERT INTO odunc (id, uye, isbn) VALUES (?, ?, ?)");

UYELER.forEach((ad, i) => {
  // sabit kural: tarih tohumun parcasidir. serbest kural: calisma anindan gelir.
  const kayit = kural === "sabit" ? `2024-01-0${i + 1}` : new Date().toISOString();
  uyeEkle.run(i + 1, ad, kayit);
});
ISBNLER.forEach((isbn, i) => {
  const uye = kural === "sabit"
    ? (i % UYELER.length) + 1
    : 1 + Math.floor(Math.random() * UYELER.length);
  oduncEkle.run(i + 1, uye, isbn);
});
db.close();
```

Kuralın tutup tutmadığı ölçülebilir: aynı tohum iki kez yazılır ve veri içeriğinin
**sağlaması** (checksum) karşılaştırılır. Sağlama dosya düzenine değil, tablo içeriğine
bakar.

```js
// dogrula.mjs — calisma zamani surumunu bildirime gore denetler, tohum sagalamasini yazar
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";

const ortam = JSON.parse(readFileSync("ortam.json", "utf8"));
const anaSurum = Number(process.versions.node.split(".")[0]);
const uygun = anaSurum >= ortam.calisma_zamani.en_az_ana_surum;
console.log(`calisma zamani: ${ortam.calisma_zamani.ad} ${anaSurum}` +
  ` (bildirilen en az ${ortam.calisma_zamani.en_az_ana_surum}) -> ${uygun ? "uygun" : "UYGUN DEGIL"}`);
if (!uygun) process.exit(1);

const sagalama = (dosya) =>
  execFileSync("sqlite3", [dosya, ".sha3sum"], { encoding: "utf8" }).trim().slice(0, 16);
for (const dosya of process.argv.slice(2)) console.log(`  ${dosya}: ${sagalama(dosya)}`);
```

```bash
for kural in sabit serbest; do
  for n in 1 2; do node tohum.mjs "$kural-$n.db" "$kural"; done
done
node dogrula.mjs sabit-1.db sabit-2.db serbest-1.db serbest-2.db
```

```
calisma zamani: node 24 (bildirilen en az 22) -> uygun
  sabit-1.db: adc75c8f1262a1f5
  sabit-2.db: adc75c8f1262a1f5
  serbest-1.db: 1ee804800e30af0a
  serbest-2.db: bb90ce9eec315bec
```

Sağlamalar kısaltılarak yazılmıştır ve serbest kuralın ürettiği iki değer her çalıştırmada
başkadır; sürüm satırı da çalıştıran makinenin çalışma zamanına bağlıdır. Okunan sonuç
şudur: sabit kural iki kurulumda aynı sağlamayı vermiştir, serbest kural vermemiştir. Aynı
ayrım iki makine arasında da geçerlidir. Serbest kuralla kurulan bir ortamda "bende
çalışıyor" cümlesi doğrulanamaz bir iddiaya dönüşür, çünkü iki makinedeki veri aynı
değildir.

#### Özet

- Bağımlı servisler bir zincir kurar: her halka kendinden öncekinin hazır olmasını
  gerektirir; geçit konmadığında ilk halkanın açılış gecikmesi tüm zinciri kırar.
- Bağlantı noktasının açık olması hazır olmak demek değildir; ölçümde bağlantı denetimi 23
  milisaniyede geçmiş, servis o anda hâlâ hazırlık işindeyken bağımlı servise `503`
  döndürmüştür.
- Hazır olma denetimi servisin kendi bildirdiği duruma bakar ve başarısız denemeden sonra
  bekleme süresini iki katına çıkararak yeniden dener.
- Sabit bekleme bir tahmindir: tohum verisi büyüdüğünde aynı yarım saniye yetmemiş, sağlık
  denetimi ise değiştirilmeden gereken süre kadar beklemiştir.
- Ortamın iki makinede aynı kurulması iki kurala dayanır: çalışma zamanı sürümünün
  bildirimle karşılaştırılması ve tohum verisinin çalışma anından değil tohumun kendisinden
  gelmesi; ikincisi sağlamanın eşitliğiyle doğrulanır.

#### Kurs Kapanışı

Bu kurs bir isteğin sunucu tarafındaki yolculuğunu baştan sona kurdu. Başlangıçta sorulan
soru "istemcinin karşısındaki taraf neyi üstlenir" idi; yanıt üç sorumlulukta toplandı —
kalıcılık, iş kuralları, tümleştirme — ve bu sorumlulukların istemciye devredilemeyeceği
ölçülerek gösterildi. Ardından "sunucu" tek bir program olmaktan çıktı: web sunucusu,
uygulama sunucusu ve vekillik ayrı roller olarak ayrıldı, istek yaşam döngüsü ile ara
katman zinciri bu roller içindeki sırayı verdi.

İkinci bölüm uygulamanın kendisini ayağa kaldırdı: yapılandırmanın ortamdan okunması,
gizli değerlerin ayrılması, yapılandırılmış kütük ve ortak hata katmanı. Üçüncü bölüm
uygulamanın dışarıya ne verdiğini ele aldı: dosyanın doğrulayıcı ve saklama yönergesiyle
sunulması, görünümün şablondan üretilmesi, gelen dosyanın sınırlarla kabul edilmesi ve
yanındaki servislerin yerelde ayağa kaldırılması.

Kurs boyunca tekrarlanan tek bir akıl yürütme vardı: **istemciden gelen hiçbir değer,
sunucunun kendi denetiminden geçmediği sürece karar sayılmaz.** Bildirilen uzunluk, bildirilen
içerik türü, gönderilen dosya adı, iletilen istemci adresi — hepsi birer iddiadır. Sunucunun
işi bu iddiaları kendi ölçümüyle karşılamaktır.

Buraya kadar sunucunun dışarıya verdiği şey ya bir dosya ya da bir belgeydi: insanın
okuyacağı içerik. Kütüphane servisinin asıl tüketicileri ise programlardır — tarayıcıdaki
uygulama, şubelerdeki uçbirimler, raporlama işleri. Programların birbirine bağlandığı yerde
biçim değil **sözleşme** belirleyicidir: hangi kaynaklar vardır, adları nedir, hangi
alanları taşır, bir alan kaldırıldığında karşı taraf ne olur? Sıradaki kurs — Web API
Tasarımı — bu sözleşmeyi kurar. İlk konusu, aynı işi karşılayan farklı API biçemlerini
kaynak temelli, uzak yordam çağrısı temelli ve sorgu temelli yaklaşımlar olarak ayırıp
aralarındaki ödünleşimleri ölçer.

