---
title: 'GraphQL İstemcisi'
source: 'https://academia.sh/tr/kurslar/frontend-mimarisi/graphql-istemcisi'
course: 'Uygulama Mimarisi: Yönlendirme, Durum ve Veri'
language: tr
updated: '2026-08-17T18:09:20+00:00'
license: 'CC BY-SA 4.0'
---

# GraphQL İstemcisi

Alan seçiminin istemciye geçmesi, tekrar eden alan kümelerinin parçalarla adlandırılması, iç içe yanıtın kimlik tabanlı bir depoya normalleştirilmesi ve kısmi başarının hata sözleşmesine katılması.

Önceki ders bir gerilimle bitti: kaynağın döndürdüğü alanlarla ekranın istediği alanlar
çakışmıyor. Kuzey Yamaç istasyon kartı istasyonun adını, son ölçümü ve son iki ölçüm
satırını birlikte gösteriyor; REST kaynak modelinde bu üç ayrı adres demek. Alternatif —
ekrana özel bir uç nokta — sunucuyu arayüzün biçimine bağlar.

Bu ders, kararı yer değiştiren bir yaklaşımı ele alır. GraphQL'de sunucu bir **şema**
yayımlar, istemci o şemadan hangi alanları istediğini yazar. Tek uç nokta, tek istek, tam
olarak istenen alanlar. Kazanç açık; bedel, istemcinin şimdi kendi başına çözmesi gereken
iki yeni problem: aynı alan kümesinin ekranlar arasında paylaşılması ve gelen iç içe
yanıtın saklanması.

## Alan Seçiminin İstemciye Geçmesi

Bir sorgu belgesi, şemadaki alanların bir alt ağacını seçer. `istasyon(kod: "KY-01")`
alanının altında `ad` ve `yukseklik` istenirse yanıt yalnız o ikisini taşır; `sonOlcum`
istenirse aynı istekte o da gelir. İç içe alanlar tek gidiş dönüşte toplanır, bu yüzden
gecikmenin ekran başına çarpanı ortadan kalkar.

Üç yapısal fark istek katmanını etkiler. Birincisi, **adres artık ayırt edici değildir**:
tüm sorgular aynı uç noktaya gider ve genellikle POST ile gönderilir. Önceki dersin
"adres bir önbellek anahtarıdır" kuralı burada geçersizdir; anahtar, sorgu belgesi ile
değişkenlerinin birleşiminden üretilir.

İkincisi, **başarı ve başarısızlık aynı yanıtta bulunabilir**. Sorgudaki bir alan
çözümlenemezse HTTP durumu 200 kalır, gövde hem kısmi `data` hem de `errors` listesi
taşır. Bu, önceki dersteki dört sınıflı hata sözleşmesinin kapsamadığı bir durumdur ve
ders sonunda ele alınacaktır.

Üçüncüsü, **sorgunun maliyeti istemcinin elindedir**. Derin bir iç içe seçim, sunucuda
kayıt başına ek okuma üretebilir; şemada bir liste alanının altında yine bir liste alanı
seçmek maliyeti çarpar. Sunucu bunu derinlik ve karmaşıklık sınırlarıyla dizginler;
istemci tarafında bunun karşılığı, ekranın gerçekten gösterdiği alanları istemektir.

## Parça: Alan Kümesinin Adı

Aynı alan kümesi birden çok ekranda görünür. Ölçüm satırı hem istasyon kartında hem
ölçüm geçmişi listesinde aynı beş alanı ister. Bu kümeyi her sorguya elle yazmak, bir alan
eklendiğinde bazı ekranların geride kalması demektir.

**Sorgu parçası (fragment)**, bir tür üzerinde adlandırılmış bir alan kümesidir. Sorgu
parçayı adıyla çağırır, belge kurulurken tanımı yanına eklenir. Aşağıdaki dosya bu
birleştirmeyi saf dizgi işlemi olarak yapar: parça çağrılarını bulur, bağımlılıkları
özyinelemeli izler ve her tanımı belgeye bir kez koyar.

