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ı:
nulldeğ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 < endDategibi 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.
