GraphQL ile İlk Sorgu: Temel Kullanım Rehberi

REST API’lere alışmış bir sysadmin olarak ilk kez GraphQL ile karşılaştığımda açıkçası biraz kaşlarımı çattım. “Bir API daha mı? Ne gerek var?” diye düşündüm. Ama birkaç projede kullandıktan sonra şunu söyleyebilirim: GraphQL, özellikle karmaşık veri ilişkilerini yönetirken ve fazladan endpoint yazmaktan kurtulmak istediğinde gerçekten hayat kurtarıyor. Bu yazıda sıfırdan başlayarak GraphQL’in temel kavramlarını, ilk sorgunuzu nasıl yazacağınızı ve gerçek dünya senaryolarında nasıl kullanacağınızı ele alacağız.

GraphQL Nedir ve Neden İhtiyacımız Var?

GraphQL, Facebook tarafından 2012’de geliştirilen ve 2015’te açık kaynak olarak yayınlanan bir sorgu dilidir. REST’ten temel farkı şu: REST’te her kaynak için ayrı bir endpoint tanımlarsınız (/users, /posts, /comments gibi), GraphQL’de ise tek bir endpoint üzerinden istediğiniz veriyi istediğiniz şekilde çekebilirsiniz.

Düşünün, bir dashboard sayfası yapıyorsunuz. REST ile bu işi yapabilmek için /users/1, /users/1/posts, /users/1/followers gibi üç ayrı istek atmak zorunda kalabilirsiniz. GraphQL’de bunu tek bir sorguda halledebilirsiniz. Hem bant genişliği kazanırsınız hem de kod karmaşıklığı azalır.

Temel avantajlar:

  • Over-fetching yok: Sadece ihtiyacınız olan alanları çekersiniz, sunucu bütün kaydı döndürmez
  • Under-fetching yok: İlişkili verileri tek sorguda alabilirsiniz
  • Güçlü tip sistemi: Schema sayesinde API’niz otomatik olarak dokümante olur
  • Gerçek zamanlı veri: Subscription mekanizması ile WebSocket desteği gelir
  • Geliştirici deneyimi: GraphiQL ve Apollo Studio gibi araçlarla sorgularınızı test edebilirsiniz

Kurulum ve Ortam Hazırlığı

Pratik yapalım. Node.js üzerinde basit bir GraphQL sunucusu kuralım. Önce gerekli paketleri yükleyelim:

mkdir graphql-demo && cd graphql-demo
npm init -y
npm install graphql express express-graphql
npm install --save-dev nodemon

Şimdi basit bir sunucu dosyası oluşturalım:

cat > server.js << 'EOF'
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');

// Schema tanımlama
const schema = buildSchema(`
  type User {
    id: ID!
    name: String!
    email: String!
    age: Int
    posts: [Post]
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User
    createdAt: String
  }

  type Query {
    user(id: ID!): User
    users: [User]
    post(id: ID!): Post
    posts: [Post]
  }
`);

// Örnek veri
const usersData = [
  { id: '1', name: 'Ahmet Yilmaz', email: '[email protected]', age: 30 },
  { id: '2', name: 'Zeynep Kaya', email: '[email protected]', age: 25 },
];

const postsData = [
  { id: '1', title: 'Linux Sunucu Yönetimi', content: 'Detaylı içerik...', authorId: '1', createdAt: '2024-01-15' },
  { id: '2', title: 'Docker ile Container', content: 'Docker rehberi...', authorId: '2', createdAt: '2024-01-20' },
];

// Resolver'lar
const root = {
  user: ({ id }) => {
    const user = usersData.find(u => u.id === id);
    if (user) {
      user.posts = postsData.filter(p => p.authorId === id);
    }
    return user;
  },
  users: () => usersData,
  post: ({ id }) => postsData.find(p => p.id === id),
  posts: () => postsData,
};

const app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true,  // Geliştirme ortamı için GUI
}));

app.listen(4000, () => {
  console.log('GraphQL sunucusu http://localhost:4000/graphql adresinde çalışıyor');
});
EOF

Sunucuyu başlatalım:

node server.js
# veya geliştirme modunda
npx nodemon server.js