```js
// parca-birlestirme.mjs — sorgu parcalarindan belge kurma
const PARCALAR = {
  IstasyonOzeti: {
    tur: "Istasyon",
    govde: ["__typename", "kod", "ad", "yukseklik"],
  },
  OlcumSatiri: {
    tur: "Olcum",
    govde: ["__typename", "kimlik", "zaman", "deger", "birim"],
  },
  IstasyonKarti: {
    tur: "Istasyon",
    govde: ["...IstasyonOzeti", "sonOlcum { ...OlcumSatiri }"],
  },
};

// Bir govdedeki parca cagrilarini bulur: "...Ad" bicimindeki her sozcuk.
const cagrilar = (satirlar) =>
  satirlar.flatMap((s) => [...s.matchAll(/\.\.\.(\w+)/g)].map((e) => e[1]));

function gerekliParcalar(kokler, toplanan = new Set()) {
  for (const ad of kokler) {
    if (toplanan.has(ad)) continue;          // ayni parca iki kez yazilmaz
    const parca = PARCALAR[ad];
    if (!parca) throw new Error(`tanimsiz parça: ${ad}`);
    toplanan.add(ad);
    gerekliParcalar(cagrilar(parca.govde), toplanan); // ic ice bagimliliklar
  }
  return toplanan;
}

function belgeKur(ad, govde) {
  const adlar = gerekliParcalar(cagrilar(govde));
  const tanimlar = [...adlar].map((a) => {
    const p = PARCALAR[a];
    return `fragment ${a} on ${p.tur} {\n  ${p.govde.join("\n  ")}\n}`;
  });
  return [`query ${ad} {\n  ${govde.join("\n  ")}\n}`, ...tanimlar].join("\n\n");
}

const belge = belgeKur("IstasyonSayfasi", [
  "istasyon(kod: \"KY-01\") {",
  "  ...IstasyonKarti",
  "  olcumler(son: 2) { ...OlcumSatiri }",
  "}",
]);

console.log(belge);
console.log("---");
console.log("belgeye giren parça sayısı:", (belge.match(/^fragment /gm) ?? []).length);
```

```
query IstasyonSayfasi {
  istasyon(kod: "KY-01") {
    ...IstasyonKarti
    olcumler(son: 2) { ...OlcumSatiri }
  }
}

fragment IstasyonKarti on Istasyon {
  ...IstasyonOzeti
  sonOlcum { ...OlcumSatiri }
}

fragment IstasyonOzeti on Istasyon {
  __typename
  kod
  ad
  yukseklik
}

fragment OlcumSatiri on Olcum {
  __typename
  kimlik
  zaman
  deger
  birim
}
---
belgeye giren parça sayısı: 3
```

`OlcumSatiri` belgede iki yerden çağrılıyor — biri doğrudan sorgudan, biri
`IstasyonKarti` içinden — ama tanımı bir kez yazılıyor. Bu, tekilleştirmenin gerçek
faydasıdır: alan kümesi tek yerde durur.

Parçanın asıl tasarım değeri, **alan ihtiyacının onu kullanan parçanın yanında
bildirilmesidir**. Ölçüm satırını çizen kod hangi alanlara ihtiyaç duyduğunu kendi
parçasında yazar; sayfa sorgusu bunu bilmez, yalnız çağırır. Ekrana yeni bir alan
eklendiğinde tek bir yer değişir ve o parçayı kullanan her sorgu alanı almaya başlar.

Her parçaya `__typename` alanının konması rastgele değildir. Bir sonraki bölümdeki depo,
kayıtları türle kimliğin birleşiminden anahtarlar; tür bilgisi yanıtta yoksa anahtar
üretilemez.

## Önbellek Normalizasyonu

Yanıt iç içe bir ağaçtır. Aynı ölçüm kaydı hem `sonOlcum` altında hem `olcumler`
listesinde görünür; iki kopya olarak saklanırsa biri güncellenip diğeri eskiyebilir.
**Normalizasyon**, ağacı kimlik anahtarlı düz bir depoya çevirir: her kayıt bir kez durur,
ağaçtaki yerinde ise ona bir başvuru kalır.

