---
title: 'Hata ve Yetkilendirme'
source: 'https://academia.sh/tr/kurslar/api-tasarimi/hata-ve-yetkilendirme'
course: 'Web API Tasarımı'
language: tr
updated: '2026-08-17T18:06:45+00:00'
license: 'CC BY-SA 4.0'
---

# Hata ve Yetkilendirme

Alan düzeyinde hata toplama, kısmi veriyle hatanın aynı yanıtta dönmesi, çözücüleri sarmalayan yetki katmanı ve zorunlu alanlarda boşluğun üst düzeylere yayılmasının ölçülmesi.

Bağlantı modeli yanıtın hangi kayıtları taşıyacağını düzenledi ama bir varsayımı korudu:
sorulan her alan okunabilir ve her alan bir değer döndürür.

Kütüphanede bu doğru değildir. Bir üyenin ceza bilgisini yalnız o üye ve görevliler
görebilir; ödünç kaydının notu yalnız kaydın açıldığı şubede okunabilir. Ayrıca alanların
bir bölümü başka servislerden gelir ve o servisler düşebilir. Bu ders iki soruyu birlikte
yanıtlar: bir alan çöktüğünde yanıtın geri kalanına ne olur ve yetki denetimi nereye
yazılır?

Ayrıştırıcı, düzleyici ve şema önceki derslerde yazıldıkları hâliyle kullanılır;
çalıştırıcı bu derste hata toplayacak biçimde yeniden yazılır.

## Alan Düzeyinde Hata

Kaynak temelli bir uç noktada hata bütün yanıtı kaplar: durum kodu ya başarıdır ya
değildir. Sorgu temelli yaklaşımda tek bir yanıt yüzlerce alan taşır ve bunlardan birinin
çökmesi diğerlerini geçersiz kılmaz. Yanıt bu yüzden iki bölümlüdür: `data` çözülebilen
alanları, `errors` çözülemeyenleri taşır. İkisi aynı yanıtta bulunur; buna **kısmi başarı**
denir.

Çalıştırıcıya iki şey eklenir. Her alan çağrısı bir `try` bloğuna alınır ve çöken alan
hata listesine, yanıt içindeki yoluyla birlikte yazılır. İkincisi daha incedir: şemada `!` ile işaretlenmiş
bir alana `null` yazılamaz, dolayısıyla o alan çöktüğünde boşluk **yukarı taşınır**.