Tarayıcınızda http://localhost:4000/graphql adresine gittiğinizde GraphiQL arayüzünü göreceksiniz. Bu araç, sorgularınızı test etmek için son derece kullanışlı.

İlk GraphQL Sorgunuzu Yazmak

GraphQL’de üç temel operasyon tipi vardır:

  • Query: Veri okuma (GET’in karşılığı)
  • Mutation: Veri yazma/güncelleme/silme (POST/PUT/DELETE’nin karşılığı)
  • Subscription: Gerçek zamanlı veri dinleme

İlk sorgumuzu GraphiQL üzerinden ya da curl ile atabiliriz. Basit bir kullanıcı sorgusundan başlayalım:

# curl ile GraphQL sorgusu atmak
curl -X POST 
  -H "Content-Type: application/json" 
  -d '{"query": "{ users { id name email } }"}' 
  http://localhost:4000/graphql

Bu sorgu bize tüm kullanıcıların id, name ve email alanlarını döndürür. Dikkat edin, age alanını istemedik, sunucu onu göndermeyecek. REST’te genelde sunucu bütün nesneyi döndürür, burada siz kontrol ediyorsunuz.

Şimdi daha karmaşık bir sorgu yazalım, kullanıcı ve ona ait postları tek seferde çekelim:

curl -X POST 
  -H "Content-Type: application/json" 
  -d '{
    "query": "query GetUserWithPosts($userId: ID!) {
      user(id: $userId) {
        id
        name
        email
        posts {
          id
          title
          createdAt
        }
      }
    }",
    "variables": {"userId": "1"}
  }' 
  http://localhost:4000/graphql

Burada birkaç önemli şey görüyorsunuz: sorguyu adlandırdık (GetUserWithPosts), değişken kullandık ($userId) ve değişkeni ayrı bir variables objesiyle gönderdik. Bu yaklaşım hem güvenli (SQL injection tarzı saldırılara karşı) hem de okunabilir.

Schema ve Tip Sistemi

GraphQL’in gücü büyük ölçüde tip sisteminden geliyor. Schema Language (SDL) ile API’nizin yapısını tanımlıyorsunuz ve bu hem dokümantasyon hem de validasyon görevi görüyor.

Daha kapsamlı bir schema örneği inceleyelim:

cat > schema.graphql << 'EOF'
# Temel skalar tipler: Int, Float, String, Boolean, ID
# ! işareti null olamaz anlamına gelir

enum UserRole {
  ADMIN
  EDITOR
  VIEWER
}

type User {
  id: ID!
  name: String!
  email: String!
  role: UserRole!
  age: Int
  createdAt: String!
  posts: [Post!]!
  followers: [User!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
  tags: [String!]!
  author: User!
  comments: [Comment!]!
  likeCount: Int!
}

type Comment {
  id: ID!
  text: String!
  author: User!
  post: Post!
}

# Giriş tipleri (mutation için)
input CreateUserInput {
  name: String!
  email: String!
  role: UserRole!
  age: Int
}

input UpdatePostInput {
  title: String
  content: String
  published: Boolean
  tags: [String!]
}

# Sorgu tipi
type Query {
  me: User
  user(id: ID!): User
  users(role: UserRole, limit: Int, offset: Int): [User!]!
  post(id: ID!): Post
  posts(published: Boolean, tag: String): [Post!]!
  searchPosts(keyword: String!): [Post!]!
}

# Mutation tipi
type Mutation {
  createUser(input: CreateUserInput!): User!
  updatePost(id: ID!, input: UpdatePostInput!): Post!
  deletePost(id: ID!): Boolean!
  publishPost(id: ID!): Post!
}

# Subscription tipi
type Subscription {
  postAdded: Post!
  commentAdded(postId: ID!): Comment!
}
EOF

echo "Schema dosyası oluşturuldu"

Mutation: Veri Yazma İşlemleri

Sorgu yazmayı öğrendik, şimdi veri oluşturma ve güncellemeye geçelim. Mutation sözdizimi Query’e çok benziyor:

# Yeni kullanıcı oluşturma
curl -X POST 
  -H "Content-Type: application/json" 
  -d '{
    "query": "mutation CreateNewUser($input: CreateUserInput!) {
      createUser(input: $input) {
        id
        name
        email
        role
        createdAt
      }
    }",
    "variables": {
      "input": {
        "name": "Mehmet Demir",
        "email": "[email protected]",
        "role": "EDITOR",
        "age": 28
      }
    }
  }' 
  http://localhost:4000/graphql

