---
title: 'Doğrulama Katmanları'
source: 'https://academia.sh/tr/kurslar/veri-erisim-katmani/dogrulama-katmanlari'
course: 'Veri Erişim Katmanı ve İş Mantığı'
language: tr
updated: '2026-08-17T18:06:50+00:00'
license: 'CC BY-SA 4.0'
---

# Doğrulama Katmanları

Girdi doğrulaması ile alan kuralının ayrımı: aynı gövdenin iki katmanda ürettiği farklı yanıtlar, kararın kaç sorgu gerektirdiğinin sayılması, alan kuralının veri durumuna bağlılığı ve son güvenceyi veren veritabanı kısıtı.

Önceki dersin gelen yön örneğinde istek gövdesi hiç denetlenmeden kabul edildi. `kitapId`
bir sayı mı, `alisTarihi` geçerli bir tarih mi, üye gerçekten var mı, o üye şu anda ödünç
almaya uygun mu?

Bu soruların hepsi "doğrulama" başlığı altında toplanır ama aynı yere ait değildir.
Birincisi ile ikincisi gövdeye bakarak yanıtlanır; üçüncüsü ile dördüncüsü veritabanına
bakmadan yanıtlanamaz. Ayrımın adı **girdi doğrulaması** (input validation) ile **alan
kuralı** (domain rule) ayrımıdır ve bu ders farkı ölçerek gösterir.

Ölçümler aşağıdaki veritabanı üzerinde yapılır. Bir üye askıya alınmış, bir üye üç açık
kayda ulaşmış, bir kitap ödünçtedir.

```sh
# kur.sh — dersin veritabanini sifirdan kurar
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL,
                  durum TEXT NOT NULL CHECK (durum IN ('aktif','askida')));
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY,
                    kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id),
                    alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
INSERT INTO uye VALUES (1,'Ayse Demir','aktif'),(2,'Bora Kaya','askida'),(3,'Cem Yilmaz','aktif');
INSERT INTO kitap VALUES (1,'Korluk'),(2,'Tutunamayanlar'),(3,'Kum Kitabi'),(4,'Puslu Kitalar'),
                         (5,'Saatleri Ayarlama'),(6,'Cocukluk'),(7,'Aylak Adam');
INSERT INTO odunc VALUES (1,1,1,'2025-06-01',NULL),(2,2,3,'2025-06-02',NULL),
                         (3,3,3,'2025-06-03',NULL),(4,4,3,'2025-06-04',NULL);
SQL
```

## Girdi Doğrulaması

Dersin dosyaları üç dizine yerleşir.

```sh
mkdir -p sunum alan altyapi
```

Girdi doğrulaması gövdenin biçimini denetler: alan var mı, türü doğru mu, deseni tutuyor
mu. Kararı yalnızca gövdeye bakarak verir. Web API Tasarımı kursundaki Doğrulama Hataları
dersi bu katmanın çıktısını bir liste olarak tanımlamıştı: her hata bir gövde yolu ve bir
kod taşır.

```js
// sunum/girdi-dogrulama.mjs — govdenin bicimini denetler; veriye hic bakmaz
const KURALLAR = {
  zorunlu: (d) => (d === undefined || d === null || d === "" ? { kod: "zorunlu" } : null),
  tamsayi: (d) => (Number.isInteger(d) && d > 0 ? null : { kod: "tamsayi_degil" }),
  tarih: (d) => (/^\d{4}-\d{2}-\d{2}$/.test(String(d)) ? null : { kod: "tarih_degil" }),
};

export const SEMA = {
  "/uyeId": ["zorunlu", "tamsayi"],
  "/kitapId": ["zorunlu", "tamsayi"],
  "/alisTarihi": ["zorunlu", "tarih"],
};

// Donen liste Dogrulama Hatalari dersindeki { yol, kod } bicimindedir.
export function girdiyiDogrula(sema, govde) {
  const hatalar = [];
  for (const [yol, kurallar] of Object.entries(sema)) {
    const deger = govde[yol.slice(1)];
    for (const ad of kurallar) {
      if (ad !== "zorunlu" && (deger === undefined || deger === null || deger === "")) break;
      const sonuc = KURALLAR[ad](deger);
      if (sonuc) { hatalar.push({ yol, ...sonuc }); break; }   // alan basina tek hata
    }
  }
  return hatalar;
}
```

İşlevin imzasında veritabanı yok. Bu, katmanın tanımıdır: aynı gövde her koşulda aynı hata
listesini üretir.

