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=300 ile 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.

Bir yanıt yazın

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