İlk GraphQL API’nizi Oluşturun

Fatih Küçükkarakurt

Fatih Küçükkarakurt

18 min read

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.

İçerik

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şı

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:

npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node

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

{
    "presets":[
        [
            "@babel/env",
            { "targets:"{"node":"current"}}
        ]
    ]
}

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

{
    ..
    "scripts":{
        "serve":"babel-node index.js"
        },
    ..
}

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

npm install --save express apollo-server-express graphql graphql-tools graphql-tag

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:

import gql from "graphql-tag";
import express from "express";
import {ApolloServer, makeExecutableSchema} from "apollo-server-express";

const port = process.env.PORT || 8080;

// GraphQL SDL kullanarak API leri tanımlayın
const typeDefs = gql`
    type Query{
        sayHello(name: String!): String!
        }

    type Mutation{
        sayHello(name: String!): String!
        }
`;

// SDL'de API tanımları için çözümleyici eşleşmesi tanımlama
const resolvers = {
    Query:{
        sayHello: (obj, args, context, info) => {
            return `Hello ${ args.name }!`;
        }
    },

    Mutation: {
        sayHello: (obj, args, context, info) => {
            return `Hello ${ args.name}!`;
            }
        },
    };

// Ekspres Yapılandırma
const app = express();

// SDL tanımlarına ve çözümleyici haritalarına dayalı GraphQL şeması oluşturun
const schema = makeExecutableSchema ({ typeDefs, resolvers });

// Apollo Sunucu Oluşturusun
const apolloServer = new ApolloServer ({schema});
apolloServer.applyMiddleware ({app});

// Server'ı Başlatın
app.listen ({port}), () => {
    console.log(`🚀Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`);
});

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:

import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date';

// GraphQL SDL kullanarak API leri tanımlayın
const typeDefs = gql`
  scalar Date
  scalar Time
  scalar DateTime

  type Query {
    sayHello(name: String!): String!
  }

  type Mutation {
    sayHello(name: String!): String!
  }
`;

// SDL de API tanımları için çözümleyici eşlemesi tanımlama
const resolvers = {
  Date: GraphQLDate,
  Time: GraphQLTime,
  DateTime: GraphQLDateTime,

  Query: {
    sayHello: (obj, args, context, info) => {
      return `Hello ${args.name}!`;
    }
  },

  Mutation: {
    sayHello: (obj, args, context, info) => {
      return `Hello ${args.name}!`;
    }
  }
};

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

Ş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:

type Query {
    ...
 }

type Mutation {
    ...
 }

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

extend type Query {
    ...
    }

extend type Mutation {
    ...
    }

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:

// Ekspress Yapılandırma
const app = express();

// SDL tanımlarına ve çözümleyici haritalarına dayalı GraphQL şeması oluşturun
const schema = makeExecutableSchema({typeDefs, resolvers});

// Apollo sunucusu oluşturun
const apolloServer = new ApolloServer ({
    schema,

    context: ({req, res}) => {
        const context = {};

        // JWT Token Doğrulaması
        const parts = req.headers.authorization ? req.headers.authorization.split(' ') : [''];
        const token = parts.length === 2 && parts[0].toLowerCase() === 'bearer' ? parts[1] : undefined;
        context.authUser = token ? verify(token) : undefined;

        return context;
    }
});
apolloServer.applyMiddleware ({app});

// Server'ı Başlat
app.listen ({port}), () => {
    console.log(`🚀Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`);
});

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:

import { allow } from 'graphql-shield';

const isAuthorized = rule()(
  (obj, args, { authUser }, info) => authUser && true
);

export const permissions = {
  Query: {
    '*': isAuthorized,
    sayHello: allow
  },

  Mutation: {
    '*': isAuthorized,
    sayHello: allow
  }
};

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

// Ekspress yapılandırma
const app = express();

// SDL tanımlarına ve çözümleyici haritalarına dayalı GraphQL şeması oluşturun
const schema = makeExecutableSchema({ typeDefs, resolvers });
const schemaWithMiddleware = applyMiddleware(schema, shield(permissions, { allowExternalErros: true }));

// Apolo sunucusunu oluşturun
const apolloServer = new ApolloServer({ schemaWithMiddleware });
apolloServer.applyMiddleware({app});

// Server'ı Başlat
app.listen ({port}), () => {
    console.log(`🚀Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`);
});

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_MANAGER rolüne sahip kullanıcılar, kullanıcı yönetimi işlevini çağırabilmelidir. Bunu şu şekilde uygularız:

export const isUserManager = rule()(
  (obj, args, { authUser }, info) =>
    authUser && authUser.role === 'USER_MANAGER'
);

export const permissions = {
  Query: {
    userById: isUserManager,
    users: isUserManager
  },

  Mutation: {
    editUser: isUserManager,
    deleteUser: isUserManager
  }
};

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:

import {UserInputError} from "apollo-server-express";
import passwordValidator from "password-validator";
import {isEmail} from "validator";

const passwordSchema = new passwordValidator()
    .is().min(8)
    .is().max(20)
    .has().letters()
    .has().digits()
    .has().symbols()
    .has().not().spaces();