Mutation’ın güzel yanı, işlem sonucunda da istediğiniz alanları döndürebiliyorsunuz. Kullanıcı oluşturdunuz ve hemen sunucudan atanan id ve createdAt değerlerini alabiliyorsunuz, ekstra bir GET isteği atmadan.

Post güncelleme mutation’ı:

curl -X POST 
  -H "Content-Type: application/json" 
  -d '{
    "query": "mutation PublishPost($postId: ID!) {
      publishPost(id: $postId) {
        id
        title
        published
      }
    }",
    "variables": {"postId": "1"}
  }' 
  http://localhost:4000/graphql

Fragment Kullanımı: Kod Tekrarını Önleme

Birden fazla sorguda aynı alanları tekrar tekrar yazmak sıkıcı ve hata yaratıcı. Fragment’lar burada devreye giriyor:

# Fragment ile sorgular
curl -X POST 
  -H "Content-Type: application/json" 
  -d '{
    "query": "
      fragment UserBasicInfo on User {
        id
        name
        email
        role
      }

      fragment PostSummary on Post {
        id
        title
        published
        likeCount
        author {
          ...UserBasicInfo
        }
      }

      query Dashboard {
        me {
          ...UserBasicInfo
          posts {
            ...PostSummary
          }
        }
        posts(published: true) {
          ...PostSummary
        }
      }
    "
  }' 
  http://localhost:4000/graphql

Fragment’lar özellikle frontend’de component bazlı geliştirme yaparken çok işe yarıyor. Her component kendi fragment’ını tanımlıyor, ana sorgu bunları birleştiriyor.

Gerçek Dünya Senaryosu: CI/CD Dashboard API’si

Diyelim ki bir CI/CD pipeline monitoring dashboard’u yapıyorsunuz. Jenkins, GitHub Actions veya GitLab CI verilerini tek bir GraphQL API üzerinden sunmak istiyorsunuz. Bu senaryo aslında GraphQL’in en güçlü olduğu yer.

cat > pipeline-schema.graphql << 'EOF'
enum PipelineStatus {
  RUNNING
  SUCCESS
  FAILED
  CANCELLED
  PENDING
}

enum TriggerType {
  PUSH
  PULL_REQUEST
  SCHEDULE
  MANUAL
}

type Repository {
  id: ID!
  name: String!
  url: String!
  defaultBranch: String!
  pipelines(limit: Int, status: PipelineStatus): [Pipeline!]!
  lastSuccessfulBuild: Pipeline
}

type Pipeline {
  id: ID!
  status: PipelineStatus!
  branch: String!
  commitSha: String!
  commitMessage: String!
  triggeredBy: TriggerType!
  startedAt: String!
  finishedAt: String
  duration: Int  # saniye cinsinden
  jobs: [Job!]!
  repository: Repository!
  logs: String
}

type Job {
  id: ID!
  name: String!
  status: PipelineStatus!
  startedAt: String
  duration: Int
  artifacts: [Artifact!]!
}

type Artifact {
  id: ID!
  name: String!
  size: Int!
  downloadUrl: String!
}

type Query {
  repository(id: ID!): Repository
  repositories: [Repository!]!
  pipeline(id: ID!): Pipeline
  runningPipelines: [Pipeline!]!
  failedPipelines(since: String): [Pipeline!]!
}

type Mutation {
  triggerPipeline(repositoryId: ID!, branch: String!): Pipeline!
  cancelPipeline(id: ID!): Pipeline!
  retryPipeline(id: ID!): Pipeline!
}

type Subscription {
  pipelineStatusChanged(pipelineId: ID!): Pipeline!
  newPipelineTriggered(repositoryId: ID!): Pipeline!
}
EOF

echo "Pipeline schema hazır"

