GraphQL Değişkenler ve Input Type ile Dinamik Sorgular

GraphQL ile uygulama geliştirirken en çok karşılaşılan sorunlardan biri şu: Her farklı filtre kombinasyonu için ayrı bir sorgu mu yazmalıyız? Kullanıcı ID’ye göre arama yapmak istediğinde bir sorgu, isme göre arama yapmak istediğinde başka bir sorgu mu? Bu yaklaşım hem kod tekrarına yol açar hem de bakımı kabusa dönüşür. İşte tam bu noktada GraphQL’in değişkenler (variables) ve input type sistemi devreye girerek hayatımızı kurtarıyor.

GraphQL Değişkenler Neden Gerekli?

Klasik REST API geliştirirken query string parametrelerini rahatlıkla kullanırız. GraphQL’de de benzer bir mekanizma var ama çok daha güçlü ve tip güvenli bir şekilde çalışıyor.

Değişkenler olmadan bir GraphQL sorgusu şöyle görünür:

# Değişken kullanmadan - hardcoded sorgu
query {
  user(id: "42") {
    name
    email
    role
  }
}

Bu yaklaşımın problemi şu: Her farklı kullanıcı için sorguyu yeniden oluşturman gerekiyor. Client tarafında string interpolation yapıyorsun, bu da potansiyel injection açıklarına kapı aralıyor. Production sistemlerde bunu görmek insanın içini sıkıştırıyor.

Değişkenlerle aynı sorgu şu hale geliyor:

# Değişken kullanan - dinamik sorgu
query GetUser($userId: ID!) {
  user(id: $userId) {
    name
    email
    role
  }
}

# Değişkenler ayrı JSON objesi olarak gönderilir
# Variables: { "userId": "42" }

Fark hemen göze çarpıyor. Sorgu şablonu sabit kalıyor, sadece değişken değerleri değişiyor. Bu yaklaşım hem güvenli hem de önbellekleme açısından çok daha verimli.

Değişken Tipleri ve Sözdizimi

GraphQL’de değişken tanımlarken üç temel unsur var:

  • $degiskenAdi: Dolar işareti ile başlayan değişken adı
  • Tip: String, Int, Boolean, ID gibi scalar tipler ya da custom tipler
  • !: Zorunlu alan belirteci, olmadan nullable kabul edilir

Pratik örneklerle görelim:

# Farklı tip kombinasyonları
query SearchProducts(
  $searchTerm: String,        # Opsiyonel string
  $minPrice: Float!,          # Zorunlu float
  $inStock: Boolean,          # Opsiyonel boolean
  $categoryId: ID!,           # Zorunlu ID
  $limit: Int = 10            # Varsayılan değerli int
) {
  products(
    search: $searchTerm,
    priceMin: $minPrice,
    available: $inStock,
    category: $categoryId,
    first: $limit
  ) {
    id
    name
    price
    stockCount
  }
}

Burada dikkat edilmesi gereken birkaç nokta var. $limit: Int = 10 şeklinde varsayılan değer atayabiliyorsun. Bu özellikle sayfalama senaryolarında çok işe yarıyor. Zorunlu olmayan bir değişkeni null geçebilirsin ama zorunlu (!) olarak işaretlenmişse mutlaka bir değer göndermen gerekiyor.

Input Type ile Karmaşık Yapıları Yönetmek

Tek tek değişkenler basit sorgular için yeterli. Ama gerçek dünya senaryolarında onlarca parametre alan sorgularla karşılaşırsın. Bir e-ticaret platformunda ürün araması düşün: fiyat aralığı, kategori, marka, stok durumu, puanlama, kargo seçeneği… Bunları tek tek parametre olarak listelemek hem sorgu imzasını çirkinleştirir hem de yönetimi zorlaştırır.

Input type tam burada parlıyor. Önce schema tarafında input type’ı tanımlıyoruz:

# Schema tanımı - schema.graphql
input ProductFilterInput {
  searchTerm: String
  minPrice: Float
  maxPrice: Float
  categoryIds: [ID!]
  inStock: Boolean
  minRating: Float
  brandId: ID
  hasDiscount: Boolean
}

input PaginationInput {
  page: Int = 1
  pageSize: Int = 20
  sortBy: String = "createdAt"
  sortOrder: SortOrder = DESC
}

enum SortOrder {
  ASC
  DESC
}

type Query {
  searchProducts(
    filter: ProductFilterInput,
    pagination: PaginationInput
  ): ProductSearchResult!
}

