GraphQL Mutation ile Veri Ekleme, Güncelleme ve Silme
Bir REST API geliştirirken veri değiştirme işlemleri için POST, PUT, PATCH, DELETE gibi farklı HTTP metodları kullanırsın. GraphQL’de ise bu işlemlerin hepsi tek bir kavram altında toplanır: Mutation. İlk başta biraz garip gelebilir, “neden her şeyi tek noktadan yapalım?” diye düşünebilirsin. Ama pratikte çalışmaya başlayınca bu yaklaşımın ne kadar temiz ve tutarlı olduğunu anlıyorsun. Bu yazıda GraphQL Mutation’ı gerçek dünya senaryolarıyla, hataları ve edge case’leriyle birlikte ele alacağız.
Mutation Nedir ve Query’den Farkı Ne?
GraphQL’de Query veri okuma işlemleri için kullanılırken, Mutation veri değiştirme işlemleri için kullanılır. Teknik olarak her ikisi de aynı HTTP POST isteğiyle çalışır, farkları semantik ve davranışsaldır.
Query’ler paralel çalışabilir, yani birden fazla query aynı anda işlenebilir. Mutation’lar ise sıralı çalışır. Bir mutation tamamlanmadan diğeri başlamaz. Bu önemli bir detay çünkü veri tutarlılığı açısından kritik.
Bir kullanıcı yönetim sistemi düşün. Kullanıcı ekleme, güncelleme, şifre değiştirme ve silme işlemleri yapman gerekiyor. REST’te bunlar farklı endpoint’lere dağılırken, GraphQL’de hepsi mutation altında toplanır.
Şema Üzerinde Mutation Tanımlamak
Mutation’ları önce şemada tanımlaman gerekir. schema.graphql dosyanda ya da SDL (Schema Definition Language) bloğunda Mutation tipini oluşturursun.
# schema.graphql - Temel bir kullanıcı yönetim şeması
type User {
id: ID!
username: String!
email: String!
role: String!
createdAt: String!
isActive: Boolean!
}
type Mutation {
createUser(input: CreateUserInput!): UserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UserPayload!
deleteUser(id: ID!): DeletePayload!
changePassword(id: ID!, oldPassword: String!, newPassword: String!): BooleanPayload!
}
input CreateUserInput {
username: String!
email: String!
password: String!
role: String
}
input UpdateUserInput {
username: String
email: String
role: String
isActive: Boolean
}
type UserPayload {
success: Boolean!
message: String
user: User
}
type DeletePayload {
success: Boolean!
message: String
deletedId: ID
}
type BooleanPayload {
success: Boolean!
message: String
}
Burada dikkat etmeni istediğim birkaç önemli nokta var. Input type kullanımı kritik. Direkt olarak scalar parametreler geçmek yerine input tipi tanımlamak, hem şemayı temiz tutar hem de ileride yeni alanlar eklemeyi kolaylaştırır. Ayrıca her mutation bir payload tipi döndürüyor. Bu pattern, hata yönetimini çok daha sağlıklı hale getirir.
İlk Mutation: Kullanıcı Ekleme
Şimdi bu mutation’ı nasıl çağıracağına bakalım. GraphQL Playground veya herhangi bir GraphQL client’ı üzerinden şunu yazabilirsin:
# Yeni kullanıcı oluşturma mutation'ı
mutation CreateNewUser {
createUser(input: {
username: "ahmet.yilmaz"
email: "[email protected]"
password: "Guclu@Sifre123"
role: "developer"
}) {
success
message
user {
id
username
email
role
createdAt
}
}
}
Burada mutation keyword’ünü kullandığımıza dikkat et. Sonrasında gelen CreateNewUser opsiyonel bir operation name. Geliştirme sürecinde bu isimleri mutlaka koy, debug ve loglama sırasında hayat kurtarır.
Resolver tarafında bu mutation’ın nasıl işlendiğine bakalım. Node.js/Apollo Server örneği üzerinden gidiyoruz:
# resolvers.js - Mutation resolver'ları
const resolvers = {
Mutation: {
createUser: async (_, { input }, { db, currentUser }) => {
// Yetki kontrolü
if (!currentUser || currentUser.role !== 'admin') {
return {
success: false,
message: 'Bu işlem için yetkiniz yok',
user: null
};
}
// Email benzersizlik kontrolü
const existingUser = await db.users.findOne({
email: input.email
});
if (existingUser) {
return {
success: false,
message: 'Bu email adresi zaten kullanımda',
user: null
};
}
// Şifreyi hashle
const hashedPassword = await bcrypt.hash(input.password, 12);
// Kullanıcıyı oluştur
const newUser = await db.users.create({
...input,
password: hashedPassword,
role: input.role || 'viewer',
isActive: true,
createdAt: new Date().toISOString()
});
return {
success: true,
message: 'Kullanıcı başarıyla oluşturuldu',
user: newUser
};
}
}
};
Context parametresine dikkat et. db ve currentUser gibi değerleri context üzerinden alıyoruz. Bu GraphQL’in güzel taraflarından biri, her resolver aynı context’e erişebiliyor.
Güncelleme Mutation’ı
Veri güncellemek, eklemekten biraz daha nüanslı bir süreç. Neyin değiştiğini, neyin değişmediğini yönetmen gerekiyor. Özellikle partial update (kısmi güncelleme) senaryoları dikkat ister.
# Kullanıcı güncelleme mutation'ı - sadece değişen alanlar gönderiliyor
mutation UpdateExistingUser {
updateUser(
id: "user_123"
input: {
email: "[email protected]"
isActive: false
}
) {
success
message
user {
id
username
email
role
isActive
}
}
}
UpdateUserInput içindeki tüm alanlar opsiyonel (sondaki ! işareti yok). Bu sayede sadece değiştirmek istediğin alanları gönderebilirsin. Resolver tarafında bunu şöyle ele alırsın:
# Güncelleme resolver'ı - null check ve merge işlemi
updateUser: async (_, { id, input }, { db, currentUser }) => {
// Kullanıcının kendisi veya admin güncelleyebilir
if (currentUser.id !== id && currentUser.role !== 'admin') {
return {
success: false,
message: 'Başka bir kullanıcıyı güncelleyemezsiniz',
user: null
};
}
// Mevcut kullanıcıyı bul
const existingUser = await db.users.findById(id);
if (!existingUser) {
return {
success: false,
message: 'Kullanıcı bulunamadı',
user: null
};
}
// Sadece gönderilen alanları güncelle
// undefined olan alanları filtrele
const updateData = Object.fromEntries(
Object.entries(input).filter(([_, v]) => v !== undefined && v !== null)
);
if (Object.keys(updateData).length === 0) {
return {
success: false,
message: 'Güncellenecek alan bulunamadı',
user: existingUser
};
}
const updatedUser = await db.users.findByIdAndUpdate(
id,
{ $set: updateData },
{ new: true }
);
return {
success: true,
message: 'Kullanıcı başarıyla güncellendi',
user: updatedUser
};
}
Silme Mutation’ı ve Soft Delete Pattern’i
Silme işlemleri, production ortamlarında genellikle soft delete olarak yapılır. Kaydı fiziksel olarak silmek yerine isDeleted: true veya deletedAt timestamp’i set edersin. Bu audit trail ve veri kurtarma açısından çok değerli.
# Silme mutation'ı çağrısı
mutation RemoveUser {
deleteUser(id: "user_456") {
success
message
deletedId
}
}
# Soft delete + hard delete seçeneği olan resolver
deleteUser: async (_, { id, hardDelete = false }, { db, currentUser }) => {
// Sadece adminler silebilir
if (currentUser.role !== 'admin') {
return {
success: false,
message: 'Kullanıcı silme yetkisi yok',
deletedId: null
};
}
const user = await db.users.findById(id);
if (!user) {
return {
success: false,
message: 'Kullanıcı bulunamadı',
deletedId: null
};
}
// Kendi hesabını silemezsin
if (user.id === currentUser.id) {
return {
success: false,
message: 'Kendi hesabınızı silemezsiniz',
deletedId: null
};
}
if (hardDelete) {
// Fiziksel silme - sadece süper admin yapabilir
await db.users.findByIdAndDelete(id);
} else {
// Soft delete - varsayılan
await db.users.findByIdAndUpdate(id, {
$set: {
isActive: false,
deletedAt: new Date().toISOString(),
deletedBy: currentUser.id
}
});
}
return {
success: true,
message: hardDelete ? 'Kullanıcı kalıcı olarak silindi' : 'Kullanıcı devre dışı bırakıldı',
deletedId: id
};
}
Toplu İşlemler: Batch Mutation
Gerçek dünyada tek tek işlem yapmak her zaman yeterli olmaz. Örneğin bir deployment sırasında yüzlerce kullanıcının rolünü değiştirmek veya bir sunucu listesini güncellemek gibi. Batch mutation’lar bunun için var:
# Çoklu kayıt güncelleme için şema tanımı
type Mutation {
bulkUpdateUserRoles(updates: [UserRoleUpdate!]!): BulkOperationPayload!
bulkDeactivateUsers(userIds: [ID!]!): BulkOperationPayload!
}
input UserRoleUpdate {
userId: ID!
newRole: String!
}
type BulkOperationPayload {
success: Boolean!
processedCount: Int!
failedCount: Int!
errors: [BulkError]
}
type BulkError {
id: ID
message: String
}
# Batch mutation kullanımı
mutation BulkRoleUpdate {
bulkUpdateUserRoles(updates: [
{ userId: "user_001", newRole: "developer" }
{ userId: "user_002", newRole: "admin" }
{ userId: "user_003", newRole: "viewer" }
]) {
success
processedCount
failedCount
errors {
id
message
}
}
}
Bu pattern’de her kaydın işlenip işlenmediğini ayrı ayrı takip edebiliyorsun. Tüm işlemin başarısız olması yerine, hangi kayıtların sorun yaşadığını görebiliyorsun.
Variables Kullanımı: Production’da Doğru Yol
Mutation yazarken değerleri direkt sorgu içine gömmek (hardcode) sadece test için kabul edilebilir. Production uygulamalarda variables kullanmak zorundasın. Bu hem güvenlik hem de kod tekrarını önleme açısından kritik.
# Variables ile mutation kullanımı
# Query kısmı:
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
success
message
user {
id
username
email
}
}
}
# Variables kısmı (ayrı JSON olarak gönderilir):
# {
# "input": {
# "username": "mehmet.kaya",
# "email": "[email protected]",
# "password": "GucluSifre!456",
# "role": "developer"
# }
# }
Frontend tarafında fetch ile nasıl kullanıldığına bakalım:
# JavaScript ile GraphQL mutation gönderme
async function createUser(userData) {
const mutation = `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
success
message
user {
id
username
email
role
}
}
}
`;
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
query: mutation,
variables: { input: userData }
})
});
const result = await response.json();
if (result.errors) {
throw new Error(result.errors[0].message);
}
return result.data.createUser;
}
// Kullanım
createUser({
username: "ali.veli",
email: "[email protected]",
password: "Sifre@789",
role: "developer"
}).then(payload => {
if (payload.success) {
console.log('Kullanici olusturuldu:', payload.user.id);
}
});
Hata Yönetimi Stratejileri
GraphQL hata yönetimi iki katmanda gerçekleşir. Birincisi GraphQL seviyesindeki hatalar (bu errors array’inde döner), ikincisi iş mantığı hataları (bu da payload içinde success: false olarak döner). İkisini karıştırmamak önemli.
GraphQL seviyesindeki hatalar genellikle şunları içerir: authentication hatası, authorization hatası, validation hatası, ve resolver içinde fırlatılan beklenmedik hatalar.
İş mantığı hataları ise kullanıcıya gösterilebilir mesajlardır. “Email zaten kullanımda”, “Kullanıcı bulunamadı” gibi. Bunları payload’a göm, GraphQL error olarak fırlatma.
Resolver’larda exception handling için şunu öneriyorum:
# Güvenli resolver wrapper - beklenmedik hataları yakalar
const safeResolver = (resolver) => async (...args) => {
try {
return await resolver(...args);
} catch (error) {
// Loglama yap
console.error('Resolver hatasi:', error.message, {
resolver: resolver.name,
timestamp: new Date().toISOString()
});
// Kullanıcıya genel hata mesajı döndür
// Detayları asla expose etme
return {
success: false,
message: 'Bir hata oluştu, lütfen tekrar deneyin',
user: null
};
}
};
// Kullanımı
const resolvers = {
Mutation: {
createUser: safeResolver(async (_, { input }, ctx) => {
// resolver kodu buraya
}),
updateUser: safeResolver(async (_, { id, input }, ctx) => {
// resolver kodu buraya
})
}
};
Idempotency ve Duplicate Prevention
Ağ sorunları nedeniyle aynı mutation birden fazla kez çalışabilir. Özellikle ödeme işlemleri veya kritik kayıt oluşturma senaryolarında idempotency key kullanmak şart.
Bunun için input type’ına bir idempotencyKey alanı ekleyebilirsin:
- idempotencyKey: Client tarafından üretilen benzersiz bir UUID
- Her istek için aynı key gönderilirse, aynı sonuç döndürülür
- Key’ler genellikle 24-48 saat önbellekte tutulur
- Redis gibi bir cache store ile implement edilir
Bu yaklaşım özellikle e-ticaret, fintech ve SaaS uygulamalarında mutation güvenliğini büyük ölçüde artırır.
Performance: N+1 Problemi ve DataLoader
Mutation’lar için çok kritik olmasa da, mutation’ın döndürdüğü payload içinde ilişkili veri çekerken N+1 problemine dikkat et. Örneğin createUser mutation’ı kullanıcıyı oluşturduktan sonra kullanıcının departman bilgisini de döndürüyorsa, her kullanıcı için ayrı bir DB sorgusu çalışır.
DataLoader kullanımı bu problemi çözer. Batch loading ile birden fazla kullanıcının departman bilgisini tek sorguda çekebilirsin. Apollo Server ile DataLoader entegrasyonu context üzerinden yapılır ve mutation resolver’larında da aynı şekilde kullanılır.
Sonuç
GraphQL Mutation’larını doğru tasarlamak, API’nin uzun vadeli sürdürülebilirliğini doğrudan etkiler. Bu yazıda ele aldığımız başlıca pratikler şunlar:
- Input type kullanımı: Mutation parametrelerini her zaman input type ile wrap et, ileride genişletmek çok kolaylaşır
- Payload pattern: Mutation’lardan her zaman
success,messageve ilgili entity’yi içeren payload döndür - Soft delete: Production’da fiziksel silme yerine soft delete tercih et
- Variables kullanımı: Hardcode değer yerine her zaman variables kullan
- Hata yönetimi iki katmanı: GraphQL hatalarını ve iş mantığı hatalarını birbirinden ayır
- Batch mutation: Toplu işlemler için ayrı mutation’lar tanımla ve her kaydın durumunu ayrı raporla
- Idempotency: Kritik işlemler için idempotency key mekanizması ekle
Mutation’lar GraphQL’in en güçlü taraflarından birini oluşturuyor. REST’in dağınık endpoint yapısının aksine, tüm veri değişikliklerini tek bir yerden yönetmek hem dokümantasyonu hem de client entegrasyonunu ciddi ölçüde basitleştiriyor. Şema tasarımına başlarken mutation’ları da query’ler kadar özenle planlarsan, ileride büyüyen bir sistemde çok daha az teknik borçla karşılaşırsın.
