REST ve GraphQL Hibrit Mimari: İkisini Birlikte Kullanma
Büyük bir e-ticaret platformunu tamamen GraphQL’e geçirme görevi aldığınızda, ilk içgüdünüz muhtemelen her şeyi bir gecede dönüştürmek olur. Sonra gerçeklik sizi yakalar: yüzlerce REST endpoint, onlarca entegre servis, müşteri uygulamaları ve mobil istemciler. Sıfırdan başlamak ne mümkün ne de mantıklı. İşte tam bu noktada hibrit mimari devreye giriyor ve aslında bu “geçici çözüm” diye düşündüğünüz yaklaşım, uzun vadede en sağlıklı mimari kararlardan biri haline gelebiliyor.
Hibrit Mimari Neden Mantıklı?
REST ve GraphQL’i birlikte kullanmak, birinin diğerinden üstün olmadığını kabul etmekle başlar. REST, basit CRUD operasyonları, dosya yüklemeleri ve cache-friendly endpoint’ler için hala güçlü bir seçenek. GraphQL ise karmaşık veri ilişkileri, esnek sorgular ve frontend ekiplerinin bağımsız çalışabilmesi için çok daha verimli.
Gerçek dünya senaryolarında şunu görürsünüz: ödeme sistemi REST’te kalır çünkü Stripe, PayPal gibi servisler REST tabanlı çalışır ve bu entegrasyonları yeniden yazmak riske değmez. Ama ürün kataloğu, kullanıcı profilleri ve öneri motorları GraphQL’e geçer çünkü buradaki veri ilişkileri çok boyutlu ve frontend ekibi her gün farklı kombinasyonlara ihtiyaç duyar.
Önemli olan şu: İkisini aynı anda kullanmak bir mimari zafiyet değil, pragmatik bir güç.
Temel Mimari Tasarım
Hibrit yapıda genellikle üç farklı yaklaşım görürsünüz.
API Gateway Yaklaşımı
Tüm istekler tek bir gateway üzerinden geçer. Gateway, isteğin türüne göre ya REST servisine ya da GraphQL sunucusuna yönlendirir.
# Nginx tabanlı basit bir gateway konfigürasyonu
# /etc/nginx/conf.d/api-gateway.conf
upstream rest_backend {
server rest-api:3000;
keepalive 32;
}
upstream graphql_backend {
server graphql-api:4000;
keepalive 32;
}
server {
listen 80;
server_name api.yourplatform.com;
# GraphQL tüm istekler tek endpoint'e
location /graphql {
proxy_pass http://graphql_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# REST endpoint'leri versiyonlanmış şekilde
location /api/v1/ {
proxy_pass http://rest_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# REST için agresif cache
proxy_cache rest_cache;
proxy_cache_valid 200 5m;
proxy_cache_use_stale error timeout updating;
}
# Dosya yüklemeleri kesinlikle REST'te kalır
location /api/v1/upload {
proxy_pass http://rest_backend;
client_max_body_size 100M;
proxy_read_timeout 300s;
}
}
GraphQL’in REST’i Sarması (Wrapper Pattern)
Bu yaklaşımda GraphQL sunucusu, arka planda mevcut REST API’larınızı çağırır. Mevcut backend’inizi dokunmadan GraphQL arayüzü eklersiniz.
# Node.js tabanlı GraphQL wrapper kurulumu
mkdir graphql-wrapper && cd graphql-wrapper
npm init -y
npm install apollo-server-express express axios dataloader graphql
# Proje yapısı
mkdir -p src/{resolvers,datasources,schema}
touch src/index.js src/schema/index.js
touch src/datasources/rest-api.js
touch src/resolvers/products.js src/resolvers/users.js
# src/datasources/rest-api.js
# Mevcut REST API'nizi GraphQL datasource olarak sarıyorsunuz
const { RESTDataSource } = require('apollo-datasource-rest');
class ProductAPI extends RESTDataSource {
constructor() {
super();
// Mevcut REST API'nizin base URL'i
this.baseURL = process.env.REST_API_URL || 'http://rest-api:3000/api/v1/';
}
// REST endpoint'lerini GraphQL resolver'larının kullanacağı
// metodlara dönüştürüyoruz
async getProduct(id) {
return this.get(`products/${id}`);
}
async getProducts({ page = 1, limit = 20, category } = {}) {
const params = { page, limit };
if (category) params.category = category;
return this.get('products', params);
}
async createProduct(input) {
return this.post('products', input);
}
async updateProduct(id, input) {
return this.put(`products/${id}`, input);
}
// REST'teki karmaşık filtreleme endpoint'ini GraphQL'e expose ediyoruz
async searchProducts(filters) {
return this.post('products/search', filters);
}
}
module.exports = { ProductAPI };
Bu pattern’in güzelliği şu: frontend ekibi GraphQL sorguları yazarken backend ekibi REST servislerini refactor etmeye devam edebilir. İkisi birbirinden bağımsız ilerler.
Schema Stitching ve Federation
Büyük ölçekli yapılarda her servis kendi GraphQL schema’sını sunar ve bunlar birleştirilir. Ama bazı servisler REST’te kalır ve onlar için gateway seviyesinde wrapper yazılır.
# Docker Compose ile hibrit ortam kurulumu
# docker-compose.yml
version: '3.8'
services:
# API Gateway
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx/api-gateway.conf:/etc/nginx/conf.d/default.conf
depends_on:
- graphql-gateway
- rest-legacy
# GraphQL Gateway (tüm GraphQL servislerini birleştirir)
graphql-gateway:
build: ./graphql-gateway
ports:
- "4000:4000"
environment:
- PRODUCTS_GRAPHQL_URL=http://products-service:4001/graphql
- USERS_GRAPHQL_URL=http://users-service:4002/graphql
- ORDERS_REST_URL=http://rest-legacy:3000/api/v1
- PAYMENT_REST_URL=http://payment-service:3001/api
depends_on:
- products-service
- users-service
- rest-legacy
# Yeni GraphQL servisleri
products-service:
build: ./services/products
ports:
- "4001:4001"
environment:
- DATABASE_URL=postgresql://user:pass@postgres:5432/products
users-service:
build: ./services/users
ports:
- "4002:4002"
# Eski REST servisleri olduğu gibi çalışmaya devam eder
rest-legacy:
build: ./rest-api
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@postgres:5432/legacy
# Ödeme servisi REST'te kalır, değiştirmiyoruz
payment-service:
image: internal/payment-service:2.1.4
ports:
- "3001:3001"
postgres:
image: postgres:15
environment:
POSTGRES_PASSWORD: pass
POSTGRES_USER: user
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
N+1 Problemi ve DataLoader Entegrasyonu
REST’i GraphQL içinden çağırdığınızda en büyük tehlike N+1 problemidir. Bir ürün listesi sorguladığınızda, her ürün için ayrı bir REST çağrısı yapılabilir. 100 ürün için 100 HTTP isteği… Bunu DataLoader ile çözüyorsunuz.
# src/datasources/dataloader-setup.js
# REST API'ye toplu istek atmak için DataLoader kullanımı
const DataLoader = require('dataloader');
const axios = require('axios');
const REST_BASE = process.env.REST_API_URL;
// Kullanıcıları tek seferde batch olarak çekiyoruz
const createUserLoader = () => new DataLoader(async (userIds) => {
console.log(`Batch loading ${userIds.length} users`);
try {
// REST API'niz batch endpoint destekliyorsa
const response = await axios.post(`${REST_BASE}/users/batch`, {
ids: userIds
});
const usersMap = {};
response.data.users.forEach(user => {
usersMap[user.id] = user;
});
// DataLoader sıra koruması gerektirir
return userIds.map(id => usersMap[id] || null);
} catch (error) {
// Batch endpoint yoksa paralel istek yapıyoruz
// Yine de N+1'den çok daha iyi
const users = await Promise.all(
userIds.map(id =>
axios.get(`${REST_BASE}/users/${id}`)
.then(r => r.data)
.catch(() => null)
)
);
return users;
}
});
// Her request için yeni loader oluşturuyoruz (cache izolasyonu için)
const createLoaders = () => ({
users: createUserLoader(),
// Diğer loader'lar buraya eklenir
});
module.exports = { createLoaders };
Authentication ve Authorization Yönetimi
İki sistemi birlikte çalıştırırken en kritik nokta kimlik doğrulamadır. JWT token’larını her iki sistemde de kabul ettirmeniz gerekir.
# middleware/auth.js
# Hem REST hem GraphQL için ortak auth middleware
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET;
// REST endpoint'leri için middleware
const restAuthMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Token gerekli' });
}
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Gecersiz token' });
}
};
# GraphQL context fonksiyonu için kullanılan auth helper
const extractUserFromToken = (token) => {
if (!token) return null;
try {
const cleanToken = token.replace('Bearer ', '');
return jwt.verify(cleanToken, JWT_SECRET);
} catch (err) {
return null;
}
};
# GraphQL Apollo Server context
const graphqlContext = ({ req }) => {
const token = req.headers.authorization || '';
const user = extractUserFromToken(token);
const { createLoaders } = require('./dataloader-setup');
return {
user,
loaders: createLoaders(),
// REST API'ye istek yaparken token'ı taşıyoruz
authHeader: token
};
};
module.exports = { restAuthMiddleware, graphqlContext };
Monitoring ve Observability
İki farklı API sistemi çalıştırdığınızda her ikisini de aynı monitoring altyapısıyla izlemeniz şart.
# Prometheus metrik toplama konfigürasyonu
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
# REST API metrikleri
- job_name: 'rest-api'
static_configs:
- targets: ['rest-legacy:3000']
metrics_path: '/metrics'
scrape_interval: 10s
# GraphQL sunucu metrikleri
- job_name: 'graphql-gateway'
static_configs:
- targets: ['graphql-gateway:4000']
metrics_path: '/metrics'
scrape_interval: 10s
# Nginx gateway metrikleri
- job_name: 'nginx'
static_configs:
- targets: ['nginx-exporter:9113']
# Alert kuralları
rule_files:
- 'alerts.yml'
# alerts.yml - Hibrit sistem için kritik alertler
groups:
- name: hybrid-api-alerts
rules:
# REST endpoint'leri yavaşlarsa
- alert: RESTHighLatency
expr: http_request_duration_seconds{job="rest-api",quantile="0.95"} > 2
for: 5m
labels:
severity: warning
annotations:
summary: "REST API yanit suresi yuksek: {{ $value }}s"
# GraphQL resolver'ları yavaşlarsa
- alert: GraphQLSlowResolvers
expr: graphql_resolver_duration_seconds{quantile="0.99"} > 5
for: 3m
labels:
severity: critical
annotations:
summary: "GraphQL resolver cok yavas: {{ $labels.field_name }}"
# REST API hata orani
- alert: RESTHighErrorRate
expr: rate(http_requests_total{job="rest-api",status=~"5.."}[5m]) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "REST API hata orani: %{{ $value | humanizePercentage }}"
Migration Stratejisi: Kademeli Geçiş
Hibrit mimariyi geçici bir durum olarak görseniz bile, geçiş planı net olmalı. Hangi endpoint’ler önce GraphQL’e geçecek, hangisi REST’te kalacak.
Geçiş önceliği belirlerken şu kriterleri kullanıyoruz:
- Yüksek öncelik: Birden fazla veri kaynağını birleştiren, karmaşık ilişki barındıran endpoint’ler
- Orta öncelik: Sık değişen, frontend’in farklı field kombinasyonlarına ihtiyaç duyduğu endpoint’ler
- Düşük öncelik veya REST’te bırak: Dosya yüklemeleri, webhook endpoint’leri, third-party entegrasyonlar
- Kesinlikle REST’te bırak: Ödeme işlemleri, PCI DSS kapsamındaki veri akışları, binary data transfer
# Geçiş sürecini takip etmek için basit bir script
#!/bin/bash
# migration-tracker.sh
REST_API_URL="http://rest-legacy:3000"
GRAPHQL_URL="http://graphql-gateway:4000/graphql"
echo "=== Hibrit API Durum Raporu ==="
echo ""
# REST endpoint'lerini kontrol et
echo "--- REST Endpoint Durumu ---"
REST_ENDPOINTS=("/api/v1/products" "/api/v1/orders" "/api/v1/upload" "/api/v1/payments")
for endpoint in "${REST_ENDPOINTS[@]}"; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X GET "${REST_API_URL}${endpoint}"
-H "Authorization: Bearer ${TEST_TOKEN}" --max-time 5)
if [ "$STATUS" = "200" ] || [ "$STATUS" = "401" ]; then
echo " AKTIF: ${endpoint} (HTTP ${STATUS})"
else
echo " SORUN: ${endpoint} (HTTP ${STATUS})"
fi
done
echo ""
echo "--- GraphQL Schema Durumu ---"
# GraphQL introspection ile aktif type'ları kontrol et
INTROSPECTION=$(curl -s -X POST "${GRAPHQL_URL}"
-H "Content-Type: application/json"
-d '{"query": "{ __schema { queryType { fields { name } } } }"}')
QUERY_COUNT=$(echo $INTROSPECTION | python3 -c "
import sys, json
data = json.load(sys.stdin)
fields = data['data']['__schema']['queryType']['fields']
print(len(fields))
" 2>/dev/null || echo "0")
echo " Toplam GraphQL query sayisi: ${QUERY_COUNT}"
echo ""
echo "=== Migration Tamamlanma: Tahmini %40 ==="
Gerçek Dünya Senaryosu: E-ticaret Platformu
Bir e-ticaret platformunda hibrit mimariyi nasıl uyguladığımıza bakalım. Kullanıcı işlemleri GraphQL’de, sipariş ve ödeme akışları REST’te kalmış durumda.
Kullanıcı arama ve ürün kataloğu artık tek bir GraphQL sorgusunda:
# Örnek GraphQL sorgusu - bu arka planda birden fazla REST çağrısı yapıyor
# ama frontend bunu bilmek zorunda değil
query GetProductsWithUserHistory($userId: ID!, $category: String) {
user(id: $userId) {
recentlyViewed {
id
name
price
}
recommendations {
id
name
rating
stockStatus
}
}
products(filter: { category: $category }, limit: 20) {
edges {
node {
id
name
price
seller {
name
rating
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
# Bu sorgu arka planda şu REST çağrılarını yapıyor:
# GET /api/v1/users/{userId}
# GET /api/v1/users/{userId}/history
# GET /api/v1/recommendations?userId={userId}
# GET /api/v1/products?category={category}&limit=20
# GET /api/v1/sellers/batch (DataLoader ile tek istek)
# Ama sipariş oluşturma hala REST üzerinden:
# POST /api/v1/orders
# POST /api/v1/payments/initiate
Bu ayrım kasıtlı. Ödeme akışı regulatory audit gerektirir, her adım log’lanır, rollback mekanizmaları var. Bu karmaşıklığı GraphQL’e taşımak şimdilik getiri sağlamaz.
Cache Stratejisi
REST ve GraphQL’in cache stratejileri birbirinden farklı çalışır. REST için HTTP cache header’ları yeterliyken, GraphQL için persisted queries ve query-level caching gerekir.
- REST cache: Nginx seviyesinde
Cache-Control: max-age=300ile basit HTTP caching, CDN entegrasyonu kolay - GraphQL cache: Apollo Server’ın response cache plugin’i, query bazında farklı TTL değerleri
- Ortak cache: Redis ile her iki sistemin de kullandığı shared cache layer
- Cache invalidation: REST endpoint’e yazma yapıldığında hem REST hem GraphQL cache’i temizleme
Cache invalidation için webhook pattern kullanıyoruz: REST’e bir ürün güncellendiğinde, bu bir event fırlatıyor ve hem REST cache hem de GraphQL response cache o ürün için temizleniyor.
Sonuç
REST ve GraphQL’i birlikte kullanmak bir ödün değil, olgunlaşmış bir mühendislik kararı. Her teknolojinin güçlü olduğu alanlarda kullanılması, mimariyi daha sağlam yapıyor.
Hibrit yaklaşımın size verdiği en büyük avantaj hız. Tüm sistemi yeniden yazmadan yeni yetenekler ekleyebiliyorsunuz. Frontend ekibi GraphQL’in esnekliğinden faydalanırken, ödeme ve kritik iş akışlarınız battle-tested REST servislerinde güvenle çalışmaya devam ediyor.
Pratikte şu üç kuralı aklınızda tutun: Ödeme ve güvenlik kritik akışları REST’te bırakın, karmaşık veri ilişkilerini ve esnek sorguları GraphQL’e taşıyın ve her iki sistemi tek bir gateway ile yönetin. Monitoring ve authentication mutlaka merkezi olsun.
Geçiş sürecini bir proje olarak değil, sürekli bir evrim olarak görün. Bugün REST’te olan bir servis, altı ay sonra GraphQL’e geçebilir. Önemli olan geçiş sırasında sisteminizin ayakta kalması ve ekibinizin bunalmadan ilerleyebilmesi.
