GraphQL Tip Sistemi ve Şema Tasarımı Temelleri

Bir API tasarlarken en çok zaman harcanan konu genellikle “hangi veriyi nasıl sunacağız” sorusudur. REST API’lerde bu soruyu endpoint’leri, HTTP metotlarını ve response body’lerini tanımlayarak cevaplarsınız. GraphQL’de ise bu sorunun cevabı şema tasarımından geçiyor. Şema, GraphQL’in kalbi ve aynı zamanda en güçlü tarafı. Tip sistemi sayesinde hem istemci hem sunucu tarafı aynı dili konuşuyor, otomatik dokümantasyon ortaya çıkıyor ve tip güvenliği sağlanıyor. Bu yazıda GraphQL tip sistemini ve şema tasarımını gerçek dünya senaryolarıyla ele alacağız.

GraphQL Tip Sistemine Giriş

GraphQL, güçlü tipli (strongly typed) bir sorgu dilidir. Yani her alanın, her argümanın ve her dönüş değerinin bir tipi vardır. Bu, sadece teorik bir güzellik değil, pratikte büyük avantajlar sağlıyor.

Tip sistemi sayesinde:

  • İstemci, hangi veriyi isteyebileceğini önceden biliyor
  • Sunucu, gelen isteğin geçerli olup olmadığını çalışma zamanından önce anlıyor
  • Araçlar (IDE, linter, code generator) otomatik olarak devreye girebiliyor
  • API değişiklikleri daha kontrollü yönetilebiliyor

GraphQL şema tanımlama dili SDL (Schema Definition Language) olarak adlandırılıyor ve oldukça okunabilir bir sözdizime sahip.

# Basit bir GraphQL şeması örneği
# schema.graphql

type User {
  id: ID!
  username: String!
  email: String!
  age: Int
  isActive: Boolean!
  createdAt: String!
}

type Query {
  user(id: ID!): User
  users: [User!]!
}

Buradaki ! işareti “null olamaz” anlamına geliyor. [User!]! ifadesi ise “null olamayan User öğelerinden oluşan, null olamayan bir liste” demek. Bu ayrımı iyi kavramak şema tasarımında kritik.

Skalar Tipler

GraphQL, beş adet yerleşik skalar tip sunar:

  • String: UTF-8 karakter dizisi
  • Int: 32-bit işaretli tam sayı
  • Float: Çift hassasiyetli kayan noktalı sayı
  • Boolean: true veya false
  • ID: Benzersiz tanımlayıcı, String olarak serileştirilir ama ID semantiği taşır

Bunların dışında özel skalar tipler de tanımlayabilirsiniz. Gerçek projelerde tarih/saat formatları, email adresleri veya URL’ler için custom skalar tanımlamak oldukça yaygın:

# Custom skalar tanımları
scalar DateTime
scalar Email
scalar URL
scalar JSON

type Post {
  id: ID!
  title: String!
  content: String!
  publishedAt: DateTime
  authorEmail: Email!
  coverImage: URL
  metadata: JSON
}

Custom skalar tanımladığınızda sunucu tarafında bu tipin nasıl serileştirileceğini ve doğrulanacağını da belirtmeniz gerekir. Örneğin graphql-scalars kütüphanesi bu iş için hazır çözümler sunar.

Object Tipler ve Alan Tanımları

GraphQL şemasının temel yapı taşı Object Type‘lardır. Bir nesneyi ve onun alanlarını tanımlarlar.

# E-ticaret senaryosu için tip tanımları
type Product {
  id: ID!
  name: String!
  description: String
  price: Float!
  currency: String!
  stock: Int!
  category: Category!
  tags: [String!]!
  images: [ProductImage!]!
  reviews: [Review!]!
  averageRating: Float
  isAvailable: Boolean!
}

type Category {
  id: ID!
  name: String!
  slug: String!
  parentCategory: Category
  subCategories: [Category!]!
  products: [Product!]!
}

