Biliyorsunuz ki Facebook, temelde veri sorgulama ve işleme için GraphQL adında, arka uç API ‘leri oluşturmanın yeni bir yolunu tanıtmıştı. İlk zamanlar, pek dikkatimi çekmemişti. Ancak sonunda, kendimi GraphQL tabanlı arka uç API ‘leri uygulamak zorunda olduğum bir hackathon projesiyle uğraşırken buldum. Daha sonra REST için öğrendiğim bilgileri, GraphQL ‘e nasıl uygulayacağımı öğrendim.

Bu uygulama döneminde, REST API ‘lerde kullanılan standart yaklaşımları, GraphQL dostu bir şekilde yeniden düşünmek zorunda kaldım. Bu makalede, GraphQL API ‘lerini uygularken göz önünde bulundurulması gereken yaygın sorunları özetlemeye çalışacağım. Elbette daha önceki makaleleri okuduysanız, konuları örnekler vererek anlatmayı sevdiğimi biliyorsunuz. Bu konu için de bu geleneği bozmayacağım. Vereceğim örnekler ve anlatacağım konu için bazı kitaplıklara ihtiyacımız olacak.

GraphQL API için Gerekli Kitaplıklar

GraphQL, Facebook tarafından geliştirildi ve 2015 yılında halka açık olarak yayınlandı. 2018 yılının sonlarında, GraphQL projesi Facebook’tan, kar amacı gütmeyen Linux tarafında barındırılan GraphQL Vakfı’na taşındı.

GraphQL hala genç bir teknoloji olduğundan ve ilk referans uygulaması JavaScript için mevcut olduğundan, çoğu bildiğimiz kitaplık Node.js üzerinde bulunmaktadır. Ancak iki önemli şirketten daha bahsetmem gerekiyor. Bunlar: Apollo ve Prisma. Bu topluluklar, GraphQL için açık kaynak araçları ve kitaplıklar sağlıyorlar. Bu makaledeki örnek proje, bu iki şirket tarafından sağlanan kitaplıklara dayanacaktır:

  • Graphql-js – JavaScript için GraphQL’in referans uygulaması
  • Apollo sunucu – Express, Connect, Hapi, Koa ve daha fazlası için GraphQL sunucusu
  • Apollo-graphql-tools – SDL’yi kullanarak bir GraphQL şeması oluşturabilir, modelleyebilir ve birleştirebilirsiniz
  • Prisma-graphql-middleware – GraphQL çözümleyicilerinizi ara yazılım işlevlerine ayırın

GraphQL dünyasında, API’lerinizi GraphQL şemalarını kullanarak tanımlarsınız ve bunlar için, GraphQL Şema Tanım Dili (SDL – Schema Defini on Language) adı verilen kendi dilini kullanır. SDL, kullanımının çok basit olmasının yanında son derece güçlüdür.

GraphQL şemaları oluşturmanın iki yolu vardır:

  • kod yaklaşımı
  • şema yaklaşımı

Kod yaklaşımında, GraphQL şemalarınızı, graphql js kitaplığına dayalı JavaScript nesneleri olarak tanımlarsınız ve SDL, kaynak koddan otomatik olarak oluşturulur.

Şema yaklaşımda ise, GraphQL şemalarınızı SDL’de tanımlar ve Apollo graphql tools kitaplığını kullanarak iş mantığınızı birleştirirsiniz.

Eğer bana soracak olursanız, ben şema öncelikli yaklaşımı tercih etmekten yanayım. Bu makaledeki örnek projemiz için de bunu kullanacağım. Klasik bir kitapevi örneğimiz olacak. Bu örnek uygulama içerisinde, yazarlar, kitaplar, kullanıcı yönetimi ve kimlik doğrulama gibi özellikler yer alacak. Tüm bu uygulamada API’ler oluşturmak için CRUD API’leri sağlayacak bir arka uç oluşturacağız.

Temel GraphQL Sunucusu Oluşturma

