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, message ve 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.

Bir yanıt yazın

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