## Alan Kuralı

Alan kuralı kurumun kuralını sorar: bu üye ödünç alabilir mi, bu kitap uygun mu? Kararı
gövdeden çıkarılamaz.

```js
// alan/odunc-kurallari.mjs — kurum kurallari; karar icin veri gerekir
export const UYE_SINIRI = 3;

export function oduncVerilebilirMi(depo, uyeId, kitapId) {
  const uye = depo.uyeyiBul(uyeId);
  if (uye === null) return { sonuc: "reddedildi", sebep: "uye_yok" };
  if (uye.durum === "askida") return { sonuc: "reddedildi", sebep: "uye_askida" };
  if (!depo.kitapVarMi(kitapId)) return { sonuc: "reddedildi", sebep: "kitap_yok" };
  if (depo.kitabinAcikKaydi(kitapId) !== null) return { sonuc: "reddedildi", sebep: "kitap_oduncte" };
  if (depo.uyeninAcikSayisi(uyeId) >= UYE_SINIRI) {
    return { sonuc: "reddedildi", sebep: "uye_siniri", sinir: UYE_SINIRI };
  }
  return { sonuc: "kabul" };
}
```

Kural bir depo alıyor ve tek bir sebep döndürüyor. İki fark burada belirir: kararın girdisi
gövde değil **durum**dur, çıktısı ise liste değil **tek bir olgu**dur.

Kararın kaça mal olduğunu görmek için depo çalıştırdığı sorguyu sayar.

```js
// altyapi/sayan-depo.mjs — sozlesmeyi karsilar ve calistirdigi sorguyu sayar
import { DatabaseSync } from "node:sqlite";

export class SayanDepo {
  constructor(dosya) { this.db = new DatabaseSync(dosya); this.sorgu = 0; }
  #tek(sql, ...p) { this.sorgu++; return this.db.prepare(sql).get(...p) ?? null; }
  uyeyiBul(uyeId) { return this.#tek("SELECT uye_id, durum FROM uye WHERE uye_id = ?", uyeId); }
  kitapVarMi(kitapId) {
    return this.#tek("SELECT kitap_id FROM kitap WHERE kitap_id = ?", kitapId) !== null;
  }
  kitabinAcikKaydi(kitapId) {
    return this.#tek(
      "SELECT odunc_id FROM odunc WHERE kitap_id = ? AND iade_tarihi IS NULL", kitapId);
  }
  uyeninAcikSayisi(uyeId) {
    return this.#tek(
      "SELECT count(*) AS n FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL", uyeId).n;
  }
}
```

## Aynı Girdi, İki Katman

Beş gövde sırayla iki katmandan geçirilir. İlki biçimsel olarak bozuk; kalan dördü biçimsel
olarak kusursuz.

```js
// iki-katman.mjs — ayni govdeler once girdi dogrulamasindan, sonra alan kuralindan gecer
import { girdiyiDogrula, SEMA } from "./sunum/girdi-dogrulama.mjs";
import { oduncVerilebilirMi } from "./alan/odunc-kurallari.mjs";
import { SayanDepo } from "./altyapi/sayan-depo.mjs";

const TABAN = "https://ornek.kutuphane/sorunlar/";
const depo = new SayanDepo("kutuphane.db");

const GOVDELER = [
  { uyeId: "iki", kitapId: 0, alisTarihi: "15/06/2025" },
  { uyeId: 2, kitapId: 6, alisTarihi: "2025-06-20" },
  { uyeId: 1, kitapId: 1, alisTarihi: "2025-06-20" },
  { uyeId: 3, kitapId: 6, alisTarihi: "2025-06-20" },
  { uyeId: 1, kitapId: 6, alisTarihi: "2025-06-20" },
];

for (const govde of GOVDELER) {
  console.log(`govde: ${JSON.stringify(govde)}`);
  const hatalar = girdiyiDogrula(SEMA, govde);
  console.log(`  girdi dogrulama: ${hatalar.length} hata, sorgu = 0`);
  if (hatalar.length > 0) {
    console.log(`  yanit 422 ${JSON.stringify({ type: TABAN + "dogrulama", hatalar })}`);
    continue;
  }
  depo.sorgu = 0;
  const karar = oduncVerilebilirMi(depo, govde.uyeId, govde.kitapId);
  console.log(`  alan kurali: ${karar.sonuc}${karar.sebep ? ` (${karar.sebep})` : ""}, sorgu = ${depo.sorgu}`);
  console.log(karar.sonuc === "kabul"
    ? "  yanit 201 " + JSON.stringify({ durum: "verildi" })
    : "  yanit 409 " + JSON.stringify({ type: TABAN + karar.sebep.replaceAll("_", "-") }));
}
```