```js
// calistirici.mjs — alan duzeyinde hata toplayan calistirici
// Bir alan cokerse hata listeye yazilir, o alan null olur. Alan "!" ile zorunluysa
// null yazilamaz; bosluk en yakin bos deger alabilen ust alana kadar yukselir.
import { tipCoz, alanTanimi } from "./sema.mjs";
import { duzle } from "./duzle.mjs";

const YAPRAK = new Set(["skaler", "sirali"]);
class BosDegil extends Error {}                       // zorunlu alana null yazma girisimi

function degiskenleriCoz(islem, gelen) {
  const d = {};
  for (const b of islem.degiskenler) {
    const v = gelen[b.ad] ?? b.varsayilan;
    if (v === undefined && b.tip.endsWith("!")) throw new Error(`$${b.ad} değişkeni zorunlu`);
    d[b.ad] = v;
  }
  return d;
}

const argCoz = (argumanlar, dv) =>
  Object.fromEntries(Object.entries(argumanlar).map(([a, d]) => [a, d.tur === "degisken" ? dv[d.ad] : d.deger]));

async function secimCalistir(o, tipAdi, secim, kaynak, yol, sirali = false) {
  const alanlar = duzle(o.sema, o.belge, tipAdi, secim);
  const sonuc = {};
  for (const a of alanlar) sonuc[a.takma] = null;
  const isle = async (a) => { sonuc[a.takma] = await alanCalistir(o, tipAdi, a, kaynak, `${yol}.${a.takma}`); };
  if (sirali) for (const a of alanlar) await isle(a);
  else await Promise.all(alanlar.map(isle));
  return sonuc;
}

async function alanCalistir(o, tipAdi, alan, kaynak, yol) {
  const tanim = alanTanimi(o.sema.tipler[tipAdi], alan.ad);
  const zorunlu = tipCoz(tanim.tip).zorunlu;
  const cozucu = o.cozucular[tipAdi]?.[alan.ad] ?? ((k) => k?.[alan.ad]);
  const bilgi = { alan: alan.ad, ustTip: tipAdi, yol };
  try {
    const deger = await cozucu(kaynak, argCoz(alan.argumanlar, o.dv), o.baglam, bilgi);
    if ((deger === null || deger === undefined) && zorunlu) throw new BosDegil();
    return await tamamla(o, tanim.tip, deger, alan.secim, yol);
  } catch (h) {
    // Yukseltilen bosluk yeniden bildirilmez; asil hata zaten listede.
    if (!(h instanceof BosDegil)) o.hatalar.push({ ileti: h.ileti ?? h.message, kod: h.kod ?? "ic_hata", yol });
    if (zorunlu) throw new BosDegil();
    return null;
  }
}

async function tamamla(o, tipDizgi, deger, secim, yol) {
  if (deger === null || deger === undefined) return null;
  const t = tipCoz(tipDizgi);
  if (t.liste) {
    const ogeZorunlu = /^\[[^\]]+!\]/.test(tipDizgi);      // [Odunc!] mi [Odunc] mu
    return Promise.all(deger.map(async (d, i) => {
      try { return await tamamla(o, t.ad, d, secim, `${yol}[${i}]`); }
      catch (h) { if (h instanceof BosDegil && !ogeZorunlu) return null; throw h; }
    }));
  }
  const tip = o.sema.tipler[t.ad];
  if (YAPRAK.has(tip.tur)) return deger;
  const somut = tip.tur === "nesne" ? t.ad : o.cozucular.__tip[t.ad](deger);
  return secimCalistir(o, somut, secim, deger, yol);
}

export async function calistir(sema, cozucular, belge, { islemAdi, degiskenler = {}, baglam = {} } = {}) {
  const islem = islemAdi ? belge.islemler.find((i) => i.ad === islemAdi) : belge.islemler[0];
  const o = { sema, cozucular, belge, dv: degiskenleriCoz(islem, degiskenler), baglam, hatalar: [] };
  const kok = islem.tur === "mutation" ? sema.mutasyon : sema.sorgu;
  let data = null;
  try { data = await secimCalistir(o, kok, islem.secim, null, "", islem.tur === "mutation"); }
  catch (h) { if (!(h instanceof BosDegil)) throw h; }      // kokte bosluk: data null olur
  return o.hatalar.length ? { data, errors: o.hatalar } : { data };
}
```

Hata kaydının `yol` alanı, önceki konudaki alan düzeyi doğrulama hatalarıyla aynı işi
görür: istemcinin hatayı yanıttaki yerine bağlamasını sağlar. Fark, yolun burada istek
gövdesini değil **yanıt ağacını** göstermesidir.

## Yetkilendirme Nereye Yazılır

Yetki denetimi çözücünün içine yazılabilir ama o zaman her çözücüde tekrarlanır ve yeni bir
çözücü yazan kişinin unutması sessizce bir sızıntı üretir. Denetim, çözücüleri sarmalayan
ayrı bir katmanda toplanır.

```js
// yetki.mjs — alan duzeyinde erisim denetimi; cozuculeri sarmalar
export class YetkiHatasi extends Error {
  constructor(alan) { super(`"${alan}" alanını görme yetkiniz yok.`); this.kod = "yetkisiz"; this.ileti = this.message; }
}

// kurallar: { Tip: { alan: (kaynak, baglam) => boolean } }
export function yetkiUygula(cozucular, kurallar) {
  const yeni = { ...cozucular };
  for (const [tip, alanlar] of Object.entries(kurallar)) {
    yeni[tip] = { ...(cozucular[tip] ?? {}) };
    for (const [alan, izinli] of Object.entries(alanlar)) {
      const asil = cozucular[tip]?.[alan] ?? ((k) => k?.[alan]);
      yeni[tip][alan] = (kaynak, arg, baglam, bilgi) => {
        if (!izinli(kaynak, baglam)) throw new YetkiHatasi(`${tip}.${alan}`);
        return asil(kaynak, arg, baglam, bilgi);
      };
    }
  }
  return yeni;
}
```

Kuralın iki girdisi vardır ve ikisi de gereklidir. **Bağlam** isteği yapanı taşır; **kaynak**
ise üzerinde karar verilen kaydı. "Üye kendi cezasını görebilir" kuralı ikisini
karşılaştırmadan yazılamaz. Bu, yetki denetiminin neden alan düzeyinde durduğunu da
açıklar: karar, kaydın kendisine bakmadan verilemez ve kayıt ancak o alanın çözücüsü
çağrılırken elde olur.