```js
// normalizasyon.mjs — ic ice yaniti duz depoya cevirme, birlestirme ve geri okuma
const KIMLIK_ALANI = { Istasyon: "kod", Olcum: "kimlik" };

const anahtar = (nesne) => {
  const alan = KIMLIK_ALANI[nesne?.__typename];
  return alan && nesne[alan] != null ? `${nesne.__typename}:${nesne[alan]}` : null;
};

function normalize(deger, depo) {
  if (Array.isArray(deger)) return deger.map((e) => normalize(e, depo));
  if (deger === null || typeof deger !== "object") return deger;

  const alanlar = {};
  for (const [ad, alt] of Object.entries(deger)) alanlar[ad] = normalize(alt, depo);

  const kimlik = anahtar(deger);
  if (!kimlik) return alanlar;                            // kimliksiz nesne yerinde kalir
  depo[kimlik] = { ...(depo[kimlik] ?? {}), ...alanlar };  // alan bazinda birlestirme
  return { __ref: kimlik };
}

// Okuma her zaman bir secim kumesiyle yapilir; depo cevrimli oldugu icin
// secimsiz dolasma biter bir sey degildir.
function oku(secim, deger, depo) {
  if (Array.isArray(deger)) return deger.map((e) => oku(secim, e, depo));
  if (deger === null || typeof deger !== "object") return deger;
  const kaynak = deger.__ref ? depo[deger.__ref] : deger;
  if (!kaynak) return { __eksik: deger.__ref };            // depoda yok
  const cikti = {};
  for (const [ad, alt] of Object.entries(secim)) {
    if (!(ad in kaynak)) { cikti[ad] = { __eksik: ad }; continue; }
    cikti[ad] = alt === true ? kaynak[ad] : oku(alt, kaynak[ad], depo);
  }
  return cikti;
}

const depo = {};
const kokler = {};

// --- Birinci sorgu: istasyon karti + son iki olcum --------------------------
const yanit1 = {
  istasyon: {
    __typename: "Istasyon", kod: "KY-01", ad: "Kuzey Yamaç", yukseklik: 1840,
    sonOlcum: {
      __typename: "Olcum", kimlik: "o-114", zaman: "2026-01-14T06:00Z",
      deger: -4.2, birim: "C",
      istasyon: { __typename: "Istasyon", kod: "KY-01", ad: "Kuzey Yamaç" },
    },
    olcumler: [
      { __typename: "Olcum", kimlik: "o-114", zaman: "2026-01-14T06:00Z", deger: -4.2, birim: "C" },
      { __typename: "Olcum", kimlik: "o-113", zaman: "2026-01-13T06:00Z", deger: -1.8, birim: "C" },
    ],
  },
};
kokler['istasyon(kod:"KY-01")'] = normalize(yanit1.istasyon, depo);

console.log("depo anahtarları:", Object.keys(depo).join("  "));
console.log("KY-01           :", JSON.stringify(depo["Istasyon:KY-01"]));
console.log("o-114           :", JSON.stringify(depo["Olcum:o-114"]));

// --- Ikinci sorgu: baska bir ekran, ayni olcumu farkli alanlarla ister ------
const yanit2 = { olcum: { __typename: "Olcum", kimlik: "o-114", deger: -4.4, kaydeden: "gözlemci-3" } };
kokler['olcum(kimlik:"o-114")'] = normalize(yanit2.olcum, depo);

console.log("--- ikinci yanıttan sonra ---");
console.log("o-114           :", JSON.stringify(depo["Olcum:o-114"]));
console.log("kayıt sayısı    :", Object.keys(depo).length);

// --- Birinci ekran, kendi secim kumesiyle depodan yeniden okunur -----------
const KART = {
  kod: true, ad: true,
  sonOlcum: { kimlik: true, deger: true, birim: true },
  olcumler: { kimlik: true, deger: true },
};
console.log("--- birinci ekranın yeni görüntüsü ---");
console.log(JSON.stringify(oku(KART, kokler['istasyon(kod:"KY-01")'], depo)));

// --- Deponun tasimadigi alan istenirse ------------------------------------
const RAPOR = { kod: true, sonOlcum: { kimlik: true, sapma: true } };
console.log("--- eksik alan isteyen ekran ---");
console.log(JSON.stringify(oku(RAPOR, kokler['istasyon(kod:"KY-01")'], depo)));
```