Şimdi bu yapıyı kullanan sorgu:

# Input type kullanan dinamik arama sorgusu
query SearchProducts(
  $filter: ProductFilterInput,
  $pagination: PaginationInput
) {
  searchProducts(filter: $filter, pagination: $pagination) {
    totalCount
    pageInfo {
      currentPage
      totalPages
      hasNextPage
    }
    items {
      id
      name
      price
      discountedPrice
      rating
      brand {
        name
      }
    }
  }
}

# Gönderilecek değişkenler:
# {
#   "filter": {
#     "searchTerm": "laptop",
#     "minPrice": 5000,
#     "maxPrice": 20000,
#     "inStock": true,
#     "minRating": 4.0
#   },
#   "pagination": {
#     "page": 1,
#     "pageSize": 12,
#     "sortBy": "price",
#     "sortOrder": "ASC"
#   }
# }

Bu yapının güzelliği şu: Yarın yeni bir filtre eklemeniz gerekirse sadece ProductFilterInput‘a yeni alan ekleyip resolver’ı güncelliyorsunuz. Mevcut sorguları bozmuyorsunuz.

Mutation’larda Input Type Kullanımı

Input type’ın en yaygın kullanım alanlarından biri mutation’lar. Veri oluşturma ve güncelleme işlemlerinde genellikle çok sayıda alan gönderiyoruz. İşte bir kullanıcı yönetim sistemi örneği:

# Mutation için input type tanımları
input CreateUserInput {
  username: String!
  email: String!
  password: String!
  role: UserRole = USER
  department: String
  phoneNumber: String
  permissions: [String!]
}

input UpdateUserInput {
  username: String
  email: String
  role: UserRole
  department: String
  phoneNumber: String
  isActive: Boolean
}

enum UserRole {
  ADMIN
  MANAGER
  USER
  READONLY
}

type Mutation {
  createUser(input: CreateUserInput!): UserPayload!
  updateUser(id: ID!, input: UpdateUserInput!): UserPayload!
  deleteUser(id: ID!): DeletePayload!
}

Dikkat edersen CreateUserInput ve UpdateUserInput ayrı tanımlanmış. Bu önemli bir pattern. Create işleminde zorunlu alanlar update işleminde opsiyonel olabilir. Örneğin şifre oluştururken zorunlu ama güncellerken değiştirilmeyebilir.

Client tarafından bu mutation’ı kullanmak:

# Kullanıcı oluşturma mutation'ı
mutation CreateNewUser($input: CreateUserInput!) {
  createUser(input: $input) {
    success
    message
    user {
      id
      username
      email
      role
      createdAt
    }
    errors {
      field
      message
    }
  }
}

# Variables:
# {
#   "input": {
#     "username": "ahmet.yilmaz",
#     "email": "[email protected]",
#     "password": "GucluSifre123!",
#     "role": "MANAGER",
#     "department": "Yazilim",
#     "permissions": ["read:reports", "write:tickets"]
#   }
# }

İç İçe Input Type Kullanımı

Gerçek dünya senaryolarında input type’lar iç içe geçebiliyor. Bir sipariş sistemi düşünelim:

# İç içe input type'lar
input AddressInput {
  street: String!
  city: String!
  district: String!
  postalCode: String!
  country: String = "TR"
}

input OrderItemInput {
  productId: ID!
  quantity: Int!
  note: String
}

input CreateOrderInput {
  items: [OrderItemInput!]!
  shippingAddress: AddressInput!
  billingAddress: AddressInput
  couponCode: String
  paymentMethod: PaymentMethod!
  notes: String
}

# Mutation
mutation PlaceOrder($input: CreateOrderInput!) {
  createOrder(input: $input) {
    orderId
    estimatedDelivery
    totalAmount
    discountAmount
    status
  }
}

# Variables:
# {
#   "input": {
#     "items": [
#       { "productId": "prod_123", "quantity": 2 },
#       { "productId": "prod_456", "quantity": 1, "note": "Hediye paketi" }
#     ],
#     "shippingAddress": {
#       "street": "Atatürk Cad. No: 42",
#       "city": "Istanbul",
#       "district": "Kadikoy",
#       "postalCode": "34710"
#     },
#     "couponCode": "YENI20",
#     "paymentMethod": "CREDIT_CARD"
#   }
# }

Burada billingAddress opsiyonel. Eğer fatura adresi kargo adresinden farklı değilse göndermeye gerek yok.