Bu schema ile hem frontend uygulamanız hem de monitoring araçlarınız aynı API’yi kullanabilir. Örneğin Grafana ile GraphQL datasource eklediğinizde, başarısız pipeline’ları tek bir sorguyla çekebilirsiniz:

# Başarısız pipeline'ları sorgulama
curl -X POST 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer YOUR_TOKEN" 
  -d '{
    "query": "query FailedBuilds($since: String!) {
      failedPipelines(since: $since) {
        id
        status
        branch
        commitMessage
        startedAt
        duration
        repository {
          name
          url
        }
        jobs {
          name
          status
        }
      }
    }",
    "variables": {"since": "2024-01-01T00:00:00Z"}
  }' 
  https://your-api.example.com/graphql

Hata Yönetimi ve Authentication

GraphQL’de hata yönetimi REST’ten biraz farklı çalışıyor. HTTP status kodu her zaman 200 olabilir (yani curl başarılı görünebilir), ama yanıt içinde errors alanı olabilir.

# Hata içeren yanıt örneği
# {
#   "data": null,
#   "errors": [
#     {
#       "message": "Kullanıcı bulunamadı",
#       "locations": [{"line": 2, "column": 3}],
#       "path": ["user"],
#       "extensions": {
#         "code": "USER_NOT_FOUND",
#         "timestamp": "2024-01-15T10:30:00Z"
#       }
#     }
#   ]
# }

# Hata kontrolü yapan bash script
check_graphql_errors() {
  local response=$1
  local errors=$(echo "$response" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if 'errors' in data:
    for err in data['errors']:
        print(f'HATA: {err["message"]}')
    sys.exit(1)
else:
    print('Sorgu başarılı')
    sys.exit(0)
  ")
  echo "$errors"
  return $?
}

# Kullanım
RESPONSE=$(curl -s -X POST 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer $GRAPHQL_TOKEN" 
  -d '{"query": "{ user(id: "999") { name } }"}' 
  https://api.example.com/graphql)

check_graphql_errors "$RESPONSE"

Authentication konusunda ise çoğu GraphQL API, HTTP header’larında Bearer token bekler. Bu REST ile aynı mantıkta çalışıyor. Bazı implementasyonlar X-API-Key de kullanıyor.

Apollo Client ile Frontend Entegrasyonu

Eğer Node.js veya React tarafında çalışıyorsanız Apollo Client kullanmak işleri ciddi ölçüde kolaylaştırıyor. Özellikle caching mekanizması müthiş:

npm install @apollo/client graphql

Basit bir Apollo Client kurulumu:

cat > apollo-client.js << 'EOF'
const { ApolloClient, InMemoryCache, gql, createHttpLink } = require('@apollo/client');
const { setContext } = require('@apollo/client/link/context');

const httpLink = createHttpLink({
  uri: 'http://localhost:4000/graphql',
});

// Auth header ekleme
const authLink = setContext((_, { headers }) => {
  const token = process.env.API_TOKEN;
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    }
  };
});

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache(),
  defaultOptions: {
    query: {
      fetchPolicy: 'cache-first',  // Önce cache'e bak
    },
  },
});

// Sorgu çalıştırma
async function getUsers() {
  const { data } = await client.query({
    query: gql`
      query GetAllUsers {
        users {
          id
          name
          email
          role
        }
      }
    `,
  });
  return data.users;
}

getUsers()
  .then(users => console.log('Kullanıcılar:', users))
  .catch(err => console.error('Hata:', err));
EOF

Performans: N+1 Problemi ve DataLoader

GraphQL kullanırken dikkat etmeniz gereken önemli bir konu var: N+1 sorgu problemi. 10 kullanıcının postlarını çekmeye çalıştığınızda, her kullanıcı için ayrı bir veritabanı sorgusu çalışabilir. Bu 1 + 10 = 11 sorgu demek.

Çözüm DataLoader kullanmak:

npm install dataloader
cat > dataloader-example.js << 'EOF'
const DataLoader = require('dataloader');