Temel bir GraphQL sunucusunu çalıştırmak için yeni bir proje oluşturmalıyız. Bunu elbette npm ile başlatacağız ve Babel‘i yapılandıracağız. Babel’i yapılandırmak için önce aşağıdaki komutla gerekli kitaplıkları kurabilirsiniz:

Babel’i kurduktan sonra, projenizin kök dizininde .babelrc isimli bir dosya oluşturun ve aşağıdaki yapılandırmayı buraya kopyalayın:

Ayrıca package.json dosyasını düzenleyin ve aşağıdaki komutu scripts bölümüne ekleyin:

Babel’i yapılandırdıktan sonra, gerekli GraphQL kitaplıklarını aşağıdaki komutla kurun:

Gerekli kitaplıkları kurduktan sonra, minimum kurulumla bir GraphQL sunucusu çalıştırmak için aşağıdaki kod parçacığını index.js dosyanıza kopyalayın:

Bundan sonra, npm run serve komutunu kullanarak sunucumuzu çalıştırabiliriz. Herhangi bir web tarayıcısında URL’ye http://localhost:8080/graphqladresini girerseniz, GraphQL’in Playground adlı etkileşimli görsel kabuğu açılacaktır. Burada GraphQL sorgularını ve değişikliklerini çalıştırabilir ve sonuç verilerini görebiliriz.

GraphQL dünyasında, API işlevleri; sorgular, mutasyonlar ve abonelikler adı verilen üç gruba ayrılmıştır:

  • Sorgular, istemcinin sunucudan ihtiyaç duyduğu verileri talep etmek için kullanılır.
  • Mutasyonlar, istemci tarafından sunucuda veri oluşturmak, güncellemek ve silmek için kullanılır.
  • Abonelikler, istemci tarafından sunucuyla gerçek zamanlı bağlantı oluşturmak ve sürdürmek için kullanılır. Bu, istemcinin sunucudan, gerçekleşen olayları almasını ve buna göre hareket etmesini sağlar.

Makalemizde sadece sorgular ve mutasyonları tartışacağız. Abonelikler çok büyük bir konudur. Bunun için ayrı bir makale yazmayı düşünüyorum. Ayrıca abonelikler, her API uygulamasında gerekli değildir.

GraphQL Gelişmiş Skaler Veri Türleri

GraphQL ile tanıştıktn çok kısa bir süre sonra, SDL’nin yalnızca ilkel veri türleri sağladığını ve her API’nin önemli bir parçası olan Date, Time ve DateTime gibi gelişmiş skaler veri türlerinin eksik olduğunu farkedeceksiniz. Neyse ki, bu sorunu çözmemize yardımcı olan bir kütüphanemiz var ve buna graphql-iso-date diyoruz. Kütüphaneyi kurduktan sonra, şemamızda yeni gelişmiş skaler veri türleri tanımlamamız ve bunları kütüphane tarafından sağlanan uygulamalara bağlamamız gerekecek:

Date ve Time’ın yanı sıra, kullanım durumunuza bağlı olarak sizin için yararlı olabilecek diğer ilginç skaler veri türü uygulamaları da vardır. Örneğin, bunlardan biri, GraphQL şemamızda dinamik yazmayı kullanma ve API’mizi kullanarak türlenmemiş JSON nesnelerini geçirme veya iade etme yeteneği sağlayan graphql-type-json‘dur. Ayrıca, bize gelişmiş temizleme, doğrulama ve dönüştürme ile özel GraphQL skalerlerini tanımlama olanağı veren graphql-scalar kitaplığı da mevcuttur.

Gerekirse, özel skaler veri türünüzü de tanımlayabilir ve yukarıda gösterildiği gibi şemanızda kullanabilirsiniz. Bu zor değil, ancak bunun tartışılması bu makalenin kapsamı dışındadır. Eğer yine de bu konuyla ilgileniyorsanız ve gerçekten araştırmak istiyorsanız, Apollo belgelerinde daha gelişmiş bilgiler bulabilirsiniz.

GraphQL Splitting Schema (Bölme Şeması)

