GraphQL Direktifleri: @skip, @include ve Özel Direktif Yazımı
GraphQL ile ciddi bir API geliştirmeye başladığında, er ya da geç şu soruyla yüzleşiyorsun: “Bu alanı her zaman döndürmek zorunda mıyım, yoksa koşula göre kontrol edebilir miyim?” İşte tam bu noktada GraphQL direktifleri devreye giriyor. Direktifler, sorgu çalışma zamanında davranışı değiştirmeni sağlayan güçlü araçlar. Hem built-in direktifler hem de özel yazabileceğin direktifler, şema tasarımını bambaşka bir seviyeye taşıyor. Bugün @skip, @include direktiflerini derinlemesine inceleyeceğiz ve ardından kendi direktifini nasıl yazarsın, gerçek dünya örnekleriyle göreceğiz.
GraphQL Direktifi Nedir?
Direktifler, GraphQL sorgularına veya şema tanımlarına ek davranış ekleyen @ ile başlayan ifadelerdir. İki ana kullanım yeri var: sorgu direktifleri (query-time) ve şema direktifleri (schema definition time).
Sorgu direktifleri, client tarafından sorgu gönderilirken kullanılır. Şema direktifleri ise SDL (Schema Definition Language) içinde type, field veya argument üzerinde tanımlanır.
Temel sözdizimi şu şekilde:
# Sorgu direktifi kullanımı
query GetUser($showEmail: Boolean!) {
user(id: "123") {
name
email @include(if: $showEmail)
role @skip(if: $isGuest)
}
}
GraphQL spec’e göre direktifler belirli “location”lara uygulanabilir. Bunlar FIELD, FRAGMENT_SPREAD, INLINE_FRAGMENT, SCHEMA, SCALAR, OBJECT, FIELD_DEFINITION, ARGUMENT_DEFINITION, INTERFACE, UNION, ENUM, ENUM_VALUE, INPUT_OBJECT, INPUT_FIELD_DEFINITION gibi lokasyonlardır.
@include Direktifi
@include direktifi, verilen koşul true olduğunda ilgili alanı sorguya dahil eder. Temel mantık şu: “Bunu sadece şu koşul sağlandığında getir.”
# @include kullanımı - basit örnek
query GetProduct($showInventory: Boolean!) {
product(id: "prod_456") {
id
name
price
# showInventory true ise bu alan gelir
inventory @include(if: $showInventory) {
quantity
warehouse
lastUpdated
}
}
}
Gerçek dünya senaryosu düşünelim: Bir e-ticaret uygulamasında admin paneli ile kullanıcı arayüzü aynı API’yi kullanıyor. Admin stok bilgisini görmeli, normal kullanıcı görmemeli. Bunu backend’de iki ayrı endpoint açmak yerine @include ile çözebilirsin.
# Fragment ile @include kullanımı
fragment ProductDetails on Product {
id
name
price
description
}
fragment AdminDetails on Product {
stockCount
supplierInfo
costPrice
}
query GetProductPage($isAdmin: Boolean!, $productId: ID!) {
product(id: $productId) {
...ProductDetails
...AdminDetails @include(if: $isAdmin)
}
}
Bu yaklaşım ile tek bir sorgu hem admin hem de normal kullanıcı için çalışıyor. Backend tarafında ekstra iş yok, client request’i kontrol ediyor.
@skip Direktifi
@skip direktifi @include‘un tam tersidir. Koşul true olduğunda o alanı atlar, yani response’a dahil etmez. Mantıksal olarak @skip(if: X) ile @include(if: !X) aynı sonucu verir ama okunabilirlik açısından hangisinin daha anlamlı olduğuna göre seçim yaparsın.
# @skip kullanımı
query GetDashboard($isLightMode: Boolean!, $userId: ID!) {
user(id: $userId) {
name
email
# Hafif modda detaylı analytics skip edilir
analyticsData @skip(if: $isLightMode) {
pageViews
sessionDuration
conversionRate
heatmapData
}
basicStats {
totalOrders
lastLogin
}
}
}
Bir mobil uygulama düşün: 3G bağlantıda olan kullanıcı için ağır veriyi skip etmek hem bant genişliği hem de pil ömrü açısından kritik. Client tarafında bağlantı türünü tespit edip buna göre isLightMode değişkenini set edebilirsin.
@skip ve @include Birlikte Kullanımı
Bu iki direktifi aynı anda bir field üzerinde kullanabilirsin ama dikkatli ol. GraphQL spec’e göre eğer @skip(if: true) varsa @include ne derse desin o alan gelmiyor.
# Birlikte kullanım - dikkatli ol!
query TestBoth($skip: Boolean!, $include: Boolean!) {
user {
name
# Bu alan sadece skip=false VE include=true ise gelir
sensitiveData @skip(if: $skip) @include(if: $include) {
ssn
creditScore
}
}
}
Pratikte bu kombinasyonu çok az görürsün. Genellikle birini seçip kullanmak kodu daha okunabilir yapar.
Özel Direktif Yazımı
İşte asıl eğlence buradan başlıyor. Built-in direktifler belirli sorunları çözer ama gerçek projelerde çok daha spesifik ihtiyaçların olur. Örneğin: yetkilendirme, rate limiting, caching, veri maskeleme, deprecation yönetimi…
Şema Direktifi Tanımlama
Özel direktif yazarken önce SDL’de tanımlarsın, sonra resolver’da veya şema transform katmanında implement edersin.
# schema.graphql - Direktif tanımları
directive @auth(
requires: Role = USER
) on OBJECT | FIELD_DEFINITION
directive @rateLimit(
max: Int!
window: String!
) on FIELD_DEFINITION
directive @deprecated(
reason: String = "Artık kullanılmıyor"
) on FIELD_DEFINITION | ENUM_VALUE
enum Role {
ADMIN
MODERATOR
USER
GUEST
}
type Query {
publicPosts: [Post!]!
adminPanel: AdminData @auth(requires: ADMIN)
userProfile: User @auth(requires: USER)
search(query: String!): [SearchResult!]! @rateLimit(max: 100, window: "1m")
}
@auth Direktifi Implementation (Apollo Server)
Apollo Server ile @auth direktifini nasıl implement edersin, adım adım bakalım:
# Node.js / Apollo Server implementasyonu
# auth-directive.js
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
import { defaultFieldResolver } from 'graphql';
export function authDirectiveTransformer(schema, directiveName = 'auth') {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
if (authDirective) {
const { requires } = authDirective;
const { resolve = defaultFieldResolver } = fieldConfig;
// Orijinal resolver'ı wrap ediyoruz
fieldConfig.resolve = async function (source, args, context, info) {
const { user } = context;
if (!user) {
throw new Error('Bu işlem için giriş yapmanız gerekiyor');
}
const roleHierarchy = {
GUEST: 0,
USER: 1,
MODERATOR: 2,
ADMIN: 3
};
const userLevel = roleHierarchy[user.role] ?? 0;
const requiredLevel = roleHierarchy[requires] ?? 1;
if (userLevel < requiredLevel) {
throw new Error(
`Bu işlem için ${requires} yetkisi gerekiyor. Mevcut yetkiniz: ${user.role}`
);
}
return resolve(source, args, context, info);
};
return fieldConfig;
}
}
});
}
# server.js - Schema'ya uygulama
import { makeExecutableSchema } from '@graphql-tools/schema';
import { authDirectiveTransformer } from './auth-directive.js';
let schema = makeExecutableSchema({
typeDefs,
resolvers
});
schema = authDirectiveTransformer(schema, 'auth');
@cache Direktifi – Pratik Senaryo
Production ortamında sık karşılaştığım bir senaryo: bazı field’ların Redis’ten cache’lenmesi gerekiyor ama bu mantığı her resolver’a tekrar tekrar yazmak istemiyorsun.
# cache direktifi tanımı ve implementasyonu
# schema.graphql
directive @cache(
ttl: Int = 300,
key: String
) on FIELD_DEFINITION
type Query {
# 5 dakika cache
popularProducts: [Product!]! @cache(ttl: 300)
# 1 saat cache, özel key ile
siteSettings: Settings! @cache(ttl: 3600, key: "global:settings")
# Cache yok
userCart(userId: ID!): Cart
}
# cache-directive.js
export function cacheDirectiveTransformer(schema, redisClient) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
const cacheDirective = getDirective(schema, fieldConfig, 'cache')?.[0];
if (cacheDirective) {
const { ttl, key } = cacheDirective;
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = async function (source, args, context, info) {
// Cache key oluştur
const cacheKey = key ||
`gql:${typeName}:${fieldName}:${JSON.stringify(args)}`;
// Önce cache'e bak
const cached = await redisClient.get(cacheKey);
if (cached) {
console.log(`Cache hit: ${cacheKey}`);
return JSON.parse(cached);
}
// Cache miss - resolver'ı çalıştır
const result = await resolve(source, args, context, info);
// Sonucu cache'e yaz
await redisClient.setEx(
cacheKey,
ttl,
JSON.stringify(result)
);
return result;
};
return fieldConfig;
}
}
});
}
@deprecated Direktifi ve Özel Versiyonu
GraphQL’in built-in @deprecated direktifi var ama production’da daha fazlasına ihtiyaç duyarsın: “Ne zaman kaldırılacak?”, “Yerine ne kullanılmalı?” gibi bilgileri de şemaya eklemek isteyebilirsin.
# Genişletilmiş deprecation direktifi
directive @willBeRemoved(
inVersion: String!
useInstead: String!
reason: String
) on FIELD_DEFINITION | ARGUMENT_DEFINITION
type User {
# Eski alan - v3.0'da kalkacak
fullName: String @deprecated(reason: "firstName ve lastName kullanın")
@willBeRemoved(
inVersion: "3.0.0"
useInstead: "firstName + lastName"
reason: "Ayrı alanlar daha esnek kullanım sağlar"
)
firstName: String!
lastName: String!
email: String!
}
# Bu direktifin implementasyonu introspection sorgularında
# bu bilgileri exposed eder, build time'da uyarı verebilirsin
Rate Limiting Direktifi
Özellikle public API’lerde her endpoint için ayrı ayrı rate limit mantığı yazmak yerine direktif kullanmak çok temiz bir yaklaşım:
# rate-limit-directive.js
import { RateLimiterRedis } from 'rate-limiter-flexible';
export function rateLimitDirectiveTransformer(schema, redisClient) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const rateLimitDirective = getDirective(
schema,
fieldConfig,
'rateLimit'
)?.[0];
if (rateLimitDirective) {
const { max, window } = rateLimitDirective;
const { resolve = defaultFieldResolver } = fieldConfig;
// Window string'ini saniyeye çevir: "1m" -> 60, "1h" -> 3600
const windowSeconds = parseWindow(window);
const limiter = new RateLimiterRedis({
storeClient: redisClient,
points: max,
duration: windowSeconds
});
fieldConfig.resolve = async function (source, args, context, info) {
const identifier = context.user?.id || context.ip || 'anonymous';
const key = `ratelimit:${info.fieldName}:${identifier}`;
try {
await limiter.consume(key);
} catch (rejRes) {
const retryAfter = Math.ceil(rejRes.msBeforeNext / 1000);
throw new Error(
`Rate limit aşıldı. ${retryAfter} saniye sonra tekrar deneyin. ` +
`Limit: ${max} istek / ${window}`
);
}
return resolve(source, args, context, info);
};
return fieldConfig;
}
}
});
}
function parseWindow(window) {
const unit = window.slice(-1);
const value = parseInt(window.slice(0, -1));
const multipliers = { s: 1, m: 60, h: 3600, d: 86400 };
return value * (multipliers[unit] || 1);
}
Direktifleri Birleştirmek ve Yönetmek
Gerçek bir production şemasında birden fazla direktif transform’unu birbirine zincirlemek gerekir:
# server.js - Tüm direktifleri bir arada uygulama
import { makeExecutableSchema } from '@graphql-tools/schema';
import { authDirectiveTransformer } from './directives/auth.js';
import { cacheDirectiveTransformer } from './directives/cache.js';
import { rateLimitDirectiveTransformer } from './directives/rate-limit.js';
import { redisClient } from './redis.js';
let schema = makeExecutableSchema({ typeDefs, resolvers });
// Transform'ları sırayla uygula - sıra önemli!
// Önce rate limit kontrol et, sonra auth, sonra cache
schema = rateLimitDirectiveTransformer(schema, redisClient);
schema = authDirectiveTransformer(schema);
schema = cacheDirectiveTransformer(schema, redisClient);
const server = new ApolloServer({
schema,
context: ({ req }) => ({
user: req.user,
ip: req.ip
})
});
Sıra önemlidir. Eğer önce cache uygularsan, yetkisiz bir kullanıcı başka birinin cache’lenmiş verisini alabilir. Doğru sıra genellikle: rate limit -> auth -> cache -> business logic şeklindedir.
Test Etme Stratejisi
Direktifleri test ederken dikkat etmen gereken birkaç nokak var:
# Jest ile direktif testi
import { buildSchema, graphql } from 'graphql';
import { authDirectiveTransformer } from './auth-directive.js';
describe('@auth direktifi testleri', () => {
const typeDefs = `
directive @auth(requires: Role = USER) on FIELD_DEFINITION
enum Role { ADMIN USER GUEST }
type Query {
publicData: String
userData: String @auth(requires: USER)
adminData: String @auth(requires: ADMIN)
}
`;
const resolvers = {
Query: {
publicData: () => 'herkese açık',
userData: () => 'kullanıcı verisi',
adminData: () => 'admin verisi'
}
};
let schema = makeExecutableSchema({ typeDefs, resolvers });
schema = authDirectiveTransformer(schema);
test('Giriş yapmamış kullanıcı korumalı alana erişemez', async () => {
const result = await graphql({
schema,
source: '{ userData }',
contextValue: { user: null }
});
expect(result.errors[0].message).toContain('giriş yapmanız gerekiyor');
});
test('Admin tüm alanlara erişebilir', async () => {
const result = await graphql({
schema,
source: '{ adminData }',
contextValue: { user: { role: 'ADMIN' } }
});
expect(result.data.adminData).toBe('admin verisi');
expect(result.errors).toBeUndefined();
});
test('Normal kullanıcı admin verisine erişemez', async () => {
const result = await graphql({
schema,
source: '{ adminData }',
contextValue: { user: { role: 'USER' } }
});
expect(result.errors[0].message).toContain('ADMIN yetkisi gerekiyor');
});
});
Sık Yapılan Hatalar
Direktif yazarken en çok karşılaştığım sorunları paylaşayım:
- Resolver override’ı unutmak:
fieldConfig.resolveassign etmeden öncedefaultFieldResolverimport etmeyi unutma. Aksi halde bazı field’larda resolverundefinedolabilir.
- Directive location hataları:
FIELD_DEFINITIONiçin tanımladığın direktifiARGUMENT_DEFINITIONüzerinde kullanmaya çalışırsan şema validation hatası alırsın.
- Context erişimi: Direktif içinde
context‘e erişmek için resolver signature’ının dördüncü parametresi olaninfo‘yu değil, üçüncü parametre olancontext‘i kullan.
- Async/await unutmak: Cache veya rate limit gibi async operasyonlarda
async/awaitkullanmazsan resolve fonksiyonu Promise döner ama GraphQL bunu beklemez.
- SDL’de direktif sırası: SDL’de direktifler tanımlanmadan kullanılırsa validation hatası alırsın. Her zaman kullanımdan önce tanımla.
Sonuç
GraphQL direktifleri, özellikle @skip ve @include, sorgu esnekliğini ciddi ölçüde artırıyor. Tek endpoint üzerinden farklı client ihtiyaçlarını karşılamak, mobil ve web arasında veri yükünü optimize etmek, admin ve user view’larını ayrıştırmak… Bunların hepsi direktiflerle temiz bir şekilde çözülebiliyor.
Özel direktifler ise cross-cutting concern’leri yönetmenin en elegant yolu. Authentication, caching, rate limiting, logging gibi her yerde tekrar eden mantığı bir kez yazıp direktif olarak şemana ekliyorsun. Her resolver’da aynı kodu tekrar yazmak yerine @auth(requires: ADMIN) yazmak yeterli oluyor.
Pratikte önerim şu: Built-in direktiflerle başla, ihtiyaç duydukça özel direktif yazmaya geç. Fazla direktif şemayı karmaşıklaştırabilir. Her direktifin iyi dokümante edilmesi ve test edilmesi şart, yoksa şemanı bilen sadece sen olursun ve o da zamanla unutulur. Direktifleri doğru kullandığında GraphQL API’n hem daha güvenli hem de çok daha bakımı kolay bir hale geliyor.