Değişkenlerle Fragment Kullanımı

Büyük projelerde aynı alanları defalarca yazmaktan kurtulmak için fragment kullanıyoruz. Değişkenlerle beraber kullanıldığında çok güçlü bir kombinasyon oluşuyor:

# Fragment tanımı
fragment UserBasicInfo on User {
  id
  username
  email
  role
  isActive
  lastLoginAt
}

fragment UserDetailedInfo on User {
  ...UserBasicInfo
  department
  phoneNumber
  permissions
  createdAt
  updatedAt
  profile {
    avatarUrl
    bio
    timezone
  }
}

# Fragment kullanan sorgular
query GetUserList(
  $filter: UserFilterInput,
  $pagination: PaginationInput
) {
  users(filter: $filter, pagination: $pagination) {
    totalCount
    items {
      ...UserBasicInfo
    }
  }
}

query GetUserDetail($userId: ID!) {
  user(id: $userId) {
    ...UserDetailedInfo
    recentActivity(limit: 10) {
      action
      timestamp
      details
    }
  }
}

Bu yaklaşım özellikle büyük ekiplerde tutarlılığı sağlamak açısından önemli. Herkes aynı fragment’ı kullanınca bir alanda değişiklik yapıldığında tek yerden güncelleme yeterli oluyor.

Validation ve Hata Yönetimi

Input type kullanmanın en büyük faydalarından biri tip güvenliği sağlaması. Ama sadece GraphQL’in sağladığı temel validasyona güvenmek yetmiyor. Resolver seviyesinde de validasyon yapman gerekiyor. İşte Node.js tarafında pratik bir yaklaşım:

# Resolver'da input validasyonu - JavaScript
# resolver/userResolver.js

const createUser = async (_, { input }, context) => {
  # Yetki kontrolü
  if (!context.user || context.user.role !== 'ADMIN') {
    throw new AuthenticationError('Bu islemi yapmak icin yetkiniz yok');
  }

  # Email format kontrolü
  const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
  if (!emailRegex.test(input.email)) {
    return {
      success: false,
      errors: [{ field: 'email', message: 'Gecersiz email formati' }]
    };
  }

  # Sifre gucluluğu kontrolü
  if (input.password.length < 8) {
    return {
      success: false,
      errors: [{ field: 'password', message: 'Sifre en az 8 karakter olmali' }]
    };
  }

  # Kullanici adi benzersizlik kontrolü
  const existingUser = await UserModel.findOne({ 
    username: input.username 
  });
  
  if (existingUser) {
    return {
      success: false,
      errors: [{ field: 'username', message: 'Bu kullanici adi zaten kullaniliyor' }]
    };
  }

  # Kullanici oluştur
  const newUser = await UserModel.create({
    ...input,
    password: await hashPassword(input.password),
    createdBy: context.user.id
  });

  return {
    success: true,
    message: 'Kullanici basariyla olusturuldu',
    user: newUser
  };
};

Burada hata döndürme yaklaşımına dikkat edin. Exception fırlatmak yerine errors array’i ile yapılandırılmış hata dönüyoruz. Bu yaklaşım client tarafında çok daha kolay işlenebiliyor.

Gerçek Dünya Senaryosu: Dashboard Filtreleme Sistemi

Bir kurumsal dashboard geliştirdiğini düşün. Yöneticiler farklı zaman aralıkları, departmanlar ve metrik türlerine göre rapor alıyor. Bu senaryo için input type mimarisini nasıl kurarsın:

# Dashboard raporlama sistemi schema
input DateRangeInput {
  startDate: String!
  endDate: String!
}

input DashboardFilterInput {
  dateRange: DateRangeInput!
  departmentIds: [ID!]
  metricTypes: [MetricType!]
  groupBy: GroupByOption = DAILY
  includeComparison: Boolean = false
  comparisonDateRange: DateRangeInput
}

enum MetricType {
  SALES
  USER_ACTIVITY
  SYSTEM_PERFORMANCE
  SUPPORT_TICKETS
  REVENUE
}

enum GroupByOption {
  HOURLY
  DAILY
  WEEKLY
  MONTHLY
}

type Query {
  dashboardMetrics(filter: DashboardFilterInput!): DashboardResult!
  exportReport(filter: DashboardFilterInput!, format: ExportFormat!): ExportJob!
}