Şemanıza daha fazla işlevsellik ekledikten sonra, büyümeye başlayacak ve tüm tanım kümesini tek bir dosyada tutmanın imkansız olduğunu anlayacağız. Bu yüzden kodu düzenlemek ve daha ölçeklenebilir hale getirmek için onu küçük parçalara ayırmamız gerekecek. Neyse ki Apollo tarafından sağlanan şema oluşturucu işlevi makeExecutableSchema, şema tanımlarını kabul eder ve bir dizi biçimindeki haritaları çözer. Bu bize şemamızı ve çözücüler haritamızı daha küçük parçalara ayırma yeteneği verir. Örnek projemizde yaptığım tam olarak buydu; API’yi aşağıdaki bölümlere ayırdım:

  • auth.api.graphql – Kullanıcı kimlik doğrulaması ve kaydı için API
  • author.api.graphql – Yazar girişleri için CRUD API
  • book.api.graphql – Kitap girişleri için CRUD API
  • root.api.graphql – Şema kökü ve ortak tanımlar (gelişmiş skaler türler gibi)
  • user.api.graphql – Kullanıcı yönetimi için CRUD API

Bölme şeması (Splitting Schema) sırasında dikkate almamız gereken bir şey var. Parçalardan biri kök şema olmalı ve diğeri kök şemayı genişletmelidir. Kulağa karmaşık geliyor ama gerçekten oldukça basit. Kök şemada sorgular ve mutasyonlar şu şekilde tanımlanır:

Ve diğerlerinde şu şekilde tanımlanırlar:

Hepsi bu kadar.

Kimlik doğrulama ve yetkilendirme

API uygulamalarının çoğunda, küresel erişimi kısıtlama ve bir tür kurala dayalı erişim ilkeleri sağlama gereksinimi vardır. Bunun için kodumuza şunları eklemeliyiz:

  • Kullanıcı kimliğini doğrulamak için Kimlik Doğrulama
  • Kural tabanlı erişim politikalarını uygulamak için Yetkilendirme

GraphQL dünyasında, REST dünyası gibi, genellikle kimlik doğrulama için JSON Web Token kullanıyoruz. Geçilen JWT belirtecini doğrulamak için, gelen tüm istekleri durdurmamız ve üzerlerindeki yetkilendirme başlığını kontrol etmemiz gerekir. Bunun için Apollo sunucusunun oluşturulması sırasında, tüm çözümleyiciler arasında paylaşılan bağlamı oluşturan mevcut istekle çağrılacak bir işlevi bir bağlam kancası olarak kaydedebiliriz. Bu şu şekilde yapılabilir:

Burada, eğer kullanıcı doğru bir JWT belirtecini geçerse, bunu doğrularız ve kullanıcı nesnesini bağlam içinde saklarız; bu istek, yürütme sırasında tüm çözümleyiciler için erişilebilir olacaktır.

Kullanıcı kimliğini doğruladık, ancak API’mize hala küresel olarak erişilebilir ve hiçbir şey kullanıcılarımızın yetkisiz olarak çağırmasını engellemiyor. Bunu önlemenin bir yolu, kullanıcı nesnesini doğrudan her çözümleyicide kontrol etmektir, ancak bu hataya açık bir yaklaşımdır çünkü çok sayıda ortak kod yazmamız gerekir ve yeni bir çözümleyici eklerken denetimi eklemeyi unutabiliriz. REST API çerçevelerine bakarsak, genellikle bu tür sorunlar HTTP istek önleyicileri kullanılarak çözülür, ancak GraphQL API konusunda, bir HTTP isteği birden fazla GraphQL sorgusu içerebileceğinden mantıklı olmaz. Yalnızca sorgunun ham dize gösterimine erişiriz ve onu manuel olarak ayrıştırmamız gerekir. Bu kesinlikle iyi bir yaklaşım değildir.

Bu yüzden, GraphQL sorgularını yakalamak için bir çözüme ihtiyacımız var ve bu çözüme prisma-graphql-middleware adını verebiliriz. Bu kitaplık, bir çözümleyici çağrılmadan önce veya sonra rastgele kod çalıştırmamızı sağlar. Kodun yeniden kullanımını sağlayarak kod yapımızı geliştirir.