Korunacak iki alan şemaya eklenir. İkisi de boş değer alabilir yazılmıştır ve bu bir
zorunluluktur: yetkisiz bir alan `null` döner, dolayısıyla yetkiye bağlı hiçbir alan `!`
ile işaretlenemez. Bu kısıtın gerekçesi dersin sonunda ölçülecek.

```js
// sema-yetki.mjs — 01. dersteki semaya korunacak iki alan eklenmis hali
import { SEMA } from "./sema.mjs";

export const SEMA_Y = {
  ...SEMA,
  tipler: {
    ...SEMA.tipler,
    Uye:   { ...SEMA.tipler.Uye,   alanlar: { ...SEMA.tipler.Uye.alanlar,   ceza: "Int" } },
    Odunc: { ...SEMA.tipler.Odunc, alanlar: { ...SEMA.tipler.Odunc.alanlar, not: "String" } },
  },
};
```

```js
// veri.mjs — odunc verisi ve cozuculer; ceza ve not alanlari korumaya alinacak
export const UYELER = new Map([
  ["U-1001", { id: "U-1001", olusturma: "2024-02-11", ad: "Ayse Yilmaz", ceza: 0 }],
  ["U-1002", { id: "U-1002", olusturma: "2025-06-03", ad: "Deniz Kaya", ceza: 12 }],
]);
export const ODUNCLER = new Map([
  ["O-1", { id: "O-1", olusturma: "2026-01-04", uyeId: "U-1001", durum: "ACIK", iadeTarihi: "2026-03-20",
            sube: "merkez", not: "Kapak zedeli teslim alındı.", kalemler: [{ isbn: "978-0262033848", sube: "merkez" }] }],
  ["O-2", { id: "O-2", olusturma: "2026-01-19", uyeId: "U-1002", durum: "ACIK", iadeTarihi: "2026-04-02",
            sube: "sahil", not: "Ikinci uzatma verildi.", kalemler: [{ isbn: "978-0201896831", sube: "sahil" }] }],
]);

export const COZUCULER = {
  __tip: { Kayit: (d) => (d.kalemler ? "Odunc" : "Uye") },
  Sorgu: {
    odunc: (_, a) => ODUNCLER.get(a.id) ?? null,
    oduncler: () => [...ODUNCLER.values()],
    uye: (_, a) => UYELER.get(a.kimlik) ?? null,
  },
  Odunc: { uye: (o) => UYELER.get(o.uyeId) },
};
```

```js
// kosu.mjs — kismi hata ve alan duzeyi yetkilendirme
import { ayristir } from "./ayristirici.mjs";
import { calistir } from "./calistirici.mjs";
import { SEMA_Y as SEMA } from "./sema-yetki.mjs";
import { COZUCULER } from "./veri.mjs";
import { yetkiUygula } from "./yetki.mjs";

// Kurallar: ceza yalniz kendi kaydinda, not yalniz kaydin acildigi subede gorulur.
const KURALLAR = {
  Uye: { ceza: (uye, b) => b.kullanici === uye.id || b.rol === "gorevli" },
  Odunc: { not: (odunc, b) => b.sube === odunc.sube },
};
const KORUMALI = yetkiUygula(COZUCULER, KURALLAR);

const SORGU = `{
  oduncler { id durum not uye { ad ceza } }
}`;

const BAGLAMLAR = {
  "üye U-1001, merkez şubede": { kullanici: "U-1001", rol: "uye", sube: "merkez" },
  "görevli, sahil şubede":     { kullanici: "P-7", rol: "gorevli", sube: "sahil" },
};

for (const [ad, baglam] of Object.entries(BAGLAMLAR)) {
  const s = await calistir(SEMA, KORUMALI, ayristir(SORGU), { baglam });
  const alanSayisi = JSON.stringify(s.data).match(/:/g).length;
  console.log(`\n=== ${ad} ===`);
  console.log(`data: ${JSON.stringify(s.data)}`);
  console.log(`hata sayısı: ${(s.errors ?? []).length}   dolu alan sayısı: ${alanSayisi}`);
  for (const h of s.errors ?? []) console.log(`  ${h.kod.padEnd(12)} ${h.yol.padEnd(24)} ${h.ileti}`);
}
```