type ProductImage {
  id: ID!
  url: URL!
  altText: String
  isPrimary: Boolean!
}

Dikkat edin, Category tipi kendi kendine referans veriyor. parentCategory bir Category döndürüyor, subCategories ise Category listesi. Bu tür özyinelemeli yapılar GraphQL’de gayet doğal çalışıyor.

Query, Mutation ve Subscription: Kök Tipler

GraphQL şemasında üç özel kök tip bulunur. Bunlar API’nizin giriş noktalarıdır.

Query tipi okuma operasyonları için kullanılır. REST’teki GET isteklerine karşılık gelir.

Mutation tipi yazma operasyonları içindir. REST’teki POST, PUT, PATCH, DELETE’e karşılık gelir.

Subscription tipi ise gerçek zamanlı güncellemeler için kullanılır, WebSocket üzerinden çalışır.

# Kök tip tanımları
type Query {
  # Tekil sorgular
  product(id: ID!): Product
  category(slug: String!): Category
  me: User

  # Liste sorgular
  products(
    categoryId: ID
    minPrice: Float
    maxPrice: Float
    inStock: Boolean
    limit: Int = 20
    offset: Int = 0
  ): ProductConnection!

  # Arama
  searchProducts(query: String!, limit: Int = 10): [Product!]!
}

type Mutation {
  # Kullanıcı işlemleri
  register(input: RegisterInput!): AuthPayload!
  login(email: String!, password: String!): AuthPayload!
  logout: Boolean!

  # Ürün işlemleri
  createProduct(input: CreateProductInput!): Product!
  updateProduct(id: ID!, input: UpdateProductInput!): Product!
  deleteProduct(id: ID!): Boolean!

  # Sipariş işlemleri
  createOrder(input: CreateOrderInput!): Order!
  cancelOrder(orderId: ID!): Order!
}

type Subscription {
  orderStatusChanged(orderId: ID!): Order!
  stockUpdated(productId: ID!): Product!
}

Input Tipler

Mutation’larda argüman olarak karmaşık objeler geçmek istediğinizde Input Type kullanırsınız. Object Type ile karıştırılmamalı, Input Type’lar sadece girdi olarak kullanılır, döndürülemezler.

# Input tip tanımları
input RegisterInput {
  username: String!
  email: String!
  password: String!
  firstName: String!
  lastName: String!
}

input CreateProductInput {
  name: String!
  description: String
  price: Float!
  currency: String = "TRY"
  stock: Int!
  categoryId: ID!
  tags: [String!]
  images: [ProductImageInput!]
}

input ProductImageInput {
  url: String!
  altText: String
  isPrimary: Boolean = false
}

input UpdateProductInput {
  name: String
  description: String
  price: Float
  stock: Int
  categoryId: ID
  tags: [String!]
}

UpdateProductInput‘taki tüm alanlar opsiyonel dikkat ettiniz mi? Bu kasıtlı bir tasarım kararı. Partial update senaryolarında sadece değişen alanları göndermek istersiniz. Bu pattern PATCH semantiğini GraphQL’e taşımanın yaygın yolu.

Enum Tipler

Belirli bir değer kümesinden birini alabilen alanlar için Enum kullanmak hem tip güvenliği sağlar hem de şemayı daha okunabilir kılar.

# Enum tanımları - gerçek e-ticaret senaryosu
enum OrderStatus {
  PENDING
  PAYMENT_PROCESSING
  PAYMENT_FAILED
  CONFIRMED
  PREPARING
  SHIPPED
  DELIVERED
  CANCELLED
  REFUNDED
}

enum PaymentMethod {
  CREDIT_CARD
  DEBIT_CARD
  BANK_TRANSFER
  CRYPTO
  WALLET
}

enum SortDirection {
  ASC
  DESC
}

enum ProductSortField {
  NAME
  PRICE
  CREATED_AT
  RATING
  STOCK
}

