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.