// Batch fonksiyonu: birden fazla ID'yi toplu olarak çeker
const batchLoadUsers = async (userIds) => {
  console.log(`Toplu kullanıcı yüklemesi: ${userIds.length} kayıt`);
  // Normalde burada tek bir SQL IN sorgusu çalışır
  // SELECT * FROM users WHERE id IN (1, 2, 3, ...)
  const users = await db.query(
    'SELECT * FROM users WHERE id = ANY($1)',
    [userIds]
  );
  
  // DataLoader, sonuçları ID sırasına göre eşleştirmek ister
  return userIds.map(id => users.find(u => u.id === id) || null);
};

const userLoader = new DataLoader(batchLoadUsers);

// Resolver'da kullanım
const resolvers = {
  Post: {
    author: (post) => {
      // Her post için ayrı sorgu değil, toplu yükleme
      return userLoader.load(post.authorId);
    }
  }
};

module.exports = { userLoader };
EOF

DataLoader ile 10 kullanıcı için 10 ayrı sorgu yerine tek bir toplu sorgu çalışır. Üretim ortamında bu farkı kesinlikle hissediyorsunuz.

Introspection: API’yi Keşfetmek

GraphQL’in harika özelliklerinden biri introspection. Yani API’ye “sen ne sunuyorsun?” diye sorabiliyorsunuz:

# Mevcut tipleri listele
curl -X POST 
  -H "Content-Type: application/json" 
  -d '{
    "query": "query IntrospectionQuery {
      __schema {
        types {
          name
          kind
          description
        }
      }
    }"
  }' 
  http://localhost:4000/graphql | python3 -m json.tool

# Belirli bir tipin alanlarını incele
curl -X POST 
  -H "Content-Type: application/json" 
  -d '{
    "query": "query TypeDetails {
      __type(name: "User") {
        name
        fields {
          name
          type {
            name
            kind
          }
          description
        }
      }
    }"
  }' 
  http://localhost:4000/graphql | python3 -m json.tool

Bu özellik sayesinde API’nin dokümantasyonunu otomatik olarak çıkartabilirsiniz. graphql-codegen gibi araçlarla schema’dan TypeScript tipleri de üretebiliyorsunuz, yani sıfırdan tip yazmanıza gerek kalmıyor.

Production’da GraphQL: Dikkat Edilmesi Gerekenler

Üretim ortamında birkaç kritik konuya dikkat etmeniz gerekiyor:

  • Introspection’ı kapatın: Production’da introspection: false yapın, aksi halde saldırganlar API yapınızı keşfedebilir
  • Query depth limiti koyun: Sonsuz iç içe sorgular sunucunuzu çökertebilir, graphql-depth-limit paketi işinizi görür
  • Query complexity limiti: Çok karmaşık sorgulara karşı graphql-query-complexity kullanın
  • Rate limiting: Standart HTTP rate limiting kuralları geçerli, nginx veya API gateway seviyesinde yapabilirsiniz
  • Persisted queries: Apollo’nun persisted query özelliği ile sadece önceden kayıtlı sorgulara izin verebilirsiniz
  • Loglama: Her GraphQL operasyonunu, süresini ve hataları loglamanız şart
  • CORS: Tarayıcı tabanlı istemciler için CORS ayarlarını doğru yapılandırın

Sonuç

GraphQL ilk bakışta karmaşık görünebilir ama temel konseptleri yakaladıktan sonra REST’e göre çok daha esnek ve güçlü bir araç olduğunu anlıyorsunuz. Özellikle şu senaryolarda GraphQL’i tercih etmenizi öneririm: birden fazla frontend istemciniz varsa (mobil, web, masaüstü), veri ilişkileri karmaşıksa ve her endpoint için ayrı ayrı REST route’u yazmaktan bıktıysanız.

Sysadmin perspektifinden bakınca, GraphQL API’leri monitoring açısından biraz daha dikkat ister. Tüm istekler tek endpoint’e geldiğinden, hangi operasyonun ne kadar sürdüğünü takip etmek için operasyon bazlı logging şart. Bunu baştan kurun, sonradan eklemek zahmetli oluyor.

Bu yazıda anlattıklarım temel seviyeydi. Subscription ile gerçek zamanlı özellikler, schema stitching ve federation ile büyük ekiplerde mikro-servis mimarisi, directives kullanımı gibi ileri konular için serinin devamını takip edin. Sorularınız veya farklı senaryolarınız varsa yorum bölümünde buluşalım.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir