GraphQL Schema Stitching ile Birden Fazla API Birleştirme
Mikroservis mimarisine geçiş yapan bir ekipte çalışıyorsanız, muhtemelen şu soruyla karşılaşmışsınızdır: “Elimde birbirinden bağımsız 5 farklı API var, bunları frontend’den nasıl tek bir yerden sorgulayacağım?” İşte GraphQL Schema Stitching tam bu noktada devreye giriyor. Birden fazla GraphQL şemasını birleştirerek tek bir unified API sunan bu yaklaşım, özellikle REST’ten GraphQL’e geçiş döneminde hayat kurtarıcı oluyor.
Schema Stitching Nedir ve Neden Kullanırız?
Schema Stitching, birden fazla GraphQL şemasını tek bir üst şema altında birleştirme işlemidir. Bunu bir çeşit API gateway olarak düşünebilirsiniz, ancak GraphQL’e özgü süper güçlerle donatılmış bir gateway.
Gerçek dünya senaryosuna bakalım: Diyelim ki bir e-ticaret platformu yönetiyorsunuz. Kullanıcı yönetimi için ayrı bir servis, ürün kataloğu için ayrı bir servis, sipariş takibi için ayrı bir servis ve ödeme işlemleri için ayrı bir servisiniz var. Bu servislerin bir kısmı hâlâ REST, bir kısmı yeni GraphQL’e geçirilmiş durumda. Frontend ekibi ise her şeyi tek endpoint’ten sorgulamak istiyor.
Schema Stitching’in öne çıkan avantajları:
- Tek endpoint: Frontend sadece
/graphqladresine istek atar - Kademeli geçiş: REST servislerini yavaş yavaş GraphQL’e dönüştürebilirsiniz
- Bağımsız deployment: Her servis kendi yaşam döngüsünde gelişmeye devam eder
- Type birleştirme: Farklı şemalardan gelen tipler birbiriyle ilişkilendirilebilir
- Merkezi authorization: Auth mantığını tek yerden yönetebilirsiniz
Temel Kurulum ve Bağımlılıklar
Önce projemizi hazırlayalım. Node.js tabanlı bir gateway servisi kuracağız:
mkdir graphql-gateway && cd graphql-gateway
npm init -y
npm install @graphql-tools/schema @graphql-tools/stitch @graphql-tools/wrap
@graphql-tools/remote-schemas graphql apollo-server express
node-fetch graphql-tag
npm install --save-dev typescript @types/node ts-node nodemon
TypeScript konfigürasyonu için:
cat > tsconfig.json << 'EOF'
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
EOF
mkdir src
Alt Servislerin Hazırlanması
Önce iki bağımsız GraphQL servisi oluşturalım. Gerçek senaryoda bunlar ayrı sunucularda çalışıyor olurdu.
Kullanıcı Servisi (port 4001):
cat > src/user-service.ts << 'EOF'
import { ApolloServer, gql } from 'apollo-server';
const typeDefs = gql`
type User {
id: ID!
username: String!
email: String!
createdAt: String!
role: String!
}
type Query {
user(id: ID!): User
users: [User!]!
me: User
}
type Mutation {
updateUserRole(userId: ID!, role: String!): User
}
`;
const users = [
{ id: '1', username: 'ahmet_sys', email: '[email protected]', createdAt: '2024-01-15', role: 'admin' },
{ id: '2', username: 'zeynep_dev', email: '[email protected]', createdAt: '2024-02-20', role: 'developer' },
{ id: '3', username: 'mehmet_ops', email: '[email protected]', createdAt: '2024-03-10', role: 'operator' },
];
const resolvers = {
Query: {
user: (_: any, { id }: { id: string }) => users.find(u => u.id === id),
users: () => users,
me: () => users[0],
},
Mutation: {
updateUserRole: (_: any, { userId, role }: { userId: string; role: string }) => {
const user = users.find(u => u.id === userId);
if (user) user.role = role;
return user;
},
},
};
const server = new ApolloServer({ typeDefs, resolvers });
server.listen(4001).then(() => {
console.log('User Service: http://localhost:4001');
});
EOF
Ürün Servisi (port 4002):
cat > src/product-service.ts << 'EOF'
import { ApolloServer, gql } from 'apollo-server';
const typeDefs = gql`
type Product {
id: ID!
name: String!
price: Float!
stock: Int!
category: String!
ownerId: ID!
}
type Query {
product(id: ID!): Product
products(category: String): [Product!]!
productsByOwner(ownerId: ID!): [Product!]!
}
type Mutation {
createProduct(name: String!, price: Float!, stock: Int!, category: String!, ownerId: ID!): Product
updateStock(productId: ID!, quantity: Int!): Product
}
`;
const products = [
{ id: 'p1', name: 'Sunucu Rack', price: 15000, stock: 10, category: 'hardware', ownerId: '1' },
{ id: 'p2', name: 'SSD 2TB', price: 3500, stock: 50, category: 'storage', ownerId: '2' },
{ id: 'p3', name: 'UPS 3000VA', price: 8000, stock: 20, category: 'power', ownerId: '1' },
];
let idCounter = 4;
const resolvers = {
Query: {
product: (_: any, { id }: { id: string }) => products.find(p => p.id === id),
products: (_: any, { category }: { category?: string }) =>
category ? products.filter(p => p.category === category) : products,
productsByOwner: (_: any, { ownerId }: { ownerId: string }) =>
products.filter(p => p.ownerId === ownerId),
},
Mutation: {
createProduct: (_: any, args: any) => {
const product = { id: `p${idCounter++}`, ...args };
products.push(product);
return product;
},
updateStock: (_: any, { productId, quantity }: { productId: string; quantity: number }) => {
const product = products.find(p => p.id === productId);
if (product) product.stock = quantity;
return product;
},
},
};
const server = new ApolloServer({ typeDefs, resolvers });
server.listen(4002).then(() => {
console.log('Product Service: http://localhost:4002');
});
EOF
Gateway Servisi: Schema Stitching’in Kalbi
Şimdi asıl gateway’i oluşturalım. Bu servis her iki şemayı birleştirecek ve aralarındaki ilişkileri kuracak:
cat > src/gateway.ts << 'EOF'
import { ApolloServer } from 'apollo-server';
import { stitchSchemas } from '@graphql-tools/stitch';
import { schemaFromExecutor } from '@graphql-tools/wrap';
import { buildHTTPExecutor } from '@graphql-tools/executor-http';
async function buildGatewaySchema() {
// Remote şemaları executor üzerinden al
const userExecutor = buildHTTPExecutor({
endpoint: 'http://localhost:4001/graphql',
});
const productExecutor = buildHTTPExecutor({
endpoint: 'http://localhost:4002/graphql',
});
const userSchema = await schemaFromExecutor(userExecutor);
const productSchema = await schemaFromExecutor(productExecutor);
// Şemaları birleştir ve type merging tanımla
return stitchSchemas({
subschemas: [
{
schema: userSchema,
executor: userExecutor,
merge: {
User: {
fieldName: 'user',
selectionSet: '{ id }',
args: ({ id }: { id: string }) => ({ id }),
},
},
},
{
schema: productSchema,
executor: productExecutor,
merge: {
Product: {
fieldName: 'product',
selectionSet: '{ id }',
args: ({ id }: { id: string }) => ({ id }),
},
},
},
],
// İki şema arasında köprü kuran ek tip tanımları
typeDefs: `
extend type Product {
owner: User
}
extend type User {
products: [Product!]!
}
`,
resolvers: {
Product: {
owner: {
selectionSet: '{ ownerId }',
resolve(product, _args, context, info) {
return info.mergeInfo.delegateToSchema({
schema: userSchema,
operation: 'query',
fieldName: 'user',
args: { id: product.ownerId },
context,
info,
});
},
},
},
User: {
products: {
selectionSet: '{ id }',
resolve(user, _args, context, info) {
return info.mergeInfo.delegateToSchema({
schema: productSchema,
operation: 'query',
fieldName: 'productsByOwner',
args: { ownerId: user.id },
context,
info,
});
},
},
},
},
});
}
async function startGateway() {
const schema = await buildGatewaySchema();
const server = new ApolloServer({
schema,
context: ({ req }) => ({
headers: req.headers,
authToken: req.headers.authorization,
}),
});
const { url } = await server.listen(4000);
console.log(`Gateway hazir: ${url}`);
}
startGateway().catch(console.error);
EOF
REST API’leri Schema Stitching’e Dahil Etmek
Gerçek geçiş senaryolarında elinizde hâlâ REST API’ler bulunuyor. @graphql-tools/wrap ile REST’i de şemaya dahil edebilirsiniz:
cat > src/rest-wrapper.ts << 'EOF'
import { makeExecutableSchema } from '@graphql-tools/schema';
import { wrapSchema, TransformObjectFields } from '@graphql-tools/wrap';
import fetch from 'node-fetch';
import { print, GraphQLSchema } from 'graphql';
// REST API'yi simüle eden bir order servisi
// Gerçekte http://siparis-api.firma.internal gibi bir adres olurdu
const ORDER_API_BASE = 'http://localhost:5000/api';
// REST endpoint'leri için custom executor
const restExecutor = async ({ document, variables }: any) => {
const query = print(document);
// GraphQL sorgusunu parse edip REST çağrısına dönüştürme mantığı
if (query.includes('order(')) {
const id = variables?.id;
const response = await fetch(`${ORDER_API_BASE}/orders/${id}`);
const order = await response.json();
return { data: { order } };
}
if (query.includes('ordersByUser')) {
const userId = variables?.userId;
const response = await fetch(`${ORDER_API_BASE}/orders?userId=${userId}`);
const orders = await response.json();
return { data: { ordersByUser: orders } };
}
return { data: {} };
};
// REST API'nin GraphQL şemasını tanımla
export const orderTypeDefs = `
type Order {
id: ID!
userId: ID!
productId: ID!
quantity: Int!
status: String!
totalPrice: Float!
createdAt: String!
}
type Query {
order(id: ID!): Order
ordersByUser(userId: ID!): [Order!]!
}
`;
export function createOrderSubschema() {
const schema = makeExecutableSchema({
typeDefs: orderTypeDefs,
resolvers: {
Query: {
// Resolver'lar executor tarafından yönetilecek
},
},
});
return {
schema,
executor: restExecutor,
merge: {
Order: {
fieldName: 'order',
selectionSet: '{ id }',
args: ({ id }: { id: string }) => ({ id }),
},
},
};
}
EOF
Monitoring ve Hata Yönetimi
Production ortamında schema stitching kullanıyorsanız, servislerden biri çökerse ne olur? Bunu yönetmek kritik:
cat > src/resilient-gateway.ts << 'EOF'
import { stitchSchemas } from '@graphql-tools/stitch';
import { schemaFromExecutor, wrapSchema } from '@graphql-tools/wrap';
import { buildHTTPExecutor } from '@graphql-tools/executor-http';
import { makeExecutableSchema } from '@graphql-tools/schema';
interface ServiceConfig {
name: string;
endpoint: string;
timeout: number;
retries: number;
}
const services: ServiceConfig[] = [
{ name: 'user-service', endpoint: 'http://localhost:4001/graphql', timeout: 5000, retries: 3 },
{ name: 'product-service', endpoint: 'http://localhost:4002/graphql', timeout: 5000, retries: 3 },
];
// Timeout ve retry mekanizması olan executor
function createResilientExecutor(config: ServiceConfig) {
const baseExecutor = buildHTTPExecutor({
endpoint: config.endpoint,
timeout: config.timeout,
headers: {
'Content-Type': 'application/json',
'X-Service-Name': 'graphql-gateway',
},
});
return async (args: any) => {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= config.retries; attempt++) {
try {
const result = await baseExecutor(args);
return result;
} catch (error: any) {
lastError = error;
console.error(`[${config.name}] Deneme ${attempt}/${config.retries} basarisiz:`, error.message);
if (attempt < config.retries) {
// Exponential backoff: 1s, 2s, 4s
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt - 1) * 1000));
}
}
}
// Tum denemeler basarisiz, fallback null donut
console.error(`[${config.name}] Servis erisimi tamamen basarisiz`);
return { data: null, errors: [{ message: `${config.name} servisi suan erisilebilir degil` }] };
};
}
// Servis saglik kontrolu
async function checkServiceHealth(endpoint: string): Promise<boolean> {
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: '{ __typename }' }),
signal: AbortSignal.timeout(2000),
});
return response.ok;
} catch {
return false;
}
}
export async function buildResilientSchema() {
const subschemas = [];
for (const service of services) {
const isHealthy = await checkServiceHealth(service.endpoint);
if (!isHealthy) {
console.warn(`[UYARI] ${service.name} sagliksiz, bos sema kullaniliyor`);
// Servis down olsa bile gateway ayakta kalsin
continue;
}
const executor = createResilientExecutor(service);
const schema = await schemaFromExecutor(executor);
subschemas.push({ schema, executor });
console.log(`[OK] ${service.name} basariyla yuklendi`);
}
if (subschemas.length === 0) {
throw new Error('Hicbir servis erisebilir degil!');
}
return stitchSchemas({ subschemas });
}
EOF
Performans Optimizasyonu: DataLoader Entegrasyonu
N+1 problemi schema stitching’de gerçek bir tehdit. Bir kullanıcının tüm ürünlerini listelerken her ürün için ayrı ayrı kullanıcı sorgusu atmak istemezsiniz:
cat > src/dataloader-setup.ts << 'EOF'
import DataLoader from 'dataloader';
import fetch from 'node-fetch';
// Batch user fetch - tek seferde birden fazla kullanıcı çek
export function createUserLoader() {
return new DataLoader(async (userIds: readonly string[]) => {
console.log(`[DataLoader] ${userIds.length} kullanici batch halinde cekiliyor`);
const response = await fetch('http://localhost:4001/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query BatchUsers($ids: [ID!]!) {
usersByIds(ids: $ids) {
id
username
email
role
}
}
`,
variables: { ids: userIds },
}),
});
const { data } = await response.json() as any;
const userMap = new Map(data.usersByIds.map((u: any) => [u.id, u]));
// DataLoader, ayni sirada sonuc bekler
return userIds.map(id => userMap.get(id) || null);
}, {
// 50ms icerisinde gelen istekleri batch'le
batchScheduleFn: (callback) => setTimeout(callback, 50),
maxBatchSize: 100,
});
}
// Context factory - her request için yeni loader instance'ı
export function createContext() {
return {
loaders: {
user: createUserLoader(),
},
};
}
EOF
Şema Dönüşümleri ve Filtreleme
Bazen alt şemalardan gelen tipleri veya alanları gizlemeniz ya da yeniden adlandırmanız gerekir:
cat > src/schema-transforms.ts << 'EOF'
import {
FilterObjectFields,
RenameTypes,
RenameRootFields,
TransformObjectFields
} from '@graphql-tools/wrap';
// Kullanici servisinden hassas alanlari gizle
export const userServiceTransforms = [
// Sadece belirli alanlari public'e ac
new FilterObjectFields((typeName, fieldName) => {
if (typeName === 'User') {
// password, internalNotes gibi hassas alanlari filtrele
const publicFields = ['id', 'username', 'email', 'role', 'createdAt'];
return publicFields.includes(fieldName);
}
return true;
}),
// Tip isimlerini namespace'le (cakisma olmasin diye)
new RenameTypes((name) => {
const renames: Record<string, string> = {
'User': 'UserProfile',
'UserEdge': 'UserProfileEdge',
};
return renames[name] ?? name;
}),
// Query isimlerini daha anlasilir hale getir
new RenameRootFields((_, fieldName) => {
const renames: Record<string, string> = {
'me': 'currentUser',
'users': 'allUsers',
};
return renames[fieldName] ?? fieldName;
}),
// Alan degerlerini transform et
new TransformObjectFields((typeName, fieldName, fieldNode) => {
// email alanini maskele (admin olmayan kullanicilar icin)
if (typeName === 'User' && fieldName === 'email') {
return {
...fieldNode,
resolve: (parent: any, args: any, context: any) => {
if (context.userRole !== 'admin') {
const [local, domain] = parent.email.split('@');
return `${local[0]}***@${domain}`;
}
return parent.email;
},
};
}
return fieldNode;
}),
];
EOF
Production Deployment Senaryosu
Tüm bu servisleri Docker ile ayağa kaldıralım:
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
user-service:
build:
context: .
dockerfile: Dockerfile.service
command: ts-node src/user-service.ts
ports:
- "4001:4001"
environment:
- NODE_ENV=production
- DB_URL=postgresql://user:pass@postgres:5432/users
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4001/graphql?query={__typename}"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
product-service:
build:
context: .
dockerfile: Dockerfile.service
command: ts-node src/product-service.ts
ports:
- "4002:4002"
environment:
- NODE_ENV=production
- DB_URL=postgresql://user:pass@postgres:5432/products
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4002/graphql?query={__typename}"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
gateway:
build:
context: .
dockerfile: Dockerfile.service
command: ts-node src/gateway.ts
ports:
- "4000:4000"
environment:
- NODE_ENV=production
- USER_SERVICE_URL=http://user-service:4001/graphql
- PRODUCT_SERVICE_URL=http://product-service:4002/graphql
- SCHEMA_POLL_INTERVAL=30000
depends_on:
user-service:
condition: service_healthy
product-service:
condition: service_healthy
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.gateway.rule=Host(`api.firma.com`)"
- "traefik.http.routers.gateway.tls=true"
redis:
image: redis:7-alpine
command: redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru
restart: unless-stopped
networks:
default:
name: graphql-network
EOF
Schema Stitching vs Federation: Ne Zaman Hangisini Seçmeli?
Bu konuda çok sık soru geliyor. Kısa cevap: Apollo Federation daha opinionated ve büyük ekipler için daha uygun. Schema Stitching ise daha esnek.
Duruma göre rehber:
- Schema Stitching tercih edin: Mevcut şemalar üzerinde değişiklik yapma imkanınız yoksa, farklı GraphQL kütüphaneleri kullanıyorsanız (Hasura, Prisma vb.), ekibiniz küçükse veya şemalar arasında ince ayarlı transformlar yapmanız gerekiyorsa.
- Federation tercih edin: Tüm servisler Apollo Server kullanıyorsa, her ekip kendi şemasını bağımsız yönetecekse ve
@key,@extendsgibi directive’lerle çalışmakta problem görmüyorsanız.
- İkisini birlikte: Evet, mümkün. Federation gateway’i olarak
@apollo/gatewaykullanıp remote şemaları stitching ile içeri alabilirsiniz.
Sık karşılaşılan sorunlar ve çözümleri:
- Circular dependency: İki şema birbirini referans alıyorsa,
delegateToSchemayerine lazy resolver kullanın. - Type conflict: Farklı şemalarda aynı isimde farklı tipler varsa
RenameTypestransform’u uygulayın. - Slow schema building: Şema initialization’ını uygulama başlangıcında bir kez yapın, her request’te tekrar yapmayin. Redis’e serialize edip cache’leyebilirsiniz.
- N+1 problemi: DataLoader olmadan batchleme yapamayan resolver’lar çok sayıda upstream istek açar. Her entity için loader yazın.
Sonuç
Schema Stitching, mikroservis geçiş döneminde gerçek anlamda köprü görevi gören bir yaklaşım. Tüm servislerinizi aynı anda GraphQL’e taşımanız gerekmiyor; REST’ten gelen verileri bile wrapper ile şemaya dahil edebiliyorsunuz.
Özetle pratikte dikkat etmeniz gereken başlıca noktalar şunlar:
- Gateway başlangıcında şemaları önbelleğe alın, her request’te remote şema çekmeyin
- Health check mekanizması kurun, bir servis düşünce gateway tamamen çöküyor olmamalı
- DataLoader olmadan üretime gitmeyin, N+1 problemi yüklü ortamlarda ölümcül olabilir
- Transform’ları erken tanımlayın, tip isim çakışmaları development’da değil production’da fark edilirse maliyetli olur
- Schema polling ekleyin, alt servislerdeki şema değişikliklerini gateway otomatik fark etsin
Bu mimariye geçen projelerde genellikle frontend ekibinden ilk geri bildirim şu oluyor: “Artık hangi endpoint’i çağıracağımı düşünmüyorum.” Bu tek cümle bile Schema Stitching’i uygulamaya değer kılıyor.