```sh
sh kur.sh
node iki-katman.mjs
```

```
govde: {"uyeId":"iki","kitapId":0,"alisTarihi":"15/06/2025"}
  girdi dogrulama: 3 hata, sorgu = 0
  yanit 422 {"type":"https://ornek.kutuphane/sorunlar/dogrulama","hatalar":[{"yol":"/uyeId","kod":"tamsayi_degil"},{"yol":"/kitapId","kod":"tamsayi_degil"},{"yol":"/alisTarihi","kod":"tarih_degil"}]}
govde: {"uyeId":2,"kitapId":6,"alisTarihi":"2025-06-20"}
  girdi dogrulama: 0 hata, sorgu = 0
  alan kurali: reddedildi (uye_askida), sorgu = 1
  yanit 409 {"type":"https://ornek.kutuphane/sorunlar/uye-askida"}
govde: {"uyeId":1,"kitapId":1,"alisTarihi":"2025-06-20"}
  girdi dogrulama: 0 hata, sorgu = 0
  alan kurali: reddedildi (kitap_oduncte), sorgu = 3
  yanit 409 {"type":"https://ornek.kutuphane/sorunlar/kitap-oduncte"}
govde: {"uyeId":3,"kitapId":6,"alisTarihi":"2025-06-20"}
  girdi dogrulama: 0 hata, sorgu = 0
  alan kurali: reddedildi (uye_siniri), sorgu = 4
  yanit 409 {"type":"https://ornek.kutuphane/sorunlar/uye-siniri"}
govde: {"uyeId":1,"kitapId":6,"alisTarihi":"2025-06-20"}
  girdi dogrulama: 0 hata, sorgu = 0
  alan kurali: kabul, sorgu = 4
  yanit 201 {"durum":"verildi"}
```

Üç fark ölçüldü.

**Yanıtın biçimi farklı.** Girdi doğrulaması üç ayrı alan hatasını tek yanıtta topladı; her
biri bir gövde yoluyla işaretli, yani istemci hangi giriş kutusunu boyayacağını biliyor.
Alan kuralı tek bir sebep döndürdü ve gövde yolu vermedi: "üye askıda" bir alanın hatası
değildir, işlemin kendisinin reddidir. Durum kodu da bu yüzden ayrışır; biri gövdenin
düzeltilebilir olduğunu, öteki isteğin şu anki durumda karşılanamayacağını söyler.

**Maliyet farklı.** Girdi doğrulaması sıfır sorguyla karar verdi. Alan kuralının kararı bir
ile dört sorgu arasında değişti; kural erken reddettiğinde sorgu sayısı düştü. Bu, katman
sırasının gerekçesidir: biçimsel olarak bozuk gövde alan katmanına hiç ulaşmadı ve dört
sorgu boşa gitmedi.

**Bilgi kaynağı farklı.** Girdi doğrulaması gövdedeki değere baktı; alan kuralı gövdede
olmayan bir bilgiye baktı. İstek `uyeId: 2` diyor, üyenin askıda olduğunu söylemiyor. Bu
bilgi gövdeye eklenebilir olsaydı zaten istemciye sorulmuş olurdu ve istemci onu istediği
gibi doldururdu.

## Kuralın Veriye Bağlılığı

Üçüncü farkın kanıtı, gövdeyi sabit tutup veriyi değiştirmektir.

```js
// veriye-bagli.mjs — ayni govde, degismeyen bicim, degisen veri
import { DatabaseSync } from "node:sqlite";
import { girdiyiDogrula, SEMA } from "./sunum/girdi-dogrulama.mjs";
import { oduncVerilebilirMi } from "./alan/odunc-kurallari.mjs";
import { SayanDepo } from "./altyapi/sayan-depo.mjs";

const GOVDE = { uyeId: 2, kitapId: 6, alisTarihi: "2025-06-20" };
const yazan = new DatabaseSync("kutuphane.db");
const depo = new SayanDepo("kutuphane.db");

for (const durum of ["askida", "aktif", "askida"]) {
  yazan.prepare("UPDATE uye SET durum = ? WHERE uye_id = 2").run(durum);
  const hata = girdiyiDogrula(SEMA, GOVDE).length;
  const karar = oduncVerilebilirMi(depo, GOVDE.uyeId, GOVDE.kitapId);
  console.log(`uye 2 durumu=${durum.padEnd(7)} girdi hatasi=${hata}  ` +
              `alan karari=${karar.sonuc}${karar.sebep ? ` (${karar.sebep})` : ""}`);
}
```