```
=== üye U-1001, merkez şubede ===
data: {"oduncler":[{"id":"O-1","durum":"ACIK","not":"Kapak zedeli teslim alındı.","uye":{"ad":"Ayse Yilmaz","ceza":0}},{"id":"O-2","durum":"ACIK","not":null,"uye":{"ad":"Deniz Kaya","ceza":null}}]}
hata sayısı: 2   dolu alan sayısı: 13
  yetkisiz     .oduncler[1].not         "Odunc.not" alanını görme yetkiniz yok.
  yetkisiz     .oduncler[1].uye.ceza    "Uye.ceza" alanını görme yetkiniz yok.

=== görevli, sahil şubede ===
data: {"oduncler":[{"id":"O-1","durum":"ACIK","not":null,"uye":{"ad":"Ayse Yilmaz","ceza":0}},{"id":"O-2","durum":"ACIK","not":"Ikinci uzatma verildi.","uye":{"ad":"Deniz Kaya","ceza":12}}]}
hata sayısı: 1   dolu alan sayısı: 13
  yetkisiz     .oduncler[0].not         "Odunc.not" alanını görme yetkiniz yok.
```

Aynı sorgu, iki farklı bağlamda iki farklı yanıt üretti. Her iki durumda da ödünç kayıtları
döndü, kimlikleri ve durumları okundu; yalnız korunan alanlar boş kaldı ve gerekçeleri
`errors` içinde bildirildi.

Görevlinin yanıtında `not` alanının biri boş: görevli olmak ceza görmeye yeter ama not
görmeye yetmez, çünkü not kuralı role değil şubeye bakar. İki kuralın farklı ölçütlere
dayanabilmesi, yetkilendirmenin alan düzeyinde tanımlanmasının kazandırdığı şeydir.

Hata iletisinin ne söylediğine de dikkat edilmelidir: alanın **var olduğunu** söyler ama
değerini vermez. Bir alanın varlığını gizlemek gerekiyorsa yetkilendirme yeterli değildir;
o alan şemadan çıkarılmalı ya da sorgu denetimi sırasında reddedilmelidir.

## Boşluğun Yayılması

Bir alanın çökmesi her zaman o alanla sınırlı kalmaz. Şemadaki `!` işaretleri, boşluğun
nereye kadar yayılacağını belirler.

Ölçüm için aynı ödünç listesini iki ayrı işaretlemeyle sunan bir alan gerekir. `oduncler`
alanı `[Odunc!]!` yazılmıştır; şemaya aynı kayıtları `[Odunc]` olarak döndüren ikinci bir
kök alan eklenir. İkisi aynı veriyi verir, yalnız işaretlemeleri ayrılır.

```js
// sema-esnek.mjs — ayni odunc listesini bos deger alabilen isaretlemeyle sunan kok alan
import { SEMA_Y } from "./sema-yetki.mjs";

export const SEMA_E = {
  ...SEMA_Y,
  tipler: {
    ...SEMA_Y.tipler,
    Sorgu: { ...SEMA_Y.tipler.Sorgu,
             alanlar: { ...SEMA_Y.tipler.Sorgu.alanlar, esnekOduncler: "[Odunc]" } },
  },
};
```

```js
// bosluk.mjs — bos deger alabilen ve alamayan alanlarda cokmenin yayilmasi
import { ayristir } from "./ayristirici.mjs";
import { calistir } from "./calistirici.mjs";
import { SEMA_E as SEMA } from "./sema-esnek.mjs";
import { ODUNCLER, UYELER } from "./veri.mjs";

const coker = (alan) => (o) => {
  if (o.id === "O-2") throw Object.assign(new Error("Gecikme servisi yanıt vermedi."), { kod: "yukari_akis" });
  return o[alan];
};

const TABAN = {
  __tip: { Kayit: (d) => (d.kalemler ? "Odunc" : "Uye") },
  Sorgu: { oduncler: () => [...ODUNCLER.values()], esnekOduncler: () => [...ODUNCLER.values()] },
  Odunc: { uye: (o) => UYELER.get(o.uyeId) },
};

const DURUMLAR = [
  ["iadeTarihi çöküyor (Tarih — boş olabilir)", "oduncler", { ...TABAN, Odunc: { ...TABAN.Odunc, iadeTarihi: coker("iadeTarihi") } }],
  ["durum çöküyor ([Odunc!]! içinde, Durum!)", "oduncler", { ...TABAN, Odunc: { ...TABAN.Odunc, durum: coker("durum") } }],
  ["durum çöküyor ([Odunc] içinde, Durum!)", "esnekOduncler", { ...TABAN, Odunc: { ...TABAN.Odunc, durum: coker("durum") } }],
];

for (const [ad, kok, cozucular] of DURUMLAR) {
  const s = await calistir(SEMA, cozucular, ayristir(`{ ${kok} { id durum iadeTarihi uye { ad } } }`));
  const metin = JSON.stringify(s.data);
  console.log(`\n${ad}`);
  console.log(`  data: ${metin}`);
  console.log(`  kaybolan: ${metin === "null" ? "tüm yanıt" : (metin.match(/null/g) ?? []).length + " alan"}   hata: ${(s.errors ?? []).map((h) => `${h.yol} (${h.kod})`).join(", ") || "yok"}`);
}
```

```
iadeTarihi çöküyor (Tarih — boş olabilir)
  data: {"oduncler":[{"id":"O-1","durum":"ACIK","iadeTarihi":"2026-03-20","uye":{"ad":"Ayse Yilmaz"}},{"id":"O-2","durum":"ACIK","iadeTarihi":null,"uye":{"ad":"Deniz Kaya"}}]}
  kaybolan: 1 alan   hata: .oduncler[1].iadeTarihi (yukari_akis)

durum çöküyor ([Odunc!]! içinde, Durum!)
  data: null
  kaybolan: tüm yanıt   hata: .oduncler[1].durum (yukari_akis)

durum çöküyor ([Odunc] içinde, Durum!)
  data: {"esnekOduncler":[{"id":"O-1","durum":"ACIK","iadeTarihi":"2026-03-20","uye":{"ad":"Ayse Yilmaz"}},null]}
  kaybolan: 1 alan   hata: .esnekOduncler[1].durum (yukari_akis)
```

Üç satır aynı çökmeyi, üç ayrı şema işaretlemesiyle gösteriyor.

Boş değer alabilen bir alan çöktüğünde yalnız o alan boşalır; ikinci kayıt yerinde kalır.
Zorunlu bir alan çöktüğünde boşluk yukarı taşınır ve **en yakın boş değer alabilen düzeyde**
durur. `[Odunc!]!` işaretlemesinde ne öğe ne de liste boş olabilir; boşluk kök alana kadar
çıkar ve `data` tümüyle null olur. Aynı çökme `[Odunc]` işaretlemesinde ikinci öğede durur
ve yanıtın kalanı kurtulur.

Bu, `!` işaretinin bir tasarım kararı olduğunu gösteriyor. Zorunluluk istemcinin işini
kolaylaştırır — boş denetimi yazmaz — ama bunun bedeli, tek bir alanın çökmesinin bütün
yanıtı götürebilmesidir. Ölçüt şudur: **bir alan gerçekten hiçbir koşulda boş olamıyorsa**
zorunlu yazılır. Başka bir servisten gelen, hesaplanan ya da yetkiye bağlı alanlar bu ölçütü
karşılamaz.

## Özet

- Sorgu temelli yanıt iki bölümlüdür: `data` çözülebilen alanları, `errors` çözülemeyenleri
  taşır ve ikisi aynı yanıtta bulunur.
- Hata kaydındaki yol, yanıt ağacındaki yeri gösterir ve istemcinin hatayı doğru alana
  bağlamasını sağlar.
- Yetki denetimi çözücülerin içine değil, onları sarmalayan bir katmana yazılır; kural hem
  isteği yapanı hem üzerinde karar verilen kaydı görmek zorundadır.
- Aynı sorgu farklı bağlamlarda farklı alanları boş bırakır; iki kural farklı ölçütlere
  dayanabilir, bu alan düzeyi tanımın kazandırdığı şeydir.
- Yetki hatası alanın var olduğunu söyler ama değerini vermez; alanın varlığını gizlemek
  gerekiyorsa şemadan çıkarılmalıdır.
- Zorunlu alanda çökme, boşluğu en yakın boş değer alabilen düzeye kadar taşır; `[Odunc!]!`
  altında tek bir alan bütün yanıtı, `[Odunc]` altında yalnız bir öğeyi götürür.

## Sonraki Adım

Yetkilendirme sorgunun **neyi görebileceğini** sınırladı ama **ne kadar isteyebileceğini**
sınırlamadı. Sayfalama dersi bunu açıkta bırakmıştı: her alan kendi sınırına uysa da iç içe
listelerin çarpımı yanıtı büyütebiliyordu. Üstelik tip sistemi çevrimler içerir — ödünç
kaydından üyeye, üyeden ödünçlerine, oradan yeniden üyeye gidilebilir — ve bu, sorgu
derinliğine bir sınır konmadığında yürütmeyi durduracak bir yol bırakmaz. Sonraki ders sorgu
derinliğini ve maliyetini çalıştırmadan önce hesaplar, sınırı aşan sorguyu reddeder ve
sınırın nereden seçileceğini ölçer.