export const validators = {
    Mutation: {
        signup: (resolver, parent, args, context) => {
            const {email, password, rePassword} = args.signupReq;

            if (!isEmail(email)){
                throw new UserInputError('Invalid Email Adress!');
            }

            if (password !== rePassword) {
                throw new UserInputError('Passwords don\'t match!');
            }

            if (!passwordSchema.validate(password)) {
                throw nre UserInputError('Password is not strong enough!');
            }

        return resolve(parent, args, context);
        }
    }
}

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

// Ekspress yapılandırma
const app = express();

// SDL tanımlarına ve çözümleyici haritalarına dayalı GraphQL şeması oluşturun
const schema = makeExecutableSchema({ typeDefs, resolvers });
const schemaWithMiddleware = applyMiddleware(
  schema,
  validators,
  shield(permissions, { allowExternalErrors: true })
);

// Apollo sunucusunu oluşturun
const apolloServer = new ApolloServer({ schemaWithMiddleware });
apolloServer.applyMiddleware({ app });

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:

xtend type Query {
 books: [Book!]!
 ...
}

extend type Mutation {
 ...
}

type Book {
 id: ID!
 creator: User!
 createdAt: DateTime!
 updatedAt: DateTime!
 authors: [Author!]!
 title: String!
 about: String
 language: String
 genre: String
 isbn13: String
 isbn10: String
 publisher: String
 publishDate: Date
 hardcover: Int
}

type User {
 id: ID!
 createdAt: DateTime!
 updatedAt: DateTime!
 fullName: String!
 email: String!
}

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:

export const resolvers = {
     Query: {
         books: (obj, args, context, info) => {
         return bookService.findAll();
     },
         ...
 },

    Mutation: {
         ...
     },

     Book: {
         creator: ({ creatorId }, args, context, info) => {
         return userService.findById(creatorId);
     },
         ...
     }
}

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

select `books`.* from `books`
select `users`.* from `users` where `users`.`id` = ?
select `users`.* from `users` where `users`.`id` = ?
select `users`.* from `users` where `users`.`id` = ?
select `users`.* from `users` where `users`.`id` = ?
select `users`.* from `users` where `users`.`id` = ?
...

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:

export class UserDataLoader extends DataLoader {
 constructor() {
     const batchLoader = userIds => {
         return userService
             .findByIds(userIds)
             .then(
                 users => userIds.map(
                     userId => users.filter(user => user.id === userId)[0]
                 )
             );
     };

 super(batchLoader);
 }static getInstance(context) {
     if (!context.userDataLoader) {
         context.userDataLoader = new UserDataLoader();
     }

 return context.userDataLoader;
     }
}

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

export const resolvers = {
     Query: {
        ...
     },

     Mutation: {
        ...
     },

     Book: {
         creator: ({ creatorId }, args, context, info) => {
             const userDataLoader = UserDataLoader.getInstance(context);

             return userDataLoader.load(creatorId);
         },
         ...
     }
}

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

select `books`.* from `books`
select `users`.* from `users` where `id` in (?)

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:

then((users) =>
  userIds.map((userId) => users.filter((user) => user.id === userId)[0])
);

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:

// Ekspress Yapilandirma
const app = express();

// SDL tanimlarina ve cozumleyici haritalarina dayali GraphQL semasi olusturun
const schema = makeExecutableSchema({ typeDefs, resolvers });

// Apollo Sunucusunu Olustur
const apolloServer = new ApolloServer({ schema, introspection: false });
apolloServer.applyMiddleware({ app });

// Serveri Baslat
app.listen({ port }, () => {
  console.log(
    `🚀Server ready at http://localhost:${port}${apolloServer.graphqlPath}`
  );
});

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:

query {
 authors {
     id, fullName
     books {
         id, title
         authors {
             id, fullName
                 books {
             id, title,
             authors {
                 id, fullName
                 books {
                     id, title
                     authors {
                     ...
                             }
                         }
                     }
                 }
             }
         }
     }
}

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:

// Ekspress Yapilandirma
const app = express();

// SDL tanimlarina ve cozumleyici haritalarina dayali GraphQL semasi olusturun
const schema = makeExecutableSchema({ typeDefs, resolvers });

// Apollo Sunucusunu Olustur
const apolloServer = new ApolloServer({
  schema,
  introspection: false,
  validationRules: [depthLimit(5)]
});
apolloServer.applyMiddleware({ app });

// Serveri Baslat
app.listen({ port }, () => {
  console.log(
    `🚀Server ready at http://localhost:${port}${apolloServer.graphqlPath}`
  );
});

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 Nedir? GraphQL, API’ler için alana özgü bir veri sorgulama ve işleme dilidir. Mevcut verilerle sorguları yürütmek için kullanıyoruz.

  • Neden GraphQL? 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.

  • GraphQL ve REST 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.

  • GraphQL, REST kullanıyor mu? 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.

  • GraphQL, REST'ten daha mı yavaş? 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.

  • GraphQL neden kullanılır? Çü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.

Mutlu Kodlamalar.

Anatoliacode Makale Aboneliği

Bize abone olarak tüm makaleleri ilk siz okuyabilirsiniz. Ayrıca asla reklam veya spam yapmıyoruz.