type Order {
  id: ID!
  status: OrderStatus!
  paymentMethod: PaymentMethod!
  items: [OrderItem!]!
  totalAmount: Float!
  createdAt: DateTime!
  updatedAt: DateTime!
}

# Enum'ları input'larda da kullanabilirsiniz
input ProductsFilterInput {
  sortBy: ProductSortField = CREATED_AT
  sortDirection: SortDirection = DESC
  minPrice: Float
  maxPrice: Float
  inStock: Boolean
}

Interface ve Union Tipler

Gerçek dünya uygulamalarında farklı tiplerin ortak özelliklere sahip olduğu durumlarla sıkça karşılaşırsınız. Burada Interface ve Union tipleri devreye girer.

Interface, birden fazla tipin ortak alanlarını tanımlar. Bir tip bir interface’i implement ettiğinde o alanları mutlaka içermek zorundadır.

# Interface kullanım örneği - bildirim sistemi
interface Notification {
  id: ID!
  title: String!
  message: String!
  isRead: Boolean!
  createdAt: DateTime!
  recipient: User!
}

type OrderNotification implements Notification {
  id: ID!
  title: String!
  message: String!
  isRead: Boolean!
  createdAt: DateTime!
  recipient: User!
  # OrderNotification'a özgü alan
  order: Order!
  previousStatus: OrderStatus
  newStatus: OrderStatus!
}

type SystemNotification implements Notification {
  id: ID!
  title: String!
  message: String!
  isRead: Boolean!
  createdAt: DateTime!
  recipient: User!
  # SystemNotification'a özgü alan
  severity: String!
  actionUrl: URL
}

type Query {
  notifications(userId: ID!, limit: Int = 20): [Notification!]!
  unreadNotificationCount(userId: ID!): Int!
}

Union ise farklı tiplerin ortak alanları olmadan bir arada döndürülebildiği senaryolar içindir. Arama sonuçları buna iyi bir örnek:

# Union kullanım örneği - arama sonuçları
union SearchResult = Product | Category | User | BlogPost

type Query {
  search(query: String!, limit: Int = 20): [SearchResult!]!
}

# İstemci tarafında fragment kullanımı
# query SearchEverything($q: String!) {
#   search(query: $q) {
#     ... on Product {
#       id
#       name
#       price
#     }
#     ... on Category {
#       id
#       name
#       slug
#     }
#     ... on User {
#       id
#       username
#     }
#   }
# }

Pagination Tasarımı: Cursor-Based ve Offset

Büyük veri setlerini döndürürken pagination vazgeçilmez. GraphQL’de iki yaygın yaklaşım var: offset-based ve cursor-based (Relay Connection Pattern).

Cursor-based pagination özellikle gerçek zamanlı veri ve büyük veri setleri için daha güvenilir:

# Relay Connection Pattern - endüstri standardı pagination
type ProductConnection {
  edges: [ProductEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type ProductEdge {
  node: Product!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type Query {
  products(
    first: Int
    after: String
    last: Int
    before: String
    filter: ProductsFilterInput
  ): ProductConnection!
}

# Basit offset pagination alternatifi
type PaginatedProducts {
  items: [Product!]!
  totalCount: Int!
  totalPages: Int!
  currentPage: Int!
  hasNextPage: Boolean!
}

Şema Tasarımında Best Practice’ler

Yıllar içinde GraphQL şema tasarımında bazı kalıplar öne çıkmıştır. Bunları gerçek projelerden damıtılmış pratik bilgi olarak düşünebilirsiniz.

Tutarlı isimlendirme kritik önem taşır. Query’ler için fiil kullanmayın, isim kullanın. getUser yerine user, listProducts yerine products gibi.

Mutation’ları fiil-isim şeklinde isimlendirin. createProduct, updateUser, deleteOrder gibi. Dönüş tipi olarak da mutasyona konu olan nesneyi döndürün, bu istemcinin cache’ini güncellemesini kolaylaştırır.

Null handling’e dikkat edin. Her şeyi ! ile non-null yapmak cazip gelse de bu ileride şemayı değiştirmeyi zorlaştırır. Opsiyonel olabilecek alanları baştan opsiyonel bırakın.

Argümanları Input Type ile gruplayın. Özellikle mutation’larda. Hem daha temiz bir API sunar hem de ileride yeni alanlar eklemeyi kolaylaştırır.

# Kötü tasarım örneği
type Mutation {
  updateUser(
    id: ID!
    firstName: String
    lastName: String
    email: String
    phone: String
    address: String
    city: String
    country: String
  ): User!
}

# İyi tasarım örneği
input UpdateUserInput {
  firstName: String
  lastName: String
  email: String
  phone: String
  address: AddressInput
}

input AddressInput {
  street: String!
  city: String!
  country: String!
  postalCode: String
}

type Mutation {
  updateUser(id: ID!, input: UpdateUserInput!): User!
}

Direktifler ile Şema Zenginleştirme

GraphQL direktifleri şemaya ve sorgulara ek davranış eklemenizi sağlar. Yerleşik direktifler @deprecated, @skip ve @include‘dur. Bunların yanı sıra custom direktifler tanımlayabilirsiniz.

# Direktif kullanımı
type Product {
  id: ID!
  name: String!
  price: Float!
  # Kullanımdan kaldırılmış alan
  priceWithVAT: Float @deprecated(reason: "Use 'totalPrice' instead")
  totalPrice: Float!
  # Sadece admin'lerin görebildiği alan
  costPrice: Float @auth(requires: ADMIN)
  internalNotes: String @auth(requires: ADMIN)
}

# Custom direktif tanımı
directive @auth(requires: Role = USER) on FIELD_DEFINITION
directive @rateLimit(max: Int!, window: String!) on FIELD_DEFINITION
directive @cacheControl(maxAge: Int!, scope: CacheScope) on FIELD_DEFINITION

enum Role {
  GUEST
  USER
  MODERATOR
  ADMIN
}

enum CacheScope {
  PUBLIC
  PRIVATE
}

type Query {
  products: [Product!]! @cacheControl(maxAge: 300, scope: PUBLIC)
  adminDashboard: AdminStats! @auth(requires: ADMIN) @rateLimit(max: 10, window: "1m")
}

Gerçek Dünya Şema Örneği: Blog Platformu

Tüm kavramları bir araya getiren kapsamlı bir blog platformu şeması:

# Tam blog platformu şeması
scalar DateTime
scalar URL

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

enum CommentStatus {
  VISIBLE
  HIDDEN
  SPAM
}

interface Node {
  id: ID!
}

type User implements Node {
  id: ID!
  username: String!
  email: String!
  displayName: String!
  avatar: URL
  bio: String
  posts(status: PostStatus, limit: Int = 10): [Post!]!
  followerCount: Int!
  followingCount: Int!
  isFollowedByMe: Boolean!
  createdAt: DateTime!
}

type Post implements Node {
  id: ID!
  title: String!
  slug: String!
  excerpt: String
  content: String!
  status: PostStatus!
  author: User!
  tags: [Tag!]!
  comments(limit: Int = 20, after: String): CommentConnection!
  likeCount: Int!
  isLikedByMe: Boolean!
  readingTime: Int!
  publishedAt: DateTime
  updatedAt: DateTime!
  createdAt: DateTime!
}

type Tag implements Node {
  id: ID!
  name: String!
  slug: String!
  postCount: Int!
}

type Comment implements Node {
  id: ID!
  content: String!
  author: User!
  post: Post!
  parentComment: Comment
  replies: [Comment!]!
  status: CommentStatus!
  likeCount: Int!
  createdAt: DateTime!
}

type CommentConnection {
  edges: [CommentEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type CommentEdge {
  node: Comment!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

input CreatePostInput {
  title: String!
  content: String!
  excerpt: String
  tags: [String!]
  status: PostStatus = DRAFT
}

input UpdatePostInput {
  title: String
  content: String
  excerpt: String
  tags: [String!]
  status: PostStatus
}

type Query {
  me: User
  user(username: String!): User
  post(slug: String!): Post
  posts(
    authorId: ID
    tag: String
    status: PostStatus
    first: Int = 20
    after: String
  ): PostConnection!
  trendingPosts(limit: Int = 10): [Post!]!
  searchPosts(query: String!): [Post!]!
}

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type PostEdge {
  node: Post!
  cursor: String!
}

type Mutation {
  createPost(input: CreatePostInput!): Post!
  updatePost(id: ID!, input: UpdatePostInput!): Post!
  deletePost(id: ID!): Boolean!
  publishPost(id: ID!): Post!
  likePost(id: ID!): Post!
  unlikePost(id: ID!): Post!
  addComment(postId: ID!, content: String!, parentCommentId: ID): Comment!
  deleteComment(id: ID!): Boolean!
  followUser(userId: ID!): User!
  unfollowUser(userId: ID!): User!
}

type Subscription {
  newComment(postId: ID!): Comment!
  newPost(authorId: ID): Post!
}

Şema Versiyonlama ve Evrim

GraphQL’in en büyük avantajlarından biri şemayı versiyonlamak zorunda kalmadan evrimleştirebildiğinizdir. Ancak bu, her değişikliği özgürce yapabileceğiniz anlamına gelmiyor.

Breaking change olarak kabul edilen durumlar:

  • Var olan bir alan veya tipi silmek
  • Alanın tipini değiştirmek
  • Zorunlu argüman eklemek
  • Non-null alan eklemek (query’leri bozmaz ama mutation input için breaking olabilir)

Non-breaking change olarak kabul edilen durumlar:

  • Yeni alan eklemek
  • Opsiyonel argüman eklemek
  • Yeni tip eklemek
  • Yeni enum değeri eklemek (dikkatli olun, istemci tüm değerleri handle etmiyorsa sorun çıkabilir)

Bir alanı silmeniz gerektiğinde @deprecated direktifini kullanın, bir süre bekleyin ve sonra kaldırın. Bu graceful degradation süreci ekipler arası iletişimi de kolaylaştırır.

Sonuç

GraphQL tip sistemi ve şema tasarımı, API geliştirmenin en kritik adımlarından birini oluşturuyor. İyi tasarlanmış bir şema, istemci geliştiriciyle olan iletişimi netleştirir, runtime hatalarını azaltır ve uygulamanın uzun vadeli bakımını kolaylaştırır.

Bu yazıda ele aldığımız konuları özetleyecek olursak: skalar ve object tipler temel yapı taşlarınızdır. Enum’lar sabit değer kümelerini ifade etmek için idealdir. Interface ve Union tipler polimorfik senaryolarda kurtarıcıdır. Input tipler mutation’larınızı temiz tutar. Pagination için Relay Connection Pattern endüstri standardı haline gelmiştir. Direktifler ise şemanıza cross-cutting concern’leri eklemenin zarif yoludur.

Şema tasarımı bir kez yapıp bitirilen bir iş değil, uygulamanızla birlikte evrilen bir süreçtir. Başlangıçta mükemmel olmaya çalışmak yerine, tutarlı olun ve değişikliklerinizde breaking change yaratmamaya özen gösterin. Ekibinizle şema tasarımı kararlarını konuşun, çünkü şema aynı zamanda bir iletişim aracıdır. GraphQL’in sunduğu introspection özelliği sayesinde şemanız otomatik olarak belgelenmiş olur, bu da yeni ekip üyelerinin ve istemci geliştiricilerin işini büyük ölçüde kolaylaştırır.

Bir yanıt yazın

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