```sh
node veriye-bagli.mjs
```

```
uye 2 durumu=askida  girdi hatasi=0  alan karari=reddedildi (uye_askida)
uye 2 durumu=aktif   girdi hatasi=0  alan karari=kabul
uye 2 durumu=askida  girdi hatasi=0  alan karari=reddedildi (uye_askida)
```

Gövde hiç değişmedi. Girdi doğrulaması üç turda da sıfır hata verdi; alan kuralı iki farklı
karar üretti. Üçüncü tur ilk durumu geri getiriyor, böylece betik tekrar çalıştırıldığında
aynı çıktıyı üretiyor.

Bu, alan kuralının girdi doğrulamasına indirgenememesinin biçimsel gerekçesidir. Girdi
doğrulaması gövdenin bir işlevidir; alan kuralı ise gövde ile durumun ortak işlevidir. Girdi
doğrulaması sabit bir şemayla anlatılabilir, alan kuralı anlatılamaz.

## Üçüncü Kat

İki katman da geçildikten sonra bir boşluk kalır. Alan kuralı "kitap ödünçte değil" dedi ve
yazma bunun ardından yapıldı; arada başka bir istek aynı kitabı yazmışsa kural yanlış
bilgiye dayanmış olur. İyimser ve Kötümser Kilitleme dersindeki denetle–sonra–yaz açığının
aynısıdır.

Aşağıdaki betik iki isteğin de yazmadan önce kuralı değerlendirdiği sırayı kurar ve aynı
sırayı kısıtlı ve kısıtsız iki veritabanında dener.

```js
// ucuncu-kat.mjs — alan kurali gecti, arada baska bir istek girdi: son soz kisitin
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";
import { oduncVerilebilirMi } from "./alan/odunc-kurallari.mjs";

function kur(dosya, kisitli) {
  rmSync(dosya, { force: true });
  const db = new DatabaseSync(dosya);
  db.exec(`CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, durum TEXT NOT NULL);
           CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL);
           CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                               uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
           INSERT INTO uye VALUES (1,'Ayse Demir','aktif'),(2,'Bora Kaya','aktif');
           INSERT INTO kitap VALUES (6,'Cocukluk');`);
  if (kisitli) {
    db.exec("CREATE UNIQUE INDEX acik_odunc ON odunc(kitap_id) WHERE iade_tarihi IS NULL");
  }
  return db;
}

const depoKur = (db) => ({
  uyeyiBul: (id) => db.prepare("SELECT uye_id, durum FROM uye WHERE uye_id = ?").get(id) ?? null,
  kitapVarMi: (id) => db.prepare("SELECT kitap_id FROM kitap WHERE kitap_id = ?").get(id) !== undefined,
  kitabinAcikKaydi: (id) => db.prepare(
    "SELECT odunc_id FROM odunc WHERE kitap_id = ? AND iade_tarihi IS NULL").get(id) ?? null,
  uyeninAcikSayisi: (id) => db.prepare(
    "SELECT count(*) AS n FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL").get(id).n,
});

for (const kisitli of [false, true]) {
  const db = kur(kisitli ? "kisitli.db" : "kisitsiz.db", kisitli);
  const depo = depoKur(db);
  const yaz = (uyeId) => db.prepare(
    "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (6,?,'2025-06-20',NULL)")
    .run(uyeId);

  // Iki istek ayni kitabi istiyor; ikisi de yazmadan once kurali degerlendiriyor.
  const karar1 = oduncVerilebilirMi(depo, 1, 6).sonuc;
  const karar2 = oduncVerilebilirMi(depo, 2, 6).sonuc;
  yaz(1);
  let ikinci;
  try { yaz(2); ikinci = "yazildi"; }
  catch (e) { ikinci = `reddedildi (${e.message})`; }

  const acik = db.prepare(
    "SELECT count(*) AS n FROM odunc WHERE kitap_id = 6 AND iade_tarihi IS NULL").get().n;
  console.log(`kisit ${kisitli ? "var" : "yok"}: karar1=${karar1} karar2=${karar2}  ` +
              `ikinci yazma=${ikinci}  kitap 6 acik kayit=${acik}`);
}
```