GraphQL topluluğu, bazı özel kullanım durumlarını çözen Prisma ara yazılım kitaplığına dayalı, bir dizi harika ara yazılım oluşturdu. Kullanıcı yetkilendirmesi için, izin katmanı oluşturmamıza yardımcı olan graphql-shield adlı bir kitaplık var.

graphql-shield’ı kurduktan sonra, API’miz için aşağıdaki gibi bir izin katmanı ekleyebiliriz:

Ve bu katmanı şemamıza şu şekilde ara katman yazılımı olarak uygulayabiliriz:

Burada bir “shield” nesnesi oluştururken, allowExternalErrors true olarak ayarladık. Çünkü varsayılan olarak shield’ın davranışı çözümleyicilerde meydana gelen hataları yakalamak ve işlemekti. Bu benim örnek uygulamam için kabul edilebilir bir davranış değildir.

Yukarıdaki örnekte, API’mize erişimi yalnızca kimliği doğrulanmış kullanıcılar için kısıtladık. Ama shield çok esnektir ve bunu kullanarak kullanıcılarımız için çok zengin bir yetkilendirme şeması uygulayabiliriz. Örneğin, örnek uygulamamızda iki rolümüz var: USER ve USER_MANAGER. Yalnızca USER_MANAGERrolüne sahip kullanıcılar, kullanıcı yönetimi işlevini çağırabilmelidir. Bunu şu şekilde uygularız:

Vurgulamak istediğim bir şey var. Projemizde ara yazılım işlevlerinin nasıl düzenleneceğinin üzerinden durmamız gerektiğini düşünüyorum. Şema tanımları ve çözücü haritalarında olduğu gibi, bunları şemaya göre bölmek ve ayrı dosyalarda tutmak daha iyidir. Ancak şema tanımları dizilerini kabul eden, haritaları çözen ve bizim için onları birleştiren Apollo sunucusundan farklı olarak, Prisma ara yazılım kitaplığı bunu yapmaz. Yalnızca bir ara katman haritası nesnesi kabul eder. Bu nedenle onları bölersek, manuel olarak yeniden birleştirmemiz gerekir.

GraphQL Kullanıcı Girişi Doğrulama

GraphQL SDL, kullanıcı girişini doğrulamak için çok sınırlı bir işlevsellik sağlar; sadece hangi alanın gerekli ve hangisinin isteğe bağlı olduğunu tanımlayabiliriz. Daha fazla doğrulama gereksinimini, manuel olarak uygulamamız gerekir. Doğrulama kurallarını doğrudan çözümleyici işlevlerine uygulayabiliriz. Ancak bu işlev gerçekten buraya ait değildir. Örneğin, kullanıcı adının doğru bir e-posta adresi olup olmadığını, şifre girişlerinin eşleşip eşleşmediğini ve şifrenin yeterince güçlü olup olmadığını doğrulamamız gereken, “kullanıcı kayıt talebi giriş verilerini” kullanalım. Bu şu şekilde uygulanabilir:

Doğrulayıcı katmanını, şema için bir ara yazılım olarak, bunun gibi bir izin katmanıyla birlikte uygulayabiliriz:

GraphQL N+1 Sorgusu

GraphQL API’lerinde meydana gelen ve genellikle göz ardı edilen bir diğer sorun da N+1 sorgularıdır. Bunu göstermek için, örnek projemizin kitap API’sini kullanalım:

Burada, User türünün Book türü ile olan ilişkisini görüyoruz. Bu şema için çözümleyiciler haritası şu şekilde tanımlanır:

Bu API’yi kullanarak bir kitap sorgusu yürütürsek ve SQL günlüğüne bakarsak, şöyle bir şey göreceğiz:

Yürütme sırasında çözücü, ilk olarak kitapların listesini döndüren kitap sorgusu için çağrıldı ve ardından, her kitap nesnesi yaratıcı alan çözümleyicisi olarak adlandırıldı. Bu davranış N+1 veritabanı sorgularına neden oldu. Veritabanımızı bozmak istemiyorsak, bu tür davranışlar iyi değildir.