```
depo anahtarları: Istasyon:KY-01  Olcum:o-114  Olcum:o-113
KY-01           : {"__typename":"Istasyon","kod":"KY-01","ad":"Kuzey Yamaç","yukseklik":1840,"sonOlcum":{"__ref":"Olcum:o-114"},"olcumler":[{"__ref":"Olcum:o-114"},{"__ref":"Olcum:o-113"}]}
o-114           : {"__typename":"Olcum","kimlik":"o-114","zaman":"2026-01-14T06:00Z","deger":-4.2,"birim":"C","istasyon":{"__ref":"Istasyon:KY-01"}}
--- ikinci yanıttan sonra ---
o-114           : {"__typename":"Olcum","kimlik":"o-114","zaman":"2026-01-14T06:00Z","deger":-4.4,"birim":"C","istasyon":{"__ref":"Istasyon:KY-01"},"kaydeden":"gözlemci-3"}
kayıt sayısı    : 3
--- birinci ekranın yeni görüntüsü ---
{"kod":"KY-01","ad":"Kuzey Yamaç","sonOlcum":{"kimlik":"o-114","deger":-4.4,"birim":"C"},"olcumler":[{"kimlik":"o-114","deger":-4.4},{"kimlik":"o-113","deger":-1.8}]}
--- eksik alan isteyen ekran ---
{"kod":"KY-01","sonOlcum":{"kimlik":"o-114","sapma":{"__eksik":"sapma"}}}
```

Çıktı dört şey söylüyor.

**Depo düzdür ve tekrar barındırmaz.** İlk yanıtta ölçüm `o-114` üç yerde görünüyordu;
depoda tek kayıt var, diğer yerlerde `__ref` başvurusu duruyor. İstasyon da aynı biçimde
tekilleşti: ölçümün içindeki gömülü istasyon nesnesi ayrı bir kayıt üretmedi, var olanla
birleşti.

**Birleştirme alan bazındadır.** İkinci sorgu ölçümün yalnız iki alanını istedi ama depo
kaydı beş alanını korudu ve yeni gelen `kaydeden` alanını ekledi. Yeni değer `-4.4`,
eskisinin üzerine yazıldı. Kısmi bir yanıt, saklanan kaydı budamaz.

**Bir ekranın verisi başka bir ekranın isteğiyle tazelenir.** İstasyon kartı yeniden
istek atmadı; depo değiştiği için kartın okuduğu değer `-4.4` oldu. Normalize edilmemiş
bir önbellekte bu güncelleme yalnız ikinci ekrana yansırdı.

**Okuma bir seçim kümesi gerektirir.** Depo çevrimlidir: istasyon ölçüme, ölçüm istasyona
başvurur. Seçimsiz bir dolaşma sonlanmaz. Okuyucu her düzeyde hangi alanların istendiğini
bilmek zorundadır — yani sorgu belgesi yalnız istek anında değil, okuma anında da gerekir.
Son satır bunun devamını gösterir: depoda olmayan bir alan istendiğinde okuma **eksik**
olduğunu bildirir; bu durumda sunucuya gitmek gerekir.

## Normalizasyonun Sınırları

Depo yalnız kimliği olan kayıtları tekilleştirir. Kimliksiz nesneler — bir toplam, bir
sayfa bilgisi, hesaplanmış bir özet — ait oldukları kaydın içinde kalır. Şemada bir tür
için kimlik alanı yoksa ya da alan adı türden türe değişiyorsa, eşleme tablosunun (burada
`KIMLIK_ALANI`) elle bakımı gerekir.

İkinci sınır listelerdir. `olcumler(son: 2)` ile `olcumler(son: 20)` aynı alanın iki farklı
sonucudur; ikisi tek bir alan değeri altında saklanamaz. Bu yüzden liste alanları
**argümanlarıyla birlikte** anahtarlanır. Sayfalama söz konusuysa bir de birleştirme
kuralı gerekir: yeni sayfa öncekinin üstüne mi yazılacak, sonuna mı eklenecek. Bu karar
alan bazında verilir ve depo bunu bilemez, uygulamanın söylemesi gerekir.

