GraphQL Resolver Yazımı: Veriyi Kaynaktan Çekme
GraphQL ile çalışmaya başladığınızda şema tasarımı güzel görünür, type’lar yerli yerinde durur, query’ler mantıklıdır. Ama asıl iş resolver’ları yazmaya geldiğinde başlar. Çünkü resolver, GraphQL’in “bu veriyi nereden alacağım?” sorusuna verdiği cevaptır. Veritabanı mı, REST API mi, cache mi, yoksa başka bir servis mi? Hepsini resolver içinde halledersiniz. Bu yazıda resolver yazımını sıfırdan ele alacağız, gerçek dünya senaryolarıyla pekiştireceğiz.
Resolver Nedir, Ne İşe Yarar?
GraphQL şemanızda bir field tanımladığınızda, o field’ın değerini kim hesaplar? İşte resolver. Her field için bir resolver fonksiyonu yazabilirsiniz. Eğer yazmazsanız GraphQL default resolver’ı kullanır, yani parent objedeki aynı isimli property’yi döner.
Resolver fonksiyonları dört parametre alır:
- root (parent): Üst resolver’dan gelen obje. Root query’lerde genellikle null ya da boş objedir.
- args: Client’ın query’de gönderdiği argümanlar.
id,filter,paginationgibi şeyler buradan gelir. - context: Tüm resolver’lar arasında paylaşılan obje. Veritabanı bağlantısı, authentication bilgisi, dataloader instance’ları buraya koyulur.
- info: Query hakkında meta bilgi. Hangi field’ların istendiği, query AST’i gibi şeyler. Çoğu zaman kullanmazsınız ama bazen işe yarar.
// Temel resolver imzası
const resolver = (parent, args, context, info) => {
// veriyi döndür
};
Bu dört parametreyi aklınızda tutun. Resolver yazarken sürekli bunlara başvuracaksınız.
İlk Resolver’ı Yazmak
Basit bir kullanıcı yönetim sistemi düşünelim. Şemamız şöyle olsun:
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
}
Bu şema için resolver map’i oluşturalım. Node.js ve Apollo Server kullandığımızı varsayalım, ama mantık her platformda aynıdır.
const resolvers = {
Query: {
// Tekil kullanıcı getir
user: async (parent, args, context) => {
const { id } = args;
const user = await context.db.users.findById(id);
if (!user) {
throw new Error(`Kullanıcı bulunamadı: ${id}`);
}
return user;
},
// Tüm kullanıcıları listele
users: async (parent, args, context) => {
return await context.db.users.findAll();
},
// Tekil post getir
post: async (parent, args, context) => {
const { id } = args;
return await context.db.posts.findById(id);
}
}
};
Burada dikkat edin, her resolver async fonksiyon. GraphQL otomatik olarak Promise’leri çözümler, dolayısıyla async/await kullanmak hem temiz hem de güvenli.
Context ile Veritabanı Bağlantısı Yönetimi
Context, resolver’lar arası köprüdür. Veritabanı bağlantınızı her resolver’a ayrı ayrı import etmek yerine context üzerinden paylaşırsınız. Apollo Server’da context şöyle ayarlanır:
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
// Her request için çalışır
const token = req.headers.authorization || '';
const user = await verifyToken(token);
return {
db: prisma,
currentUser: user,
};
},
listen: { port: 4000 },
});
Artık tüm resolver’larınızda context.db ile Prisma’ya, context.currentUser ile oturum açmış kullanıcıya erişebilirsiniz. Bu pattern büyük projelerde çok temiz kalmanızı sağlar.
İlişkili Veri Çekme: Nested Resolver’lar
GraphQL’in gerçek gücü ilişkili verileri tek sorguda çekebilmektir. Kullanıcının postlarını çekmek istediğimizde User type’ına bir resolver eklememiz gerekir:
const resolvers = {
Query: {
user: async (parent, args, context) => {
return await context.db.user.findUnique({
where: { id: args.id }
});
},
users: async (parent, args, context) => {
return await context.db.user.findMany();
}
},
// User type'ının field resolver'ları
User: {
posts: async (parent, args, context) => {
// parent burada User objesi
// parent.id ile o kullanıcının postlarını çekiyoruz
return await context.db.post.findMany({
where: { authorId: parent.id }
});
}
},
// Post type'ının field resolver'ları
Post: {
author: async (parent, args, context) => {
// parent burada Post objesi
return await context.db.user.findUnique({
where: { id: parent.authorId }
});
}
}
};
Bu yapı çalışır ama bir sorun var: N+1 problemi. 10 kullanıcı getirirseniz, her kullanıcı için ayrı bir veritabanı sorgusu tetiklenir. 10 kullanıcı için 10 + 1 = 11 sorgu. Bunu ileride DataLoader ile çözeceğiz.
Gerçek Dünya Senaryosu: E-ticaret Uygulaması
Bir e-ticaret sisteminde ürünleri ve kategorileri yöneten resolver’lar yazalım. Bu sefer daha karmaşık bir senaryo:
type Product {
id: ID!
name: String!
price: Float!
stock: Int!
category: Category!
reviews: [Review!]!
averageRating: Float
}
type Query {
products(
categoryId: ID
minPrice: Float
maxPrice: Float
inStock: Boolean
page: Int
limit: Int
): ProductConnection!
}
type ProductConnection {
items: [Product!]!
totalCount: Int!
hasNextPage: Boolean!
}
const resolvers = {
Query: {
products: async (parent, args, context) => {
const {
categoryId,
minPrice,
maxPrice,
inStock,
page = 1,
limit = 20
} = args;
// Filtre objesi oluştur
const where = {};
if (categoryId) where.categoryId = categoryId;
if (minPrice !== undefined) where.price = { ...where.price, gte: minPrice };
if (maxPrice !== undefined) where.price = { ...where.price, lte: maxPrice };
if (inStock === true) where.stock = { gt: 0 };
if (inStock === false) where.stock = 0;
const skip = (page - 1) * limit;
// Toplam sayıyı ve ürünleri paralel çek
const [items, totalCount] = await Promise.all([
context.db.product.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' }
}),
context.db.product.count({ where })
]);
return {
items,
totalCount,
hasNextPage: skip + items.length < totalCount
};
}
},
Product: {
category: async (parent, args, context) => {
return await context.db.category.findUnique({
where: { id: parent.categoryId }
});
},
reviews: async (parent, args, context) => {
return await context.db.review.findMany({
where: { productId: parent.id },
orderBy: { createdAt: 'desc' }
});
},
// averageRating hesaplanan bir field
averageRating: async (parent, args, context) => {
const result = await context.db.review.aggregate({
where: { productId: parent.id },
_avg: { rating: true }
});
return result._avg.rating;
}
}
};
Burada Promise.all kullanımına dikkat edin. Toplam sayı ve ürün listesini paralel sorgularla çekiyoruz. Bu, sıralı sorguya göre neredeyse yarı sürede tamamlanır.
DataLoader ile N+1 Problemini Çözmek
N+1 problemi GraphQL’in en bilinen zorluğudur. DataLoader bu sorunu batch loading ve caching ile çözer. Her request başında yeni DataLoader instance’ları oluşturmanız gerekir, aksi halde cache’ler requestler arasında karışır.
const DataLoader = require('dataloader');
// Context factory'nizde DataLoader'ları oluşturun
const createLoaders = (db) => ({
userLoader: new DataLoader(async (userIds) => {
// Tüm id'leri tek sorguda çek
const users = await db.user.findMany({
where: { id: { in: userIds } }
});
// DataLoader sırayı korumak ister, map ile düzenle
const userMap = {};
users.forEach(user => {
userMap[user.id] = user;
});
return userIds.map(id => userMap[id] || null);
}),
postsByUserLoader: new DataLoader(async (userIds) => {
const posts = await db.post.findMany({
where: { authorId: { in: userIds } }
});
// Her userId için postları grupla
const postMap = {};
userIds.forEach(id => { postMap[id] = []; });
posts.forEach(post => {
if (postMap[post.authorId]) {
postMap[post.authorId].push(post);
}
});
return userIds.map(id => postMap[id]);
})
});
// Context'e ekle
context: async ({ req }) => {
return {
db: prisma,
loaders: createLoaders(prisma),
currentUser: await verifyToken(req.headers.authorization)
};
}
Artık resolver’larınızda DataLoader kullanırsınız:
const resolvers = {
Post: {
// Artık N+1 yok, batch halinde yükleniyor
author: async (parent, args, context) => {
return await context.loaders.userLoader.load(parent.authorId);
}
},
User: {
posts: async (parent, args, context) => {
return await context.loaders.postsByUserLoader.load(parent.id);
}
}
};
DataLoader magic’i şudur: Aynı request döngüsünde birden fazla load() çağrısı yapılırsa, bunları tek bir batch’te toplar ve tek veritabanı sorgusuyla çözer. 100 kullanıcının yazarını çekmek isteseydiniz 100 sorgu yerine 1 sorgu gider.
REST API’den Veri Çekme
Her şey veritabanı değil. Bazen resolver’ınız üçüncü parti bir REST API’ye bağlanmak zorunda kalır. Mesela bir hava durumu servisi:
const fetch = require('node-fetch');
const resolvers = {
Query: {
weather: async (parent, args, context) => {
const { city } = args;
try {
const response = await fetch(
`https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${encodeURIComponent(city)}`
);
if (!response.ok) {
throw new Error(`Hava durumu servisi hata döndürdü: ${response.status}`);
}
const data = await response.json();
// GraphQL şemanıza uygun formata dönüştür
return {
city: data.location.name,
temperature: data.current.temp_c,
humidity: data.current.humidity,
description: data.current.condition.text,
updatedAt: new Date(data.current.last_updated).toISOString()
};
} catch (error) {
// Hataları GraphQL'e uygun şekilde yönet
console.error('Hava durumu çekme hatası:', error);
throw new Error('Hava durumu bilgisi şu an alınamıyor');
}
}
}
};
Burada önemli bir nokta: External API hatalarını doğrudan client’a yansıtmayın. Loglayın, sonra kullanıcı dostu bir hata mesajı döndürün. Servis API key’lerinizi veya internal hata detaylarını client’a sızdırmak güvenlik açığıdır.
Mutation Resolver’ları
Veri yazmak da okumak kadar önemli. Mutation resolver’ları genellikle validation, authorization ve veritabanı işlemlerini birleştirir:
const resolvers = {
Mutation: {
createPost: async (parent, args, context) => {
// Authentication kontrolü
if (!context.currentUser) {
throw new Error('Bu işlem için giriş yapmanız gerekiyor');
}
const { title, content, categoryId } = args.input;
// Basit validation
if (!title || title.trim().length < 3) {
throw new Error('Başlık en az 3 karakter olmalı');
}
if (!content || content.trim().length < 10) {
throw new Error('İçerik en az 10 karakter olmalı');
}
// Kategori var mı kontrol et
const category = await context.db.category.findUnique({
where: { id: categoryId }
});
if (!category) {
throw new Error('Belirtilen kategori bulunamadı');
}
// Postu oluştur
const post = await context.db.post.create({
data: {
title: title.trim(),
content: content.trim(),
authorId: context.currentUser.id,
categoryId
}
});
return post;
},
updatePost: async (parent, args, context) => {
if (!context.currentUser) {
throw new Error('Bu işlem için giriş yapmanız gerekiyor');
}
const { id, ...updateData } = args.input;
// Post var mı ve bu kullanıcıya ait mi?
const existingPost = await context.db.post.findUnique({
where: { id }
});
if (!existingPost) {
throw new Error('Post bulunamadı');
}
if (existingPost.authorId !== context.currentUser.id) {
throw new Error('Bu postu düzenleme yetkiniz yok');
}
return await context.db.post.update({
where: { id },
data: updateData
});
}
}
};
Resolver’larda Hata Yönetimi
GraphQL hataları REST’ten farklı çalışır. Bir field hata verirse GraphQL o field’ı null yapar ve errors array’ine ekler, ama diğer field’lar çalışmaya devam eder. Bunu daha iyi yönetmek için custom error class’ları kullanın:
const { GraphQLError } = require('graphql');
// Custom hata tipleri
class NotFoundError extends GraphQLError {
constructor(resource, id) {
super(`${resource} bulunamadı: ${id}`, {
extensions: {
code: 'NOT_FOUND',
resource,
id
}
});
}
}
class AuthorizationError extends GraphQLError {
constructor(message = 'Bu işlem için yetkiniz yok') {
super(message, {
extensions: {
code: 'FORBIDDEN'
}
});
}
}
// Resolver'larda kullanımı
const resolvers = {
Query: {
user: async (parent, args, context) => {
const user = await context.db.user.findUnique({
where: { id: args.id }
});
if (!user) {
throw new NotFoundError('Kullanıcı', args.id);
}
// Admin değilse başka kullanıcıların detayını gösterme
if (user.id !== context.currentUser?.id && !context.currentUser?.isAdmin) {
throw new AuthorizationError();
}
return user;
}
}
};
extensions alanı client tarafında hata tipini programatik olarak kontrol etmek için kullanılır. code: 'NOT_FOUND' gibi sabit kodlar frontend’in hataları işlemesini kolaylaştırır.
Resolver Performans Optimizasyonu
Büyük projelerde resolver performansı kritik hale gelir. Birkaç pratik ipucu:
- Sadece ihtiyaç duyulan field’ları seç:
infoparametresi sayesinde client’ın hangi field’ları istediğini bilip veritabanı sorgusunu optimize edebilirsiniz. - Cache kullanın: Sık değişmeyen veriler için Redis veya in-memory cache ekleyin.
- Promise.all ile paralel sorgular çalıştırın: Bağımsız sorgular için asla sıralı await kullanmayın.
- Pagination zorunlu kılın: Liste döndüren her resolver’da limit/offset veya cursor-based pagination uygulayın.
// Kötü: Sıralı sorgular
const user = await db.user.findUnique({ where: { id } });
const posts = await db.post.findMany({ where: { authorId: id } });
const stats = await db.userStat.findUnique({ where: { userId: id } });
// İyi: Paralel sorgular
const [user, posts, stats] = await Promise.all([
db.user.findUnique({ where: { id } }),
db.post.findMany({ where: { authorId: id } }),
db.userStat.findUnique({ where: { userId: id } })
]);
Sonuç
Resolver yazmak başta basit görünür, ama production kalitesinde resolver’lar için göz önünde bulundurmanız gereken pek çok şey var. Authentication ve authorization kontrollerini unutmamak, N+1 problemine karşı DataLoader kullanmak, hataları doğru yönetmek, performans için paralel sorgular çalıştırmak ve dış servislere yapılan çağrılarda sağlam hata yakalama mekanizmaları kurmak bunların başında geliyor.
Context’i iyi tasarlamak resolver kodunu ciddi ölçüde sadeleştirir. Veritabanı bağlantısı, DataLoader instance’ları ve kullanıcı bilgisini context üzerinden paylaşmak, her resolver’ı kendi başına yönetilebilir bir birim haline getirir.
Başlangıç için her şeyi mükemmel yapmanıza gerek yok. Önce çalışan resolver’lar yazın, sonra DataLoader ekleyin, ardından error handling’i iyileştirin. GraphQL’in güzelliği şema değişmeden resolver implementasyonunu istediğiniz gibi geliştirebilmenizdir.
