Skip to content

Bölüm 8 / 11

GraphQL temelleri

Şema, query, mutation, resolver; REST'ten farkı.

13 dakikalık okuma

3. derste "istemci her ekranda farklı alan kümesi istiyorsa" diye bir sınır çizmiş, 4. derste alan seçiminin (sparse fieldsets) belgelemeyi ve önbelleği nasıl zorlaştırdığını görmüştük. GraphQL tam bu boşluğu doldurmak için doğdu: istemci ne istediğini kendisi tarif eder, sunucu tam olarak onu döndürür. Bu ders GraphQL'i sıfırdan kuruyor ve daha önemlisi, kazandırdığının yanında ne kaybettirdiğini gösteriyor — çünkü mülakatta ayırt edici soru "GraphQL nedir" değil, "ne zaman kullanmazsınız"dır.

01Ne öğreneceksin

  • Aşırı ve eksik veri çekme (over/under-fetching) problemi
  • Şema ve tip sistemi: SDL, Query, Mutation, nullable kuralı
  • Sorgu dili: alan seçimi, argüman, değişken, fragment, takma ad
  • Yanıt biçimi: data + errors ve kısmi başarı
  • Çözücü (resolver) modeli ve yürütmenin ağaç yapısı
  • N+1 sorununun nereden doğduğu
  • REST ile karşılaştırma: önbellek, hata, durum kodu, araçlar
  • Güvenlik: sorgu derinliği, karmaşıklık ve iç gözlem (introspection)

02Çözdüğü problem

Bir mobil uygulamanın sipariş detay ekranını düşün: sipariş bilgisi, müşterinin adı, ilk üç satırın ürün adları ve kargo durumu gerekiyor. REST ile tipik akış:

GET /api/siparisler/42          → 40 alanlık nesne (2 tanesi lazım)
GET /api/musteriler/17          → 25 alanlık nesne (1 tanesi lazım)
GET /api/siparisler/42/satirlar → 20 satır × 15 alan
GET /api/urunler/101, /102, /103 → 3 ayrı istek
GET /api/kargolar?siparis=42    → 1 istek

İki ayrı sorun var. Aşırı veri çekme (over-fetching): gelen alanların çoğu kullanılmıyor; mobil ağda bu doğrudan pil ve veri tüketimidir. Eksik veri çekme (under-fetching): tek ekran için altı gidiş-dönüş gerekiyor ve her biri gecikme ekliyor — yüksek gecikmeli mobil bağlantılarda toplam süre bunun toplamı olur.

REST'in bu soruna üç cevabı vardır ve hepsinin bedeli vardır: alan seçimi (?alanlar=no,urun — şema değişkenleşir, önbellek anahtarı çeşitlenir, belgeleme zorlaşır); bileşik uçlar (/siparisler/42/detay-ekrani — her ekran için ayrı uç, sözleşme şişer); BFF (Backend for Frontend — her istemci türü için ayrı bir ara servis; 12 ve 19. hatlarda göreceksin).

GraphQL dördüncü cevabı verir: tek uç nokta, istemcinin tarif ettiği yanıt.

POST /graphql

query SiparisDetay($no: Int!) {
  siparis(no: $no) {
    no
    durum
    musteri { ad }
    satirlar(ilk: 3) { urun { ad } adet }
    kargo { durum tahminiTeslim }
  }
}

Yanıt, sorgunun şeklinin aynısıdır — istenmeyen hiçbir alan yoktur, tek gidiş dönüşte her şey gelir:

{
  "data": {
    "siparis": {
      "no": 42, "durum": "Onaylandi",
      "musteri": { "ad": "A. Yılmaz" },
      "satirlar": [ { "urun": { "ad": "kitap" }, "adet": 2 } ],
      "kargo": { "durum": "Yolda", "tahminiTeslim": "2026-09-02" }
    }
  }
}

03Şema ve tip sistemi

GraphQL'in merkezinde şema vardır: sunucunun neyi sunduğunu tanımlayan, katı tipli sözleşme. Şema Tanım Dili (SDL) ile yazılır:

type Siparis {
  no: Int!                      # ! = null OLAMAZ
  durum: SiparisDurumu!
  tutar: Decimal!
  musteri: Musteri!             # ilişki: iç içe tip
  satirlar(ilk: Int = 20): [SiparisSatiri!]!
  kargo: Kargo                  # null olabilir: kargo henüz oluşmamış olabilir
}

type SiparisSatiri { urun: Urun!  adet: Int!  birimFiyat: Decimal! }
type Musteri       { id: ID!  ad: String!  eposta: String! }

enum SiparisDurumu { YENI  ONAYLANDI  KARGOLANDI  IPTAL }

input SiparisOlusturGirdisi { urun: String!  adet: Int!  musteriId: ID! }

type Query {
  siparis(no: Int!): Siparis
  siparisler(durum: SiparisDurumu, ilk: Int = 20, imlec: String): SiparisBaglantisi!
}

type Mutation {
  siparisOlustur(girdi: SiparisOlusturGirdisi!): Siparis!
  siparisIptal(no: Int!): Siparis!
}

Bilinmesi gereken kurallar:

  • Her şey varsayılan olarak null olabilir; zorunluluk ! ile eklenir. Bu, C#'ın tersidir ve şaşırtır. Gerekçesi dayanıklılıktır: bir alan çözülemezse (bağımlı servis çöktü) o alan null döner ve sorgunun geri kalanı çalışmaya devam eder. ! koyduğun alan çözülemezse hata yukarı yayılır ve üst nesneyi de null yapar — yani ! bir katılık taahhüdüdür, bedeli vardır.
  • Üç kök tip vardır: Query (okuma), Mutation (yazma) ve Subscription (sunucudan akan olaylar; genelde WebSocket üzerinden).
  • Girdi tipleri ayrıdır (input): çıktı tipi girdi olarak kullanılamaz. Bu, 5. hattaki "istek DTO'su ile yanıt DTO'su ayrıdır" kuralının dile gömülmüş hâlidir.
  • Skaler tipler sınırlıdır: Int, Float, String, Boolean, ID. Tarih ve Decimal gibi tipler özel skaler olarak tanımlanır — 2. dersteki "JSON'da tarih tipi yok" sorununun burada da karşılığı vardır.
  • interface ve union çok biçimliliği taşır; istemci ... on Tip { } ile hangi tipte ne isteyeceğini söyler.

Şema aynı zamanda belgedir: 7. derste OpenAPI ile ayrı bir belge üretmek gerekiyordu, GraphQL'de şemanın kendisi makine okunur sözleşmedir ve iç gözlem (introspection) ile sorgulanabilir. Araçlar (GraphiQL, Banana Cake Pop, Apollo Studio) otomatik tamamlamayı buradan alır.

04Sorgu dili ve yanıt

Sorgunun dört yapısal öğesi vardır:

query SiparisDetay($no: Int!, $satirSayisi: Int = 3) {     # 1) adlandırılmış işlem + değişkenler
  aktif: siparis(no: $no) {                               # 2) takma ad (alias)
    ...siparisOzeti                                       # 3) fragment kullanımı
    satirlar(ilk: $satirSayisi) { urun { ad } adet }      # 4) argüman
  }
  gecmis: siparis(no: 41) { ...siparisOzeti }             # aynı alan, iki kez, farklı adla
}

fragment siparisOzeti on Siparis {                        # tekrar kullanılabilir alan kümesi
  no durum tutar
}

Değişkenler önemlidir: sorgu metnini istemcide birleştirmek yerine parametre göndermek, hem önbelleklemeyi hem güvenliği sağlar (12. hattaki enjeksiyon mantığının karşılığı). Takma ad, aynı alanı farklı argümanlarla iki kez istemeyi mümkün kılar. Fragment, alan kümelerini tekrar kullanılabilir hâle getirir ve büyük sorguları yönetilebilir tutar.

Yanıt biçimi sabittir ve iki üst düzey alanı vardır:

{
  "data":   { "siparis": { "no": 42, "kargo": null } },
  "errors": [
    {
      "message": "Kargo servisi yanıt vermedi",
      "path": ["siparis", "kargo"],
      "extensions": { "code": "BAGIMLILIK_HATASI" }
    }
  ]
}

Bu, GraphQL'in en tartışmalı yanına götürür: hata olsa bile HTTP durum kodu genellikle 200'dür. 5. derste "hata 200 içinde gönderilmez" diye kesin bir kural koymuştuk; GraphQL bu kuralı bilinçli olarak çiğner, çünkü tek bir sorgu kısmen başarılı olabilir: on alandan dokuzu geldi, biri gelmedi. Tek bir HTTP kodu bu durumu anlatamaz.

Bedeli ise doğrudan 5. dersteki kazanımların kaybıdır: ara katmanlar, ağ geçitleri ve izleme araçları hata oranını göremez; istemci kütüphaneleri istisna fırlatmaz; ProblemDetails gibi standart bir hata sözleşmesi yoktur — hata anlamı extensions.code alanına kendi koyduğun bir sözleşmeyle taşınır. Üretimde bunun karşılığı, GraphQL hatalarını ayrıca ölçmektir: HTTP 5xx metriği yeterli değildir, errors alanının doluluğu da bir gösterge olarak toplanmalıdır.

05Çözücüler ve N+1'in doğuşu

Sunucu tarafında GraphQL şu modelle çalışır: şemadaki her alanın bir çözücüsü (resolver) vardır. Yürütme, sorgu ağacını kökten yapraklara doğru gezer ve her alan için ilgili çözücüyü çağırır.

query { siparisler(ilk: 20) { no  musteri { ad } } }

Yürütme:
  siparisler          → 1 çağrı  (20 sipariş döner)
  ├─ no               → alan zaten nesnede, ek iş yok
  └─ musteri          → HER SİPARİŞ İÇİN AYRI ÇAĞRI → 20 çağrı
                        ────────────────────────────────
                        toplam 21 veritabanı sorgusu

Bu, meşhur N+1 sorunudur: bir sorgu listeyi getirir, sonra listedeki her öğe için bir sorgu daha çalışır. GraphQL'e özgü değildir — 7. hatta ORM tarafında aynısını göreceksin — ama GraphQL'de daha kolay ortaya çıkar, çünkü sorgunun şeklini istemci belirler: senin hiç öngörmediğin bir iç içe geçme, üretimde birden bire yüzlerce sorgu üretebilir.

Çözümü toplu yükleyici (DataLoader) kalıbıdır: aynı yürütme adımındaki tekil istekler biriktirilir ve tek bir toplu sorguya çevrilir (WHERE id IN (17, 18, 19, ...)). Böylece 21 sorgu 2 sorguya iner. .NET tarafında bunun uygulamasını 9. derste HotChocolate ile yazacaksın.

Aynı esneklik maliyet denetimi sorununu da doğurur. Şema iç içe ilişkiler içerdiği için, kötü niyetli ya da dikkatsiz bir istemci sonsuza yakın derinlikte sorgu yazabilir:

query { siparis(no: 1) { musteri { siparisler { musteri { siparisler { ... } } } } } }

REST'te bir uç noktanın maliyeti aşağı yukarı sabittir ve 4. derste öğrendiğin sayfa boyutu sınırıyla kontrol edilir. GraphQL'de tek bir isteğin maliyeti sınırsız olabilir. Bu yüzden üretimde üç koruma zorunludur: derinlik sınırı, karmaşıklık puanı (her alana bir maliyet verilip toplam sınırlanır) ve kalıcı sorgular (persisted queries — yalnız önceden onaylanmış sorgular çalıştırılır). Ayrıca iç gözlem üretimde kapatılır ya da yetkiye bağlanır: açık bırakmak, 7. derste konuştuğumuz "Swagger'ı üretimde açık bırakmak" ile aynı sınıfta bir bilgi ifşasıdır.

06REST'e karşı: karar