N+1 sorgu sorununu çözmek için Facebook geliştiricileri, DataLoader adlı çok ilginç bir çözüm geliştirdiler:

“DataLoader, toplu işlem ve önbelleğe alma yoluyla veritabanları veya web hizmetleri gibi çeşitli uzak veri kaynakları üzerinde basitleştirilmiş ve tutarlı bir API sağlamak için uygulamanızın veri getirme katmanının bir parçası olarak kullanılacak genel bir yardımcı programdır.”

DataLoader’ın nasıl çalıştığını anlamak çok kolay değil, bu yüzden önce yukarıda gösterilen sorunu çözen örneğe bakalım ve ardından arkasındaki mantığı açıklayalım.

Örnek projemizde DataLoader, içerik oluşturucu alanı için şu şekilde tanımlanmıştır:

UserDataLoader’ı tanımladıktan sonra, creator alanının çözümleyicisini şu şekilde değiştirebiliriz:

Uygulanan değişikliklerden sonra, kitaplar sorgusunu tekrar yürütürsek ve SQL ifadeleri günlüğünde, şöyle bir şey göreceğiz:

Burada, N+1 veritabanı sorgularının iki sorguya indirgendiğini görebiliriz. Birincisi kitapların listesini seçer, ikincisi kitap listesinde yaratıcı olarak sunulan kullanıcıların listesini seçer. Şimdi DataLoader’ın bu sonucu nasıl elde ettiğini açıklayalım.

DataLoader’ın birincil özelliği gruplamadır. Tek yürütme aşamasında, DataLoader tüm bireysel yükleme işlevi çağrılarının tüm farklı kimliklerini toplayacak ve ardından istenen tüm kimliklerle toplu iş  fonksiyonunu çağıracaktır. Hatırlanması gereken önemli bir şey, DataLoader örneklerinin yeniden kullanılamayacağıdır. Toplu iş fonksiyonu çağrıldığında, döndürülen değerler sonsuza kadar önbelleğe alınacaktır. Bu davranış nedeniyle, her yürütme aşaması için yeni DataLoader örneği oluşturmalıyız. Bunu başarmak için, DataLoader örneğinin bir bağlam nesnesinde sunulup sunulmadığını kontrol eden ve bulunamazsa bir tane yaratan statik bir getInstance işlevi oluşturduk. Her yürütme aşaması için yeni bir bağlam nesnesinin oluşturulduğunu ve tüm çözümleyiciler arasında paylaşıldığını unutmayın.

Bir toplu yükleme fonksiyonu yazarken, iki önemli şeyi hatırlamalıyız:

  1. Sonuç dizisi, istenen kimlik dizisiyle aynı uzunlukta olmalıdır. Örneğin, [1, 2, 3]kimliklerini talep ettiysek, döndürülen sonuç dizisi tam olarak üç nesne içermelidir:[{ "id": 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }, { “id”: 3, “fullName”: “user3” }]
  2. Sonuç dizisindeki her bir dizin, istenen kimlik dizisindeki aynı dizine karşılık gelmelidir. Örneğin, istenen kimlikler dizisi aşağıdaki sıraya sahipse:, [3, 1, 2]bu durumda döndürülen sonuç dizisi tam olarak aynı sıradaki nesneleri içermelidir:[{ "id": 3, “fullName”: “user3” }, { “id”: 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }]

Örneğimizde, sonuç sırasının istenen kimliklerin sırasına aşağıdaki kodla uymasını sağlıyoruz:

GraphQL API Güvenlik

Son olarak, güvenlikten bahsetmek istiyorum. GraphQL ile çok esnek API’ler oluşturabilir ve kullanıcıya verilerin nasıl sorgulanacağı konusunda zengin özellikler sağlayabiliriz. Bu, uygulamanın müşteri tarafına, oldukça fazla güç sağlar ve elbette büyük güçler büyük sorumluluklar getirir. Uygun güvenlik olmadan, kötü niyetli bir kullanıcı, istemediğiniz bir sorgu gönderebilir ve sunucumuzda DoS (Hizmet Reddi) saldırısına neden olabilir.

