Mevcut REST Endpoint’lerini GraphQL ile Sarmalama
Eski bir REST API’nizi tamamen yeniden yazmak zorunda kalmadan GraphQL’in güçlü özelliklerinden yararlanmak mümkün. Bu yaklaşım, özellikle büyük ve köklü sistemlerde son derece değerli çünkü mevcut backend mantığını korurken istemcilere çok daha esnek bir arayüz sunabiliyorsunuz. Ben de bu yazıda, gerçek dünya senaryolarıyla REST endpoint’lerinizi GraphQL ile nasıl saracağınızı adım adım anlatacağım.
Neden REST’i Tamamen Değiştirmek Yerine Sarmalayalım?
Çoğu şirkette yıllarca üzerine çalışılmış, test edilmiş ve üretimde stabil çalışan REST API’leri bulunuyor. Bu API’leri sıfırdan yeniden yazmak hem riskli hem de maliyetli. Öte yandan modern frontend ekipleri GraphQL’in sunduğu esnekliği, tip güvenliğini ve tek sorguda birden fazla kaynağa ulaşabilmeyi talep ediyor.
GraphQL sarmalama (wrapping) yaklaşımı tam da bu boşluğu dolduruyor. REST endpoint’lerini GraphQL resolver’ları arkasına gizleyerek her iki dünyanın da avantajlarından yararlanabiliyorsunuz. Mevcut iş mantığı, authentication mekanizmaları ve veritabanı sorguları olduğu gibi kalıyor; sadece istemciye bakan yüz değişiyor.
Geliştirme Ortamını Hazırlamak
Bu örneklerde Node.js ve Apollo Server kullanacağız. Önce gerekli paketleri yükleyelim:
mkdir graphql-wrapper && cd graphql-wrapper
npm init -y
npm install apollo-server graphql axios node-fetch
npm install --save-dev nodemon @types/node
Proje yapısını da şöyle organize edelim:
mkdir -p src/{resolvers,schema,services,utils}
touch src/index.js src/schema/typeDefs.js src/resolvers/index.js
touch src/services/userService.js src/services/productService.js
touch src/utils/httpClient.js
HTTP İstemcisi Oluşturmak
REST endpoint’lerine istek atacak merkezi bir HTTP istemcisi oluşturmak, kod tekrarını önlemek açısından kritik. Bu katman aynı zamanda authentication token’larını, hata yönetimini ve loglama işlemlerini merkezi olarak yönetmenizi sağlıyor:
cat > src/utils/httpClient.js << 'EOF'
const axios = require('axios');
const createHttpClient = (baseURL, defaultHeaders = {}) => {
const client = axios.create({
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
...defaultHeaders,
},
});
// Request interceptor - her istekte token ekle
client.interceptors.request.use((config) => {
console.log(`[HTTP] ${config.method?.toUpperCase()} ${config.url}`);
return config;
});
// Response interceptor - hataları standartlaştır
client.interceptors.response.use(
(response) => response.data,
(error) => {
const message = error.response?.data?.message || error.message;
const status = error.response?.status || 500;
console.error(`[HTTP Error] Status: ${status}, Message: ${message}`);
throw new Error(`REST API Hatası (${status}): ${message}`);
}
);
return client;
};
module.exports = { createHttpClient };
EOF
GraphQL Şemasını Tanımlamak
Şimdi TypeDefs ile GraphQL şemasını oluşturalım. Gerçek dünya senaryosunda bir e-ticaret sistemini ele alıyoruz:
cat > src/schema/typeDefs.js << 'EOF'
const { gql } = require('apollo-server');
const typeDefs = gql`
type User {
id: ID!
email: String!
username: String!
fullName: String
avatar: String
createdAt: String
orders: [Order]
}
type Product {
id: ID!
name: String!
description: String
price: Float!
stock: Int!
category: Category
images: [String]
}
type Category {
id: ID!
name: String!
slug: String!
}
type Order {
id: ID!
userId: ID!
status: OrderStatus!
totalAmount: Float!
items: [OrderItem]
createdAt: String!
}
type OrderItem {
productId: ID!
quantity: Int!
unitPrice: Float!
product: Product
}
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
type PaginatedProducts {
items: [Product]
total: Int
page: Int
pageSize: Int
hasNextPage: Boolean
}
type Query {
user(id: ID!): User
users(page: Int, limit: Int): [User]
product(id: ID!): Product
products(page: Int, limit: Int, category: String): PaginatedProducts
userOrders(userId: ID!, status: OrderStatus): [Order]
}
type Mutation {
createOrder(userId: ID!, items: [OrderItemInput!]!): Order
updateOrderStatus(orderId: ID!, status: OrderStatus!): Order
updateUserProfile(userId: ID!, input: UserUpdateInput!): User
}
input OrderItemInput {
productId: ID!
quantity: Int!
}
input UserUpdateInput {
fullName: String
avatar: String
}
`;
module.exports = { typeDefs };
EOF
Servis Katmanını Oluşturmak
Her REST API grubu için ayrı bir servis dosyası oluşturmak, bakımı kolaylaştırıyor. Kullanıcı servisi şöyle görünüyor:
cat > src/services/userService.js << 'EOF'
const { createHttpClient } = require('../utils/httpClient');
const userClient = createHttpClient(process.env.USER_API_URL || 'https://api.example.com/v1');
const userService = {
async getUserById(id, context) {
const headers = context?.token ? { Authorization: `Bearer ${context.token}` } : {};
return userClient.get(`/users/${id}`, { headers });
},
async getUsers(page = 1, limit = 10, context) {
const headers = context?.token ? { Authorization: `Bearer ${context.token}` } : {};
return userClient.get('/users', {
params: { page, limit },
headers,
});
},
async updateUserProfile(userId, updateData, context) {
const headers = context?.token ? { Authorization: `Bearer ${context.token}` } : {};
return userClient.patch(`/users/${userId}`, updateData, { headers });
},
async getUserOrders(userId, status, context) {
const headers = context?.token ? { Authorization: `Bearer ${context.token}` } : {};
const params = status ? { status: status.toLowerCase() } : {};
return userClient.get(`/users/${userId}/orders`, { params, headers });
},
};
module.exports = { userService };
EOF
Resolver’ları Yazmak
Resolver’lar, GraphQL sorgularını REST çağrılarına dönüştüren köprüler. Burada dikkat edilmesi gereken en önemli nokta N+1 sorgu problemi. Bunu DataLoader ile çözebiliriz ama önce temel resolver yapısına bakalım:
cat > src/resolvers/index.js << 'EOF'
const { userService } = require('../services/userService');
const { productService } = require('../services/productService');
const { orderService } = require('../services/orderService');
const resolvers = {
Query: {
user: async (_, { id }, context) => {
return userService.getUserById(id, context);
},
users: async (_, { page, limit }, context) => {
return userService.getUsers(page, limit, context);
},
product: async (_, { id }, context) => {
return productService.getProductById(id, context);
},
products: async (_, { page, limit, category }, context) => {
const result = await productService.getProducts({ page, limit, category }, context);
return {
items: result.data,
total: result.meta.total,
page: result.meta.page,
pageSize: result.meta.pageSize,
hasNextPage: result.meta.page * result.meta.pageSize < result.meta.total,
};
},
userOrders: async (_, { userId, status }, context) => {
return userService.getUserOrders(userId, status, context);
},
},
Mutation: {
createOrder: async (_, { userId, items }, context) => {
return orderService.createOrder({ userId, items }, context);
},
updateOrderStatus: async (_, { orderId, status }, context) => {
return orderService.updateOrderStatus(orderId, status, context);
},
updateUserProfile: async (_, { userId, input }, context) => {
return userService.updateUserProfile(userId, input, context);
},
},
// İlişkisel alanlar için field resolver'lar
User: {
orders: async (parent, _, context) => {
// parent.id üzerinden kullanıcının siparişlerini çek
return userService.getUserOrders(parent.id, null, context);
},
},
Order: {
items: async (parent, _, context) => {
// Sipariş item'larını zenginleştir
return parent.items || [];
},
},
OrderItem: {
product: async (parent, _, context) => {
// Her item için ürün detayını getir - DataLoader kullanılması önerilir
return productService.getProductById(parent.productId, context);
},
},
};
module.exports = { resolvers };
EOF
Context ve Authentication Yönetimi
GraphQL’in en güçlü özelliklerinden biri context mekanizması. Her resolver’a ortak bilgileri (kullanıcı, token, DataLoader instance’ları) buradan taşıyabiliyorsunuz:
cat > src/index.js << 'EOF'
const { ApolloServer } = require('apollo-server');
const { typeDefs } = require('./schema/typeDefs');
const { resolvers } = require('./resolvers');
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
// REST API'nizin kullandığı token'ı doğrudan aktarıyoruz
const token = req.headers.authorization?.replace('Bearer ', '') || '';
const userId = req.headers['x-user-id'] || null;
return {
token,
userId,
// İstekte bulunulan IP gibi bilgileri de ekleyebilirsiniz
clientIp: req.ip,
};
},
formatError: (error) => {
// Hassas hata bilgilerini istemciye sızdırmamak için
console.error('[GraphQL Error]', error);
return {
message: error.message,
code: error.extensions?.code || 'INTERNAL_ERROR',
};
},
plugins: [
{
requestDidStart() {
const start = Date.now();
return {
willSendResponse() {
console.log(`[GraphQL] İstek süresi: ${Date.now() - start}ms`);
},
};
},
},
],
});
const PORT = process.env.PORT || 4000;
server.listen(PORT).then(({ url }) => {
console.log(`GraphQL Wrapper ${url} adresinde çalışıyor`);
});
EOF
DataLoader ile N+1 Sorununu Çözmek
Sipariş listesi döndüğünde her sipariş için ayrı ürün sorgusu yapmak, REST API’nizi boğabilir. DataLoader bu sorguları batch’leyerek tek seferde halleder:
npm install dataloader
cat > src/utils/loaders.js << 'EOF'
const DataLoader = require('dataloader');
const { productService } = require('../services/productService');
const createLoaders = (context) => ({
productLoader: new DataLoader(async (productIds) => {
console.log(`[DataLoader] ${productIds.length} ürün batch sorgusu`);
// REST API'niz toplu sorguyu destekliyorsa:
// GET /products?ids=1,2,3,4,5
const products = await productService.getProductsByIds(productIds, context);
// DataLoader, sonuçları gelen ID sırasına göre döndürmeyi bekler
const productMap = {};
products.forEach((p) => { productMap[p.id] = p; });
return productIds.map((id) => productMap[id] || null);
}),
});
module.exports = { createLoaders };
EOF
Context’e loader’ları ekleyin:
# src/index.js context fonksiyonunu güncelleyin
context: ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '') || '';
const loaders = createLoaders({ token });
return { token, loaders };
},
REST API Yanıtlarını Dönüştürmek
REST API’leri genellikle GraphQL şemanızla birebir örtüşmeyen yapılar döndürür. Özellikle snake_case’den camelCase’e dönüşüm ve iç içe geçmiş veri yapıları sıkça karşılaşılan durumlar:
cat > src/utils/transformers.js << 'EOF'
// Snake case alanları camelCase'e çevir
const toCamelCase = (obj) => {
if (Array.isArray(obj)) return obj.map(toCamelCase);
if (obj === null || typeof obj !== 'object') return obj;
return Object.keys(obj).reduce((acc, key) => {
const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
acc[camelKey] = toCamelCase(obj[key]);
return acc;
}, {});
};
// REST yanıtından GraphQL User tipine dönüştür
const transformUser = (restUser) => {
if (!restUser) return null;
const user = toCamelCase(restUser);
return {
id: user.id || user.userId,
email: user.email,
username: user.username || user.login,
fullName: user.fullName || `${user.firstName} ${user.lastName}`.trim(),
avatar: user.avatar || user.profilePicture || null,
createdAt: user.createdAt || user.registeredAt,
};
};
// REST yanıtından GraphQL Product tipine dönüştür
const transformProduct = (restProduct) => {
if (!restProduct) return null;
const product = toCamelCase(restProduct);
return {
id: product.id || product.productId,
name: product.name || product.title,
description: product.description || product.shortDesc,
price: parseFloat(product.price) || 0,
stock: parseInt(product.stockQuantity || product.stock, 10) || 0,
images: product.images || product.photos || [],
category: product.category || null,
};
};
module.exports = { toCamelCase, transformUser, transformProduct };
EOF
Gerçek Dünya Senaryosu: Mikro Servis Entegrasyonu
Diyelim ki user servisi api.users.internal, product servisi api.products.internal ve order servisi api.orders.internal adreslerinde çalışıyor. Tüm bu servislere tek bir GraphQL endpoint üzerinden erişmek isteyen bir frontend ekibiniz var:
cat > src/services/productService.js << 'EOF'
const { createHttpClient } = require('../utils/httpClient');
const { transformProduct } = require('../utils/transformers');
const productClient = createHttpClient(
process.env.PRODUCT_API_URL || 'https://api.products.internal/v2'
);
const productService = {
async getProductById(id, context) {
const raw = await productClient.get(`/catalog/items/${id}`);
return transformProduct(raw);
},
async getProductsByIds(ids, context) {
// Toplu sorgu endpoint'i varsa kullan
const raw = await productClient.get('/catalog/items/batch', {
params: { ids: ids.join(',') },
});
return (raw.items || []).map(transformProduct);
},
async getProducts({ page = 1, limit = 20, category }, context) {
const params = { page, per_page: limit };
if (category) params.category_slug = category;
const raw = await productClient.get('/catalog/items', { params });
return {
data: (raw.results || []).map(transformProduct),
meta: {
total: raw.count || 0,
page: raw.current_page || page,
pageSize: limit,
},
};
},
};
module.exports = { productService };
EOF
Hata Yönetimi ve Monitoring
Prodüksiyonda çalışırken REST API hatalarını düzgün yönetmek çok önemli. Apollo Server’ın hata sınıflarını kullanmak daha anlamlı hata mesajları üretmenizi sağlar:
cat > src/utils/errorHandler.js << 'EOF'
const { ApolloError, AuthenticationError, ForbiddenError } = require('apollo-server');
const handleRestError = (error, resourceType = 'Kaynak') => {
const status = parseInt(error.message.match(/((d+))/)?.[1]);
switch (status) {
case 400:
throw new ApolloError(`Geçersiz istek: ${error.message}`, 'BAD_REQUEST');
case 401:
throw new AuthenticationError('Kimlik doğrulama başarısız. Lütfen tekrar giriş yapın.');
case 403:
throw new ForbiddenError(`Bu işlem için yetkiniz bulunmuyor: ${resourceType}`);
case 404:
throw new ApolloError(`${resourceType} bulunamadı`, 'NOT_FOUND');
case 429:
throw new ApolloError('Çok fazla istek gönderildi. Lütfen bekleyin.', 'RATE_LIMITED');
case 503:
throw new ApolloError('Servis geçici olarak kullanılamıyor', 'SERVICE_UNAVAILABLE');
default:
throw new ApolloError(`Beklenmeyen hata: ${error.message}`, 'INTERNAL_ERROR');
}
};
module.exports = { handleRestError };
EOF
Caching Stratejisi
REST API’lerinize giden yükü azaltmak için basit bir in-memory cache veya Redis entegrasyonu ekleyebilirsiniz. Apollo Server’ın @cacheControl direktifi bu iş için kullanışlı:
cat > src/utils/cache.js << 'EOF'
// Basit in-memory cache - prodüksiyon için Redis kullanın
const cache = new Map();
const withCache = async (key, ttlSeconds, fetchFn) => {
const now = Date.now();
const cached = cache.get(key);
if (cached && now - cached.timestamp < ttlSeconds * 1000) {
console.log(`[Cache HIT] ${key}`);
return cached.data;
}
console.log(`[Cache MISS] ${key}`);
const data = await fetchFn();
cache.set(key, { data, timestamp: now });
// Bellek temizliği - production'da bunu Redis TTL ile yönetin
setTimeout(() => cache.delete(key), ttlSeconds * 1000);
return data;
};
module.exports = { withCache };
EOF
Sunucuyu Ayağa Kaldırmak ve Test Etmek
Ortam değişkenlerini ayarlayıp sunucuyu başlatın:
export USER_API_URL="https://jsonplaceholder.typicode.com"
export PRODUCT_API_URL="https://fakestoreapi.com"
export PORT=4000
node src/index.js
# veya geliştirme modunda:
npx nodemon src/index.js
Apollo Studio veya Insomnia ile test edebileceğiniz örnek sorgular:
# GraphQL Playground'da test için
curl -X POST http://localhost:4000/graphql
-H "Content-Type: application/json"
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
-d '{
"query": "{ user(id: "1") { id email username orders { id status totalAmount } } }"
}'
Performans İpuçları
Gerçek dünyada bu mimarinin sorunsuz çalışması için birkaç konuya dikkat etmeniz gerekiyor:
- Connection pooling: Her resolver çağrısında yeni HTTP bağlantısı açmak yerine axios instance’larını modül seviyesinde oluşturun. Yukarıdaki
createHttpClientbunu zaten yapıyor. - Timeout ayarları: REST servislerinizin yanıt süresine göre makul timeout değerleri belirleyin. 10 saniye genellikle iyi bir başlangıç noktası.
- Paralel sorgular: Birbirinden bağımsız REST çağrılarını
Promise.allile paralel çalıştırın, sıralı beklemeyin. - DataLoader kullanımı: Özellikle liste döndüren sorgularda field resolver’larınızın DataLoader üzerinden geçmesini sağlayın.
- Query complexity limiting: Kötü niyetli veya hatalı yazılmış sorgular, zincirleme REST çağrılarıyla backend’inizi boğabilir.
graphql-query-complexitypaketi bu riski azaltır. - Introspection’ı prodüksiyonda kapatın:
introspection: process.env.NODE_ENV !== 'production'ayarını mutlaka yapın.
Sonuç
REST endpoint’lerini GraphQL ile sarmak, mevcut yatırımlarınızı korurken modern API deneyimi sunmanın en pragmatik yolu. Özellikle büyük kurumlarda veya legacy sistemlerin bulunduğu ortamlarda “büyük patlama” yerine bu kademeli geçiş stratejisi çok daha az risk taşıyor.
Bu yaklaşımın en büyük avantajı, backend ekibinin REST servislerini normal şekilde geliştirmeye devam edebilmesi. GraphQL katmanı, frontend ekibinin ihtiyaçlarına göre bağımsız olarak evrilebiliyor. DataLoader ile N+1 problemini, merkezi hata yönetimiyle tutarsız API davranışlarını ve context mekanizmasıyla da authentication karmaşasını çözebiliyorsunuz.
Uzun vadede, yeterince emin olduğunuz servisler için resolver’ların arkasındaki REST çağrılarını doğrudan veritabanı sorgularıyla değiştirerek tam bir GraphQL’e geçiş yapabilirsiniz. Bu da sarmalama yaklaşımını güzel yapan şey: size zaman kazandırıyor, risklerinizi azaltıyor ve geri dönüş noktanız her zaman mevcut kalıyor.