ÖlçütRESTGraphQL
Yanıt şekliSunucu belirlerİstemci belirler
Uç nokta sayısıÇokTek (/graphql)
Ekran başına istekBirkaçGenelde bir
HTTP önbelleğiDoğal (ETag, Cache-Control)Çalışmaz; istemci tarafı önbellek gerekir
Durum kodlarıAnlamlıÇoğunlukla 200
Hata sözleşmesiProblemDetails standardıerrors + kendi extensions kuralın
İstek maliyetiÖngörülebilirSınırlanmazsa öngörülemez
SürümlemeSürüm çıkarılırAlan ekle / eskiyi @deprecated işaretle
Dosya yüklemeDoğalEk sözleşme gerekir
Araç ve altyapı uyumuHer yerdeAğ geçidi/WAF desteği sınırlı
Öğrenme ve işletme maliyetiDüşükOrta-yüksek

Sürümleme farkı özellikle dikkate değer: 6. dersteki tüm tartışma GraphQL'de biçim değiştirir. İstemci hangi alanları istediğini kendisi söylediği için, şemaya alan eklemek hiçbir istemciyi kırmaz — kimse istemediği alanı almaz. Alan kaldırmak yine kırıcıdır, ama şimdi elinde güçlü bir araç vardır: @deprecated işaretlemesi ve alan bazında kullanım ölçümü. "Bu alanı son 90 günde kimse sormadı" bilgisi, REST'te ancak zahmetle elde edilirken GraphQL'de doğaldır. Bu yüzden GraphQL API'leri genelde sürümsüz yaşar.

Ne zaman GraphQL: çok sayıda ve farklı istemci (web, mobil, iş ortağı) aynı veriyi farklı şekillerde istiyorsa; veri modeli grafik gibi bağlıysa ve ekranlar iç içe veri gösteriyorsa; ekipler ön yüz tarafında hızlı iterasyon yapıyor ve her ekran için arka uçtan yeni uç nokta beklemek istemiyorsa.

Ne zaman REST: istemci tek ve sen kontrol ediyorsan; kaynak modeli basit CRUD'sa; HTTP önbelleği ve ara katman uyumu senin için kritikse (kamuya açık, yüksek trafikli, önbelleklenebilir içerik); dosya yükleme/indirme ağırlıklıysa; ekipte GraphQL deneyimi yoksa ve işletme yükünü kaldıracak kapasite yoksa.

Doğru cevap çoğu zaman ikisi birdendir: iç servisler arası REST ya da gRPC, istemciye bakan yüzeyde GraphQL. Mülakatta "GraphQL REST'in yerini alır mı" sorusuna verilecek en iyi cevap budur — biri diğerinin yerini almaz, farklı kısıtlar için farklı araçlardır.

07Mini alıştırma — Şemayı ve sorguyu tasarla

