Serverless Mimari ile GraphQL API Oluşturma
Bulut dünyasında sunucu yönetiminden kurtulmak ve sadece iş mantığına odaklanmak artık bir lüks değil, rekabetçi kalabilmek için neredeyse zorunluluk haline geldi. Serverless mimarisi bu ihtiyacı karşılarken, GraphQL de REST’in getirdiği over-fetching ve under-fetching problemlerine modern bir çözüm sunuyor. Bu ikisini birleştirdiğinde hem altyapı yükünden kurtulan hem de esnek, performanslı bir API’ye sahip olan bir sistem elde ediyorsun. Bu yazıda AWS Lambda üzerinde GraphQL API nasıl kurulur, production ortamına nasıl taşınır ve karşılaşacağın sorunları nasıl çözersin, hepsini gerçek dünya senaryolarıyla ele alacağız.
Neden Serverless + GraphQL?
REST API’lerin klasik sorunlarından birini düşün: frontend ekibi bir sayfa için 5 farklı endpoint’e istek atmak zorunda kalıyor, ya da tek endpoint’ten ihtiyacı olmayan 50 alan geliyor. Bu hem bant genişliği israfı hem de frontend geliştirme sürecini yavaşlatan bir durum.
GraphQL bu sorunu şu şekilde çözüyor: client tam olarak neye ihtiyaç duyduğunu belirtiyor. Fazla veri yok, eksik veri yok. Serverless mimarisi de burada devreye giriyor çünkü GraphQL’in tek endpoint yapısı Lambda gibi fonksiyonlarla son derece uyumlu. Tek bir /graphql endpoint’ini bir Lambda fonksiyonuna yönlendiriyorsun, gerisi otomatik scale oluyor.
Gerçek dünya senaryosu olarak şunu düşün: E-ticaret platformun var, mobil uygulaman ürün listesi için kategori bilgisi, fiyat ve stok durumu istiyor. Admin panelin ise aynı ürün için tedarikçi bilgisi, maliyet ve kâr marjını istiyor. REST ile bu iki farklı endpoint ya da aşırı şişirilmiş bir response demek. GraphQL ile her client kendi ihtiyacına göre sorgu yazıyor.
Proje Yapısını Kurmak
Önce geliştirme ortamını hazırlayalım. Node.js tabanlı ilerleyeceğiz, AWS SAM kullanarak local geliştirme ve deployment sürecini yöneteceğiz.
# Gerekli araçları kur
npm install -g @aws/aws-sam-cli
npm install -g serverless
# Proje dizinini oluştur
mkdir graphql-serverless-api
cd graphql-serverless-api
npm init -y
# Temel bağımlılıkları yükle
npm install apollo-server-lambda graphql @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
npm install --save-dev @types/aws-lambda esbuild serverless-esbuild
Proje dizin yapısı şu şekilde olmalı:
- src/: Lambda fonksiyon kodları
- src/schema/: GraphQL şema tanımlamaları
- src/resolvers/: Resolver fonksiyonları
- src/models/: DynamoDB veri modelleri
- src/utils/: Yardımcı fonksiyonlar
- template.yaml: SAM template dosyası
GraphQL Şemasını Tanımlamak
Şema tanımı GraphQL’in kalbidir. Burada yapacağın tasarım kararları ileride seni ya kurtarır ya da başının belasına dönüşür. Şema tasarımında domain-driven düşünmek önemli.
// src/schema/typeDefs.js
const { gql } = require('apollo-server-lambda');
const typeDefs = gql`
type Product {
id: ID!
name: String!
description: String
price: Float!
stock: Int!
category: Category!
createdAt: String!
updatedAt: String!
}
type Category {
id: ID!
name: String!
slug: String!
products: [Product!]!
}
type Order {
id: ID!
userId: ID!
items: [OrderItem!]!
totalAmount: Float!
status: OrderStatus!
createdAt: String!
}
type OrderItem {
product: Product!
quantity: Int!
unitPrice: Float!
}
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
type Query {
product(id: ID!): Product
products(
categoryId: ID
minPrice: Float
maxPrice: Float
limit: Int
nextToken: String
): ProductConnection!
category(id: ID!): Category
categories: [Category!]!
order(id: ID!): Order
myOrders(userId: ID!, limit: Int): [Order!]!
}
type ProductConnection {
items: [Product!]!
nextToken: String
}
type Mutation {
createProduct(input: CreateProductInput!): Product!
updateProduct(id: ID!, input: UpdateProductInput!): Product!
deleteProduct(id: ID!): Boolean!
createOrder(input: CreateOrderInput!): Order!
updateOrderStatus(id: ID!, status: OrderStatus!): Order!
}
input CreateProductInput {
name: String!
description: String
price: Float!
stock: Int!
categoryId: ID!
}
input UpdateProductInput {
name: String
description: String
price: Float
stock: Int
}
input CreateOrderInput {
userId: ID!
items: [OrderItemInput!]!
}
input OrderItemInput {
productId: ID!
quantity: Int!
}
`;
module.exports = typeDefs;
Lambda Handler ve Apollo Server Entegrasyonu
Ana handler dosyası, Lambda ile Apollo Server’ı birbirine bağlayan noktadır. Production ortamında handler’ın soğuk başlatma süresini minimize etmek kritik önem taşıyor.
// src/handler.js
const { ApolloServer } = require('apollo-server-lambda');
const typeDefs = require('./schema/typeDefs');
const resolvers = require('./resolvers');
const { createDynamoDBClient } = require('./utils/dynamodb');
const { verifyToken } = require('./utils/auth');
// DynamoDB client'ı handler dışında oluşturuyoruz
// Bu şekilde warm start'larda yeniden oluşturulmaz
const dynamoClient = createDynamoDBClient();
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ event, context }) => {
// Her request için context oluştur
let user = null;
const authHeader = event.headers?.Authorization ||
event.headers?.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7);
try {
user = await verifyToken(token);
} catch (err) {
console.warn('Token doğrulama hatası:', err.message);
}
}
return {
user,
dynamoClient,
requestId: context.awsRequestId,
};
},
formatError: (error) => {
// Hassas bilgileri client'a sızdırma
console.error('GraphQL Hatası:', {
message: error.message,
path: error.path,
extensions: error.extensions,
});
if (error.message.startsWith('Internal server error')) {
return new Error('Bir hata oluştu, lütfen tekrar deneyin');
}
return error;
},
introspection: process.env.STAGE !== 'production',
playground: process.env.STAGE !== 'production',
});
exports.graphqlHandler = server.createHandler({
cors: {
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
credentials: true,
},
});
Resolver Yapısını Organize Etmek
Resolver’lar büyüdükçe tek dosyada tutmak kabus haline geliyor. Domain bazlı ayırım burada seni kurtarır.
// src/resolvers/productResolvers.js
const { UserInputError, ForbiddenError } = require('apollo-server-lambda');
const productResolvers = {
Query: {
product: async (_, { id }, { dynamoClient }) => {
const result = await dynamoClient.get({
TableName: process.env.PRODUCTS_TABLE,
Key: { id },
});
if (!result.Item) {
throw new UserInputError(`Ürün bulunamadı: ${id}`);
}
return result.Item;
},
products: async (_, { categoryId, minPrice, maxPrice, limit = 20, nextToken }, { dynamoClient }) => {
let filterExpression = [];
let expressionValues = {};
if (categoryId) {
filterExpression.push('categoryId = :categoryId');
expressionValues[':categoryId'] = categoryId;
}
if (minPrice !== undefined) {
filterExpression.push('price >= :minPrice');
expressionValues[':minPrice'] = minPrice;
}
if (maxPrice !== undefined) {
filterExpression.push('price <= :maxPrice');
expressionValues[':maxPrice'] = maxPrice;
}
const params = {
TableName: process.env.PRODUCTS_TABLE,
Limit: limit,
ExclusiveStartKey: nextToken
? JSON.parse(Buffer.from(nextToken, 'base64').toString())
: undefined,
};
if (filterExpression.length > 0) {
params.FilterExpression = filterExpression.join(' AND ');
params.ExpressionAttributeValues = expressionValues;
}
const result = await dynamoClient.scan(params);
return {
items: result.Items || [],
nextToken: result.LastEvaluatedKey
? Buffer.from(JSON.stringify(result.LastEvaluatedKey)).toString('base64')
: null,
};
},
},
Mutation: {
createProduct: async (_, { input }, { user, dynamoClient }) => {
if (!user || user.role !== 'ADMIN') {
throw new ForbiddenError('Bu işlem için yetkiniz yok');
}
const product = {
id: require('crypto').randomUUID(),
...input,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
await dynamoClient.put({
TableName: process.env.PRODUCTS_TABLE,
Item: product,
ConditionExpression: 'attribute_not_exists(id)',
});
return product;
},
updateProduct: async (_, { id, input }, { user, dynamoClient }) => {
if (!user || user.role !== 'ADMIN') {
throw new ForbiddenError('Bu işlem için yetkiniz yok');
}
const updateFields = Object.entries(input)
.filter(([_, v]) => v !== undefined);
if (updateFields.length === 0) {
throw new UserInputError('Güncelleme için en az bir alan gerekli');
}
const updateExpression = 'SET ' + updateFields
.map(([key]) => `#${key} = :${key}`)
.join(', ') + ', updatedAt = :updatedAt';
const expressionNames = Object.fromEntries(
updateFields.map(([key]) => [`#${key}`, key])
);
const expressionValues = {
...Object.fromEntries(updateFields.map(([key, val]) => [`:${key}`, val])),
':updatedAt': new Date().toISOString(),
};
const result = await dynamoClient.update({
TableName: process.env.PRODUCTS_TABLE,
Key: { id },
UpdateExpression: updateExpression,
ExpressionAttributeNames: expressionNames,
ExpressionAttributeValues: expressionValues,
ConditionExpression: 'attribute_exists(id)',
ReturnValues: 'ALL_NEW',
});
return result.Attributes;
},
},
Product: {
category: async ({ categoryId }, _, { dynamoClient }) => {
const result = await dynamoClient.get({
TableName: process.env.CATEGORIES_TABLE,
Key: { id: categoryId },
});
return result.Item;
},
},
};
module.exports = productResolvers;
N+1 Problemini DataLoader ile Çözmek
Serverless GraphQL’in en sinir bozucu taraflarından biri N+1 problemi. Her ürün için ayrı bir kategori sorgusu atıyorsan, 100 ürünlük bir listede 100 DynamoDB çağrısı yapıyorsun. DataLoader bu sorunu batch’leyerek çözüyor.
// src/utils/dataLoaders.js
const DataLoader = require('dataloader');
const createCategoryLoader = (dynamoClient) => {
return new DataLoader(
async (categoryIds) => {
// Tekrar eden id'leri temizle
const uniqueIds = [...new Set(categoryIds)];
// Batch get ile tek seferde çek
const result = await dynamoClient.batchGet({
RequestItems: {
[process.env.CATEGORIES_TABLE]: {
Keys: uniqueIds.map((id) => ({ id })),
},
},
});
const categories = result.Responses[process.env.CATEGORIES_TABLE];
const categoryMap = Object.fromEntries(
categories.map((cat) => [cat.id, cat])
);
// DataLoader sıralamayı korumamızı bekler
return categoryIds.map((id) => categoryMap[id] || null);
},
{
// Aynı request içinde cache'le
cache: true,
maxBatchSize: 100,
}
);
};
// Handler'da context'e ekle
// Her request için yeni loader oluşturmak önemli
const createLoaders = (dynamoClient) => ({
category: createCategoryLoader(dynamoClient),
});
module.exports = { createLoaders };
// handler.js context kısmını güncelle
const { createLoaders } = require('./utils/dataLoaders');
context: async ({ event, context }) => {
// ...mevcut kod...
return {
user,
dynamoClient,
loaders: createLoaders(dynamoClient), // Her request için taze loader
requestId: context.awsRequestId,
};
},
// Resolver'da kullan
Product: {
category: async ({ categoryId }, _, { loaders }) => {
return loaders.category.load(categoryId);
},
},
SAM Template ile Deployment
# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: GraphQL Serverless API
Globals:
Function:
Timeout: 30
MemorySize: 512
Runtime: nodejs18.x
Environment:
Variables:
PRODUCTS_TABLE: !Ref ProductsTable
CATEGORIES_TABLE: !Ref CategoriesTable
ORDERS_TABLE: !Ref OrdersTable
STAGE: !Ref Stage
Parameters:
Stage:
Type: String
Default: dev
AllowedValues: [dev, staging, production]
Resources:
GraphQLFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/handler.graphqlHandler
Description: Ana GraphQL Lambda Fonksiyonu
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ProductsTable
- DynamoDBCrudPolicy:
TableName: !Ref CategoriesTable
- DynamoDBCrudPolicy:
TableName: !Ref OrdersTable
Events:
GraphQL:
Type: Api
Properties:
Path: /graphql
Method: ANY
RestApiId: !Ref ApiGateway
ApiGateway:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Stage
Cors:
AllowMethods: "'GET,POST,OPTIONS'"
AllowHeaders: "'Content-Type,Authorization'"
AllowOrigin: "'*'"
Auth:
DefaultAuthorizer: NONE
GatewayResponses:
DEFAULT_4XX:
ResponseParameters:
Headers:
Access-Control-Allow-Origin: "'*'"
ProductsTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
- AttributeName: categoryId
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
GlobalSecondaryIndexes:
- IndexName: CategoryIndex
KeySchema:
- AttributeName: categoryId
KeyType: HASH
Projection:
ProjectionType: ALL
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: true
Outputs:
GraphQLEndpoint:
Description: GraphQL API Endpoint
Value: !Sub 'https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/graphql'
# Build ve deploy
sam build --use-container
sam deploy --guided --stack-name graphql-api-production
--parameter-overrides Stage=production
--capabilities CAPABILITY_IAM
# Local test
sam local start-api --env-vars env.json
# Log takibi
sam logs -n GraphQLFunction --stack-name graphql-api-production --tail
Production’da Dikkat Edilmesi Gerekenler
Cold Start Optimizasyonu
Lambda’nın en büyük sorunu soğuk başlatma süreleri. GraphQL şeması büyüdükçe bu süre uzuyor. Birkaç pratik önlem:
- Provisioned Concurrency kullan: Kritik Lambda’lar için belirli sayıda instance’ı sıcak tut. Maliyeti var ama latency garantisi sağlıyor.
- Bundle size’ı küçük tut: esbuild ile tree-shaking yaparak gereksiz modülleri dışarıda bırak.
apollo-server-lambdayerine@as-integrations/aws-lambdadaha hafif bir alternatif. - Lazy loading: Kullanılmayan modülleri fonksiyonun başında değil, gerçekten ihtiyaç duyulduğunda yükle.
Hata Yönetimi ve Observability
// src/utils/logger.js
const logger = {
info: (message, meta = {}) => {
console.log(JSON.stringify({
level: 'INFO',
message,
timestamp: new Date().toISOString(),
...meta,
}));
},
error: (message, error, meta = {}) => {
console.error(JSON.stringify({
level: 'ERROR',
message,
error: {
name: error?.name,
message: error?.message,
stack: process.env.STAGE !== 'production' ? error?.stack : undefined,
},
timestamp: new Date().toISOString(),
...meta,
}));
},
};
module.exports = logger;
CloudWatch Logs Insights ile query atmak için structured logging şart. JSON formatında log basmak bu nedenle standart haline gelmeli.
Rate Limiting ve DDoS Koruması
GraphQL’in tek endpoint yapısı API Gateway’in standart rate limiting’ini devre dışı bırakmaz ama complexity limiting eklemelisin. Kötü niyetli bir kullanıcı iç içe geçmiş devasa sorgularla sistemini çökertebilir.
npm install graphql-query-complexity
Apollo Server plugin olarak ekleyerek her sorgunun karmaşıklığını hesaplayabilir ve belirli bir eşiği aşan sorguları reddedebilirsin. Sınır değeri olarak genellikle 1000 iyi bir başlangıç noktası.
Gerçek Dünya Senaryosu: E-ticaret API Testi
Sistemi ayağa kaldırdıktan sonra şu sorgu ile test edebilirsin:
# API endpoint'ini test et
curl -X POST https://your-api-id.execute-api.eu-west-1.amazonaws.com/production/graphql
-H "Content-Type: application/json"
-H "Authorization: Bearer your-jwt-token"
-d '{
"query": "query GetProducts($categoryId: ID, $limit: Int) { products(categoryId: $categoryId, limit: $limit) { items { id name price stock category { name slug } } nextToken } }",
"variables": {
"categoryId": "elektronik",
"limit": 10
}
}'
# Mutation testi
curl -X POST https://your-api-id.execute-api.eu-west-1.amazonaws.com/production/graphql
-H "Content-Type: application/json"
-H "Authorization: Bearer admin-jwt-token"
-d '{
"query": "mutation CreateProduct($input: CreateProductInput!) { createProduct(input: $input) { id name price createdAt } }",
"variables": {
"input": {
"name": "Test Ürün",
"price": 299.99,
"stock": 50,
"categoryId": "elektronik"
}
}
}'
Maliyet Optimizasyonu
Serverless’ın güzelliği maliyet modelinde yatıyor ama yanlış kullanırsan sürprizlerle karşılaşabilirsin. DynamoDB için On-Demand yerine iş yükünü tahmin edebildiğinde Provisioned kapasiteye geç. Lambda için memory ayarını doğru yapmak önemli: 512MB yeterli olduğunda 1024MB tahsis etmek hem yavaşlatmaz hem de maliyeti ikiye katlar değil ama optimize de etmez. Lambda Power Tuning aracını kullanarak optimum memory değerini bul.
Sonuç
Serverless + GraphQL kombinasyonu doğru kurulduğunda gerçekten güçlü bir altyapı sunuyor. Altyapı yönetimiyle vakit kaybetmek yerine iş mantığına odaklanabiliyorsun, scale otomatik geliyor ve maliyet kullanım bazlı oluyor.
Ancak bu yapının da kendine özgü zorlukları var. Soğuk başlatmalar, N+1 problemleri, şema tasarımındaki kararların ileride nasıl etki edeceği ve debugging zorluğu bunların başında geliyor. Bu yazıda ele aldığımız DataLoader, structured logging, complexity limiting ve bundle optimizasyonu gibi pratikler bu sorunları büyük ölçüde gideriyor.
Başlangıçta küçük tut: tek bir domain’den başla, şemayı iyi tasarla, observability’yi baştan kur. Sonra her şeyi yerleştirilmiş ama ölçeklenemeyen bir monolith’ten dönüştürmeye çalışmak yerine temelden sağlam bir sistem inşa etmiş olursun. Serverless ile GraphQL öğrenme eğrisi var ama yerleştiğinde altyapıyı düşünmeden API geliştirmenin keyfini çıkarıyorsun.
