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 createHttpClient bunu 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.all ile 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-complexity paketi 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.

Bir yanıt yazın

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