# Sorgu kullanımı
query GetDashboardData($filter: DashboardFilterInput!) {
  dashboardMetrics(filter: $filter) {
    summary {
      totalRevenue
      activeUsers
      openTickets
      systemUptime
    }
    timeSeries {
      timestamp
      value
      metric
    }
    departmentBreakdown {
      department {
        id
        name
      }
      metrics {
        type
        value
        changePercent
      }
    }
    comparison {
      previousPeriodData {
        timestamp
        value
        metric
      }
    }
  }
}

Bu tasarımın güzelliği şu: Aynı sorgu hem günlük hem haftalık hem de aylık rapor için kullanılabiliyor. includeComparison: true geçildiğinde önceki dönemle karşılaştırma da geliyor. Farklı ihtiyaçlar için yeni sorgu yazmak yerine değişkenleri değiştirmek yeterli.

Performans ve Önbellekleme

Değişken kullanmanın gizli bir faydası daha var: önbellekleme. GraphQL sorgu metni sabit kaldığında (sadece değişkenler değişiyor) hem client hem de server tarafında sorgu şablonunu önbellekleyebiliyorsunuz.

  • Persisted Queries: Sorgu metnini hash’e dönüştürüp sadece hash + değişkenler gönderebiliyorsunuz. Ağ trafiğini önemli ölçüde azaltıyor.
  • Automatic Persisted Queries (APQ): Apollo Client gibi kütüphaneler bunu otomatik yapıyor. İlk istekte hash gönderiliyor, server tanımıyorsa tam sorgu gönderiliyor ve cache’leniyor.
  • Server-side query planning: Sabit sorgu şablonu sayesinde server sorgu planını cache’leyebiliyor, her seferinde parse etmek zorunda kalmıyor.

Bir API Gateway veya load balancer kullanıyorsan değişkenli sorgular rate limiting ve logging açısından da çok daha temiz bir görünüm sunuyor. Her unique değer için ayrı sorgu yerine aynı operation name altında istekler gruplanabiliyor.

Yaygın Hatalar ve Çözümleri

Değişkenler ve input type kullanırken sıkça yapılan hatalar:

  • Tip uyumsuzluğu: Schema’da Int! tanımladın ama string gönderiyorsun. GraphQL bunu reddeder. Değişkenleri göndermeden önce client tarafında type casting yap.
  • Null vs Undefined farkı: null değeri açıkça “bu alan boş” demektir. Değişken listesinde hiç yer almayan alan ise “bu alan gönderilmedi” anlamına gelir. Resolver’larda bu farkı gözetmelisin.
  • Input type’da circular reference: Input type’lar birbirini referans alamaz, bu sonsuz döngüye yol açar. Schema validasyonu bunu yakalar ama tasarım aşamasında dikkat etmek gerekir.
  • Çok derin iç içe yapı: Input type’ları 3-4 seviyeden fazla iç içe geçirmek hem performansı hem de okunabilirliği olumsuz etkiler. Flatten edilmiş yapılar genellikle daha iyi çalışır.
  • Validation mantığını schema’ya yüklemek: GraphQL schema tipler arası ilişkileri doğrulayamaz. startDate < endDate gibi mantıksal validasyonları resolver seviyesinde yapman gerekiyor.

Sonuç

GraphQL değişkenler ve input type sistemi, dinamik ve yeniden kullanılabilir sorgular yazmanın temel taşları. Doğru kullanıldığında hem kod kalitesini artırıyor hem de uygulamanın performansını iyileştiriyor.

Özetlemek gerekirse:

  • Değişkenler sorguları dinamik hale getirir ve injection saldırılarına karşı koruma sağlar
  • Input type’lar karmaşık parametre yapılarını organize eder ve şema tutarlılığını garanti eder
  • Create ve Update için ayrı input type kullanmak zorunluluk/opsiyonellik yönetimini kolaylaştırır
  • Fragment’larla kombinasyon kod tekrarını minimuma indirir
  • Resolver seviyesinde validasyon her zaman gereklidir, schema tiplerine güvenmek yetmez

Gerçek bir projede bu kavramları uygulamaya başladığında farkı hemen göreceksin. Özellikle büyük ekiplerde çalışırken input type’ların sağladığı standartlaşma paha biçilmez oluyor. Yeni bir geliştirici projeye dahil olduğunda input type tanımlarına bakarak API’nin nasıl çalıştığını anlayabiliyor, dokümantasyona ihtiyaç duymadan.

Bir yanıt yazın

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