API’mizi korumak için yapabileceğimiz ilk şey, GraphQL şemasının iç gözlemini devre dışı bırakmaktır. Varsayılan olarak, bir GraphQL API sunucusu, GraphQL ve Apollo Playground gibi etkileşimli görsel kabuklar tarafından kullanılan tüm şemasının, iç gözlem yeteneğini ortaya çıkarır. Apollo Sunucusunu oluştururken introspection  adlıparametreyi false olarak ayarlayarak bunu devre dışı bırakabiliriz:

GraphQL API’mizi korumak için yapabileceğimiz bir sonraki şey, sorgunun derinliğini sınırlamaktır. Veri türlerimiz arasında döngüsel bir ilişkimiz varsa bu özellikle önemlidir. Örneğin, bizim örneğimizde,  Author ‘ın kitapları var ve  Booktürünün de yazarları var. Bu açıkça döngüsel bir ilişkidir ve hiçbir şey kötü niyetli kullanıcının böyle bir sorgu yazmasını engellemez:

Yeterince yerleştirme ile böyle bir sorgunun sunucumuzu kolayca patlatabileceği açıktır. Sorguların derinliğini sınırlamak için, graphql-deep-limit adlı bir kitaplık kullanabiliriz . Kurduktan sonra, Apollo Sunucusu oluştururken aşağıdaki gibi bir derinlik kısıtlaması uygulayabiliriz:

Böylece burada, maksimum sorgu derinliğini beş ile sınırlamış olduk.

Özetlemek Gerekirse

Bu makalede, GraphQL API’lerini uygulamaya başlarken karşılaşacağınız yaygın sorunları göstermeye çalıştım. GraphQL’in gerçekten ilginç bir teknoloji olduğunu söylemek istiyorum. Belki yarın hızla değişen Bilişim Teknolojileri dünyasında, API’leri geliştirmek için daha iyi bir yaklaşım ortaya çıkacak. Ancak GraphQL gerçekten de öğrenmeye değer ilginç teknolojiler kategorisine giriyor.

GraphQL, API’ler için alana özgü bir veri sorgulama ve işleme dilidir. Mevcut verilerle sorguları yürütmek için kullanıyoruz.

Açıkçası buna tek bir cevap vermem gerekirse, güçlü bir şekilde yazılmış API’leri tanımlamak için kullanılan ve aynı zamanda iyi bir dokümantasyon işlevi gören GraphQL SDL‘dir diyebilirim. Diğer bir avantaj ise, GraphQL’in veri modelini bir grafiğe dönüştürmesi ve istemcilere sunucudan tam olarak ihtiyaç duydukları verileri sorgulama yeteneği vermesidir.

Kullanım durumuna bağlı değişen bir soru bu. GraphQL, veri getirme ile alakalı olabilecek bazı REST sorunlarını çözdü. Ancak REST kadar basit ve anlaşılır değil.

Hayır, GraphQL REST kullanmaz; API’lerin nasıl oluşturulacağı konusunda alternatif bir yaklaşımdır. Ancak REST API’leriniz GraphQL kullanılarak paketlenebilir.

Genel olarak hiçbir GraphQL REST’ten daha yavaş değildir. Bazen tam olarak ihtiyacımız olan verileri talep edebildiğimiz ve bizi aşırı veya yetersiz veri getirmekten kurtarabildiği için REST’ten daha hızlı olabilir.

Çünkü ilginç bir teknoloji ve bazı kullanım durumlarını çok iyi çözüyor.

 

Umarım açıklayıcı ve anlaşılır bir makale olmuştur. Kendinize iyi bakın.

Sosyal Mesafe, Maske ve Temizlik Kurallarına Dikkat Edin.

Mutlu Kodlamalar 🙂

 

Diğer blog yazıları için buraya tıklayabilirsiniz.

Paylaşmak İster Misiniz?