Üçüncü sınır silmedir. Bir ölçüm silindiğinde depo kaydını atmak yetmez; o kayda başvuran
listelerin de temizlenmesi gerekir. Aksi hâlde okuma `__eksik` döndürür ve ekranda boşluk
belirir. Değişiklik işlemlerinden sonra hangi kök alanların geçersizleştirileceği,
Durum Yönetimi konusundaki geçersizleştirme kuralının bu bağlamdaki karşılığıdır.

## Kısmi Başarı ve Hata Sözleşmesi

Önceki dersin sözleşmesi dört sınıf tanıyordu: ağ, HTTP, gövde, iptal. Sorgu tabanlı
erişim beşinci bir durum getirir: **taşıma başarılı, alan başarısız**. Yanıt 200 döner,
`data` içinde istenen alanların bir kısmı `null` olur ve `errors` listesi hangi yolda ne
olduğunu söyler.

Bu yanıtı tek bir "hata" ya da tek bir "başarı" saymak ikisi de yanlıştır. İstek katmanı
üç bilgiyi ayrı taşımalıdır: elde edilen kısmi veri, başarısız alan yolları ve bu
başarısızlığın kullanıcıya gösterilip gösterilmeyeceği. İstasyon adı geldi ama son ölçüm
alanı boş döndüyse kart çizilebilir; ölçüm bölümünün yerine hata bildirimi konur. Tüm
ekranı hata durumuna düşürmek, elde olan veriyi çöpe atar.

Ayrım şu soruyla yapılır: **ekranın anlamlı olması için hangi alanlar zorunlu?** Bu soru
sunucu tarafından yanıtlanamaz; hangi alanın vazgeçilebilir olduğunu ekranı çizen kod
bilir. Bu yüzden kısmi başarı, istek katmanının otomatik olarak karara bağlayabileceği bir
şey değildir — taşınır, karar çağrı noktasında verilir.

Alan düzeyindeki hataların yanında protokol düzeyindeki hatalar da vardır: geçersiz sorgu
belgesi, şemada olmayan alan, tip uyumsuzluğu. Bunlar sorgu hiç çalıştırılmadan bildirilir
ve bir kullanıcı hatası değil, bir program hatasıdır; kullanıcıya gösterilecek iletisi
yoktur, geliştirme sırasında yakalanması gerekir.

## Özet

- Sorgu tabanlı erişimde alan seçimi istemciye geçer; tek uç nokta ve tek istek, ekran
  başına gidiş dönüş sayısını ekranın karmaşıklığından bağımsızlaştırır.
- Adres artık ayırt edici değildir; önbellek anahtarı sorgu belgesi ile değişkenlerin
  birleşiminden üretilir.
- Sorgu parçası, bir tür üzerinde adlandırılmış alan kümesidir; alan ihtiyacı onu kullanan
  kodun yanında bildirilir ve belgeye bir kez yazılır.
- Normalizasyon iç içe yanıtı tür ile kimliğin birleştiği anahtarlarla düz bir depoya
  çevirir; birleştirme alan bazındadır ve bir ekranın isteği diğerinin verisini tazeler.
- Normalleştirilmiş depo çevrimlidir; okuma her zaman bir seçim kümesiyle yapılır ve
  depoda bulunmayan alan eksik olarak bildirilir.
- Kısmi başarı beşinci bir hata durumudur: kısmi veri, başarısız alan yolları ve
  zorunluluk kararı ayrı taşınır; hangi alanın vazgeçilebilir olduğunu ekran bilir.

## Sonraki Adım

Buraya kadar veri her zaman istemcinin sorduğu anda geldi: sayfa açıldı, istek atıldı,
yanıt işlendi. Kuzey Yamaç istasyonu ise ölçümü kimse sormadan üretir. Ölçüm geçmişi
ekranı açıkken yeni bir kayıt oluşursa, kullanıcının bunu görmesi için sayfayı yenilemesi
gerekmemeli. Bu, veri akışının yönünü tersine çevirir: bağlantıyı istemci kurar ama
zamanlamayı sunucu belirler. Sonraki ders bu tersine akışın iki taşıyıcısını — tek yönlü
olay akışını ve çift yönlü soketi — ve gelen olayların depoya nasıl işleneceğini ele alır.