Yaklaşık 20 dakika · kâğıt + herkese açık bir GraphQL denemesi

  1. Problemi ölç: 5. hattaki servisinde bir "sipariş detay" ekranı için kaç REST çağrısı ve kaç kullanılmayan alan gerektiğini say. Aşırı/eksik veri çekme somut olarak ne kadar?
  2. Şemayı yaz: aynı alan için SDL yaz — Siparis, Musteri, SiparisSatiri, bir enum ve Query kökü. Hangi alanlara ! koydun, neden?
  3. Null kararı: kargo alanını önce Kargo!, sonra Kargo yap. Kargo servisi çöktüğünde her iki durumda yanıt ne olur?
  4. Sorguyu yaz: yalnız ekranın ihtiyacı olan alanları isteyen sorguyu yaz; değişken, fragment ve takma ad kullan.
  5. Denemeli gör: herkese açık bir GraphQL uç noktasında (ör. bir örnek/eğitim API'si) aynı sorguyu çalıştır; iç gözlemle şemayı gezin ve otomatik tamamlamanın nereden geldiğini gör.
  6. Kısmi başarı: var olmayan bir alan iste ve yanıtı incele. HTTP kodu ne? data ve errors birlikte döndü mü?
  7. N+1'i çıkar: "20 sipariş ve her birinin müşterisi" sorgusu için kaç veritabanı sorgusu çalışacağını kâğıtta hesapla. Toplu yükleyici olsaydı kaç olurdu?
  8. Maliyet saldırısı: şemandaki ilişkileri kullanarak beş kat iç içe geçen bir sorgu yaz. Bunu hangi üç mekanizmayla engellersin?

08Kendini sına

GraphQL hangi problemi çözer? REST'in bu probleme cevapları neydi?

İki problemi: aşırı veri çekme — sunucunun belirlediği yanıt şekli yüzünden istemcinin kullanmadığı alanların da gelmesi (mobil ağda doğrudan veri ve pil maliyeti); ve eksik veri çekme — tek bir ekran için birden çok gidiş-dönüş gerekmesi, ki yüksek gecikmeli bağlantılarda süreler toplanır. REST'in üç cevabı vardır ve her birinin bedeli vardır: alan seçimi (sparse fieldsets) yanıt şemasını değişkenleştirir, önbellek anahtarını çeşitlendirir ve belgelemeyi zorlaştırır; ekrana özel bileşik uçlar sözleşmeyi şişirir; BFF her istemci türü için ayrı bir servis bakımı getirir. GraphQL dördüncü cevabı verir: tek uç nokta ve istemcinin tarif ettiği yanıt — yanıt sorgunun şeklinin aynısıdır ve tek gidiş-dönüşte gelir.

GraphQL şemasının tip sistemi nasıl çalışır? Null kuralı neden ters?

Şema, sunucunun ne sunduğunu tanımlayan katı tipli sözleşmedir ve SDL ile yazılır; aynı zamanda belgedir ve iç gözlemle (introspection) sorgulanabilir, araç desteği buradan gelir. Üç kök tip vardır: Query, Mutation, Subscription. Girdi tipleri (input) çıktı tiplerinden ayrıdır — "istek DTO'su ile yanıt DTO'su ayrıdır" kuralının dile gömülmüş hâli. Skaler tipler sınırlıdır (Int, Float, String, Boolean, ID); tarih ve Decimal özel skaler olarak tanımlanır. Çok biçimlilik interface ve union ile taşınır. Null kuralı terstir: her alan varsayılan olarak null olabilir, zorunluluk ! ile eklenir. Gerekçesi dayanıklılıktır — bir alan çözülemezse (bağımlı servis çöktü) o alan null döner ve sorgunun geri kalanı çalışmaya devam eder. ! koyduğun alan çözülemezse hata yukarı yayılır ve üst nesneyi de null yapar; yani ! bedeli olan bir katılık taahhüdüdür.

Çözücü modeli nedir, N+1 nasıl doğar ve nasıl çözülür?

Şemadaki her alanın bir çözücüsü vardır; yürütme sorgu ağacını kökten yapraklara gezer ve her alan için ilgili çözücüyü çağırır. Bu model N+1'i kolayca doğurur: "20 sipariş ve her birinin müşterisi" sorgusunda liste için 1, her siparişin müşterisi için 20 olmak üzere 21 sorgu çalışır. Sorun GraphQL'e özgü değildir (ORM'lerde de vardır) ama GraphQL'de daha kolay ortaya çıkar, çünkü sorgunun şeklini istemci belirler — öngörülmemiş bir iç içe geçme üretimde birden bire yüzlerce sorgu üretebilir. Çözüm toplu yükleyici (DataLoader) kalıbıdır: aynı yürütme adımındaki tekil istekler biriktirilip tek bir toplu sorguya çevrilir (WHERE id IN (...)), böylece 21 sorgu 2'ye iner. Aynı esneklik maliyet denetimi sorununu da doğurur: REST'te bir ucun maliyeti aşağı yukarı sabitken GraphQL'de tek isteğin maliyeti sınırsız olabilir. Üretimde üç koruma zorunludur: derinlik sınırı, karmaşıklık puanı ve kalıcı sorgular; ayrıca iç gözlem üretimde kapatılır ya da yetkiye bağlanır.

GraphQL ile REST'i karşılaştır. Ne zaman hangisini seçersin?

GraphQL'de yanıt şeklini istemci belirler, tek uç nokta vardır ve ekran başına genelde tek istek yeter; sürümleme farklıdır çünkü alan eklemek hiçbir istemciyi kırmaz (kimse istemediği alanı almaz) ve alan bazında kullanım ölçümü sayesinde @deprecated ile kaldırma kararı verilebilir, bu yüzden GraphQL API'leri genelde sürümsüz yaşar. Kaybedilenler: HTTP önbelleği doğal olarak çalışmaz (istemci tarafı önbellek gerekir), yanıt çoğunlukla 200 döner ve kısmi başarıyı data+errors ile bildirir — bu, ara katmanların, ağ geçitlerinin ve izleme araçlarının hata oranını görememesi demektir, bu yüzden errors doluluğu ayrıca ölçülmelidir; standart bir hata sözleşmesi (ProblemDetails) yoktur; istek maliyeti sınırlanmazsa öngörülemez; dosya yükleme ek sözleşme ister ve ağ geçidi/WAF desteği sınırlıdır. GraphQL'i çok sayıda farklı istemci aynı veriyi farklı şekillerde istediğinde, veri modeli grafik gibi bağlı olduğunda ve ön yüz hızlı iterasyon yaptığında seçerim; REST'i tek istemcili, basit CRUD, HTTP önbelleği ve ara katman uyumu kritik, dosya ağırlıklı işlerde ve ekipte GraphQL deneyimi yokken seçerim. Çoğu zaman doğru cevap ikisi birden: iç iletişimde REST/gRPC, istemciye bakan yüzeyde GraphQL.

09Sınav köşesi

Yazılıda nasıl sorulur

(1) GraphQL nedir, hangi problemi çözer. (2) Over-fetching ve under-fetching tanımları. (3) REST ile GraphQL karşılaştırması (tablo) — en sık çıkan soru. (4) Şema, tip, çözücü kavramlarını tanımlayınız. (5) Query, Mutation, Subscription farkı. (6) SDL ile verilen bir tip için örnek sorgu yazınız. (7) GraphQL'de null kuralı ve ! ne anlama gelir. (8) Fragment ve değişken ne işe yarar. (9) N+1 problemi nedir, GraphQL'de neden sık görülür, nasıl çözülür. (10) GraphQL'de hata ve durum kodu nasıl ele alınır. (11) GraphQL'de sürümleme nasıl yapılır. (12) Sorgu derinliği ve karmaşıklık sınırı neden gerekir.

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Problem — "REST'te yanıtın şeklini sunucu belirler; mobil istemci ya kullanmadığı alanları çeker ya da tek ekran için altı istek atar." (2) Çözüm — "GraphQL'de tek uç nokta vardır ve istemci istediği alan ağacını tarif eder; yanıt sorgunun şeklinin aynısıdır." (3) Model — "merkezde katı tipli şema vardır; her alanın bir çözücüsü vardır ve yürütme sorgu ağacını gezer. Şema aynı zamanda belgedir, iç gözlemle sorgulanır." (4) Bedel — "HTTP önbelleği çalışmaz, yanıt çoğunlukla 200 döner ve hata errors alanında taşınır; bu yüzden hata oranını ayrıca ölçmek gerekir." (5) Kıdem vuruşu — "en büyük iki risk N+1 ve maliyet denetimidir: sorgunun şeklini istemci belirlediği için öngörmediğim bir iç içe geçme yüzlerce sorgu üretebilir — DataLoader ile toplu yükleme yaparım. Ve derinlik sınırı, karmaşıklık puanı ve kalıcı sorgular olmadan üretime çıkarmam; iç gözlemi de üretimde kapatırım. Sürümleme tarafında ise avantajlıdır: alan eklemek kırıcı değildir, alan bazlı kullanım ölçümüyle @deprecated alanları güvenle kaldırırım. REST'in yerine geçmez; iç servislerde REST, istemciye bakan yüzeyde GraphQL kullanırım."