```sh
node ucuncu-kat.mjs
```

```
kisit yok: karar1=kabul karar2=kabul  ikinci yazma=yazildi  kitap 6 acik kayit=2
kisit var: karar1=kabul karar2=kabul  ikinci yazma=reddedildi (UNIQUE constraint failed: odunc.kitap_id)  kitap 6 acik kayit=1
```

İki durumda da alan kuralı iki isteği de kabul etti; kural yanlış değil, zamanlaması
elverişsizdi. Kısıt yokken kitabın iki açık kaydı oluştu ve bu, iş kuralının doğrudan
ihlalidir. Kısıt varken ikinci yazma reddedildi ve açık kayıt sayısı birde kaldı.

Katmanların rolü böylece ayrışıyor. Alan kuralı **kararın gerekçesini** üretir: kullanıcıya
"bu kitap ödünçte" denebilmesinin tek yolu odur; kısıt hatası bu cümleyi veremez. Kısıt ise
**değişmezi** garanti eder ve kural atlandığında ya da yarıştığında son sözü söyler. İkisi
birbirinin yerine geçmez: yalnız kısıta güvenen bir uygulama kullanıcıya anlamlı hata
veremez, yalnız kurala güvenen bir uygulama bozuk veriye açıktır.

## Sınırın Çizilmesi

Yeni bir kuralın hangi katmana ait olduğu tek soruyla belirlenir: **kural, gövdeye bakarak
yanıtlanabiliyor mu?**

"İade tarihi geçmişte olamaz" gövdeye bakarak yanıtlanır; bir tarih karşılaştırmasıdır ve
girdi doğrulamasına aittir. "Üyenin gecikmiş kaydı varken yeni ödünç verilemez" gövdeye
bakarak yanıtlanamaz; alan kuralıdır. "ISBN on üç haneli olmalı" biçimdir. "Bu ISBN
katalogda olmalı" veridir.

Ara bir durum vardır: gövde içi tutarlılık. "Alış tarihi son teslim tarihinden önce olmalı"
kuralı iki alanı karşılaştırır, veri istemez ve girdi doğrulamasında kalır. Doğrulama
Hataları dersindeki şema bu tür kuralları alan yolu üzerinden ifade edebiliyordu; ölçüt
değişmiyor.

Sıra da bu ayrımdan çıkar: önce biçim, sonra kural, en sonda kısıt. Ters sıra iki maliyet
üretir. Biçimsiz gövdeyle sorgu çalıştırmak boşa iştir; kural yerine kısıt hatasını
kullanıcıya göstermek ise sorunun ne olduğunu anlatmayan bir yanıttır.

## Özet

- Girdi doğrulaması gövdenin işlevidir ve sıfır sorguyla karar verdi; alan kuralı gövde ile
  durumun ortak işlevidir ve kararı bir ile dört sorgu arasında değişti.
- İki katmanın yanıtı biçim olarak farklıdır: biri gövde yollarıyla işaretli bir hata
  listesi, öteki tek bir ret sebebi üretti.
- Gövde sabit tutulup üyenin durumu değiştirildiğinde girdi doğrulaması üç turda da sıfır
  hata verdi, alan kuralı iki farklı karar üretti.
- Alan kuralı denetle–sonra–yaz açığına açıktır: kısıtsız veritabanında aynı kitabın iki
  açık kaydı oluştu, kısıtlı veritabanında ikinci yazma reddedildi.
- Kural gerekçeyi, kısıt değişmezi verir; biri kullanıcıya anlamlı hata üretir, öteki veriyi
  garanti eder.

## Sonraki Adım

Beşinci gövde iki katmandan da geçti ve ödünç kaydının yazılması sırası geldi. Yazmayla
birlikte başlaması gereken başka işler de var: üyeye bildirim gitmeli, günlük istatistik
güncellenmeli, ilerideki raporlar için hareket kaydı düşülmeli. Bunların hepsini ödünç verme işlevinin içine yazmak
o işlevi her yeni yan etkide değiştirmek demektir. Sonraki ders ödünç verme kararının
ürettiği olguyu bir **alan olayı** olarak yayımlar, iki dinleyici bağlar ve olayın işlem
sınırıyla ilişkisini ölçer: işlem geri alındığında dinleyicinin ürettiği yanlış bildirim
sayılır ve giden kutusu düzeniyle sıfıra indirilir.
