Node.js’te GraphQL API Sunucusu Kurulumu: Kapsamlı Bir Rehber
Modern web uygulamaları, veriye erişim ve veri manipülasyonu için güçlü ve esnek API’lere ihtiyaç duyar. Geleneksel RESTful API’ler bu ihtiyacı büyük ölçüde karşılarken, özellikle karmaşık uygulamalarda bazı sınırlamaları beraberinde getirebilir. Bu noktada, Facebook tarafından geliştirilen ve açık kaynak hale getirilen GraphQL, API tasarımına yepyeni bir bakış açısı sunarak geliştiricilerin imdadına yetişmiştir. Node.js ise, JavaScript tabanlı, yüksek performanslı ve ölçeklenebilir sunucu tarafı uygulamalar geliştirmek için ideal bir ortamdır. Bu makalede, Node.js kullanarak nasıl bir GraphQL API sunucusu kuracağımızı, temelden ileri seviyeye kadar tüm adımlarıyla ele alacağız. GraphQL’in temel prensiplerinden başlayarak, bir sunucu kurma, şema tanımlama, çözümleyicileri yazma, veri kaynaklarıyla entegrasyon, kimlik doğrulama, abonelikler ve en iyi uygulamalara kadar geniş bir yelpazeyi kapsayacağız.
GraphQL Temelleri
GraphQL Nedir?
GraphQL, API’ler için bir sorgu dili ve bu sorguları çalıştırmak için bir çalışma zamanı ortamıdır. Geleneksel REST API’lerinden farklı olarak, istemcilerin tam olarak neye ihtiyaç duyduklarını belirtmelerine olanak tanır. Bu, istemcinin tek bir istekte birden fazla kaynakla ilgili veri alabileceği anlamına gelir ve “over-fetching” (gereğinden fazla veri alma) veya “under-fetching” (gereğinden az veri alıp birden fazla istek yapma) sorunlarını ortadan kaldırır. GraphQL, API’nizin tüm yeteneklerini açıklayan güçlü bir tip sistemine sahiptir. Bu tip sistemi, istemci tarafında veri doğrulama ve otomatik tamamlama gibi avantajlar sunar.
REST ile Karşılaştırma
REST ve GraphQL arasındaki temel farkları anlamak, GraphQL’in neden tercih edildiğini daha iyi kavramamızı sağlar:
- Veri Çekme Modeli: REST’te genellikle her kaynak için ayrı bir endpoint bulunur (örn.
/users,/posts). Bir kullanıcının gönderilerini almak için iki ayrı istek gerekebilir. GraphQL’de ise istemci, tek bir sorgu ile ihtiyacı olan tüm veriyi (kullanıcı ve gönderileri) tek bir endpoint’ten çekebilir. - Esneklik: REST API’leri genellikle sabit veri yapıları döndürür. İstemcinin farklı alanlara ihtiyacı olduğunda API’nin değiştirilmesi gerekebilir. GraphQL’de istemci, sorgusunda hangi alanları istediğini açıkça belirtir, bu da istemciye büyük bir esneklik sağlar.
- Endpoint Sayısı: REST API’lerinde çok sayıda endpoint bulunabilir (
GET /users,GET /users/{id},POST /usersvb.). GraphQL’de ise genellikle tek bir endpoint (örn./graphql) üzerinden tüm işlemler yürütülür. - Versiyonlama: REST API’lerinde versiyonlama (örn.
/v1/users,/v2/users) yaygınken, GraphQL’in esnek yapısı sayesinde şema evrimi daha kolay yönetilir ve genellikle versiyonlamaya gerek kalmaz.
Temel Kavramlar
GraphQL ile çalışırken karşılaşacağımız temel kavramlar şunlardır:
- Schema (Şema): API’nizin tüm veri yapısını ve mevcut operasyonları (sorgular, mutasyonlar, abonelikler) tanımlayan bir dildir. GraphQL Şema Tanımlama Dili (Schema Definition Language – SDL) kullanılarak yazılır.
- Types (Türler): Şemanın yapı taşlarıdır. Veri nesnelerinin (örn.
User,Product) ve bunların alanlarının türlerini (String, Int, Boolean vb.) belirtir. Özel türler (Enum, Input, Interface, Union) de tanımlanabilir. - Queries (Sorgular): API’den veri okumak için kullanılır. REST’teki GET isteklerine benzer.
- Mutations (Değişiklikler): API’deki veriyi değiştirmek (oluşturmak, güncellemek, silmek) için kullanılır. REST’teki POST, PUT, DELETE isteklerine benzer.
- Resolvers (Çözümleyiciler): Şemadaki her alan için, o alanın verisini nasıl alacağını veya işleyeceğini belirten fonksiyonlardır. Bir sorgu veya mutasyon geldiğinde, ilgili resolver çalışır ve veriyi döndürür.
- Subscriptions (Abonelikler): Gerçek zamanlı veri akışı sağlamak için kullanılır. Bir olay (örn. yeni bir mesaj) meydana geldiğinde istemcilere otomatik olarak bildirim gönderir.
- Introspection (İç Gözlem): Bir GraphQL sunucusunun kendi şemasını sorgulama yeteneğidir. Bu sayede geliştirme araçları (GraphQL Playground, GraphiQL) otomatik tamamlama ve dokümantasyon sağlayabilir.
Node.js Ortamını Hazırlama
Ön Gereksinimler
Node.js ile GraphQL API sunucusu kurmak için aşağıdaki ön gereksinimlere ihtiyacımız var:
- Node.js: Sisteminizde kurulu olmalıdır. nodejs.org adresinden indirebilirsiniz.
- npm veya Yarn: Node.js ile birlikte gelen paket yöneticisi npm (Node Package Manager) veya alternatif olarak Yarn kurulu olmalıdır.
Proje Başlatma
Yeni bir Node.js projesi oluşturarak başlayalım. Terminalinizi açın ve aşağıdaki komutları çalıştırın:
mkdir graphql-api-server
cd graphql-api-server
npm init -y
Bu komutlar, projeniz için bir klasör oluşturur, içine girer ve package.json dosyasını varsayılan ayarlarla oluşturur.
Temel Bir GraphQL Sunucusu Kurulumu
Bağımlılıkları Yükleme
GraphQL sunucumuzu kurmak için birkaç temel bağımlılığa ihtiyacımız var. Bu makalede, popüler ve güçlü bir araç olan Apollo Server’ı kullanacağız. Apollo Server, GraphQL sunucusu oluşturmayı ve yönetmeyi büyük ölçüde basitleştirir. Ayrıca, Express.js ile entegrasyonu için apollo-server-express paketini kullanacağız.
npm install apollo-server-express graphql express
apollo-server-express: Apollo Server’ı Express.js uygulamasıyla birlikte kullanmamızı sağlar.graphql: GraphQL’in temel çalışma zamanı ve şema tanımlama yeteneklerini sağlar.express: Temel web sunucusu ve middleware altyapısını sağlar.
Apollo Server Kullanımı
Apollo Server, bir GraphQL sunucusu oluşturmak için gerekli tüm araçları sağlar. Şema tanımlarınızı ve çözümleyicilerinizi alarak bir GraphQL endpoint’i oluşturur ve sorguları işler. Ayrıca, geliştirme sırasında çok faydalı olan GraphQL Playground gibi araçları da otomatik olarak entegre eder.
Şema Tanımlama (Type Definitions)
GraphQL API’mizin kalbi şemadır. Şema, API’nizin desteklediği tüm veri türlerini ve operasyonları (sorgular ve mutasyonlar) tanımlar. src/schema.js adında bir dosya oluşturalım ve aşağıdaki şemayı ekleyelim:
// src/schema.js
const { gql } = require('apollo-server-express');
// GraphQL Şema Tanımlama Dili (SDL) kullanılarak şema oluşturulur.
const typeDefs = gql
# 'Book' adında bir nesne türü tanımlıyoruz.
# Her kitabın bir ID'si, başlığı ve yazarı olacak.
type Book {
id: ID! # ID benzersiz bir tanımlayıcıdır ve '!' boş olamayacağını belirtir.
title: String!
author: String!
year: Int
}
# 'Query' türü, istemcilerin sunucudan veri çekmek için kullanabileceği sorguları tanımlar.
type Query {
# Tüm kitapları döndüren bir sorgu. Dizi döndürdüğü için '[Book]' kullanılır.
books: [Book!]!
# Belirli bir ID'ye sahip kitabı döndüren bir sorgu.
# 'id: ID!' argümanı alır ve bir 'Book' veya null döndürebilir.
book(id: ID!): Book
}
# 'Mutation' türü, istemcilerin sunucudaki veriyi değiştirmek için kullanabileceği operasyonları tanımlar.
type Mutation {
# Yeni bir kitap ekleyen mutasyon. Gerekli argümanlar 'title' ve 'author'dır.
# Eklenen kitabı döndürür.
addBook(title: String!, author: String!, year: Int): Book!
# Varolan bir kitabı güncelleyen mutasyon. 'id' zorunlu, diğerleri isteğe bağlıdır.
updateBook(id: ID!, title: String, author: String, year: Int): Book!
# Bir kitabı silen mutasyon. Silinen kitabın ID'sini döndürür.
deleteBook(id: ID!): ID!
}
;
module.exports = typeDefs;
Yukarıdaki şemada:
Book: Kitap nesnesinin yapısını tanımlayan bir türdür.Query: API’mizden veri almak için kullanabileceğimiz sorguları içerir (booksvebook).Mutation: API’mizdeki veriyi değiştirmek için kullanabileceğimiz operasyonları içerir (addBook,updateBook,deleteBook).ID!,String!gibi ifadeler, alanın boş (null) olamayacağını belirtir.[Book!]!gibi ifadeler, birBooknesneleri dizisi döndürüleceğini ve bu dizinin kendisinin boş olamayacağını ve içindeki her bir elemanın da boş olamayacağını belirtir.
Çözümleyicileri (Resolvers) Yazma
Şemayı tanımladıktan sonra, bu şemadaki alanların verilerini nasıl alacağımızı veya işleyeceğimizi belirten çözümleyici (resolver) fonksiyonlarını yazmamız gerekir. src/resolvers.js adında bir dosya oluşturalım:
// src/resolvers.js
// Geçici bir veri deposu olarak kullanacağımız bir dizi oluşturalım.
// Gerçek uygulamalarda burası bir veritabanı veya başka bir veri kaynağı olacaktır.
const books = [
{ id: '1', title: 'Lord of the Rings', author: 'J.R.R. Tolkien', year: 1954 },
{ id: '2', title: 'The Hobbit', author: 'J.R.R. Tolkien', year: 1937 },
{ id: '3', title: '1984', author: 'George Orwell', year: 1949 },
];
let nextBookId = books.length + 1; // Yeni kitaplar için ID oluşturucu
const resolvers = {
Query: {
// 'books' sorgusu için çözümleyici: Tüm kitapları döndürür.
books: () => books,
// 'book' sorgusu için çözümleyici: Belirli bir ID'ye sahip kitabı döndürür.
// 'args' parametresi sorguda geçirilen argümanları (bu durumda 'id') içerir.
book: (parent, args) => books.find(book => book.id === args.id),
},
Mutation: {
// 'addBook' mutasyonu için çözümleyici: Yeni bir kitap ekler.
addBook: (parent, args) => {
const newBook = {
id: String(nextBookId++), // Yeni bir ID atar
title: args.title,
author: args.author,
year: args.year || null, // Yıl isteğe bağlı olduğu için null olabilir
};
books.push(newBook);
return newBook;
},
// 'updateBook' mutasyonu için çözümleyici: Varolan bir kitabı günceller.
updateBook: (parent, args) => {
const bookIndex = books.findIndex(book => book.id === args.id);
if (bookIndex === -1) {
throw new Error(Kitap bulunamadı: ${args.id});
}
const updatedBook = {
...books[bookIndex],
...args, // Argümanlardaki tüm güncel alanları mevcut kitaba uygula
};
books[bookIndex] = updatedBook;
return updatedBook;
},
// 'deleteBook' mutasyonu için çözümleyici: Bir kitabı siler.
deleteBook: (parent, args) => {
const bookIndex = books.findIndex(book => book.id === args.id);
if (bookIndex === -1) {
throw new Error(Kitap bulunamadı: ${args.id});
}
books.splice(bookIndex, 1); // Diziden kitabı kaldır
return args.id; // Silinen kitabın ID'sini döndür
},
},
};
module.exports = resolvers;
Her resolver fonksiyonu dört argüman alabilir: (parent, args, context, info):
parent(veyaroot): Önceki resolver’ın döndürdüğü sonuçtur. Genellikle üst türün verisini içerir.args: Sorguya veya mutasyona geçirilen argümanlardır (örn.book(id: "1")sorgusundakiid).context: Tüm resolver’lar arasında paylaşılan bir nesnedir. Veritabanı bağlantıları, kimlik doğrulama bilgileri gibi ortak kaynakları taşımak için kullanılır.info: Sorgunun yürütme durumu hakkında daha fazla bilgi içeren gelişmiş bir nesnedir.
Sunucuyu Başlatma
Şema ve çözümleyicileri tanımladıktan sonra, Apollo Server’ı kullanarak sunucumuzu başlatabiliriz. index.js adında ana bir dosya oluşturalım:
// index.js
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const typeDefs = require('./src/schema');
const resolvers = require('./src/resolvers');
async function startApolloServer() {
const app = express(); // Express uygulamasını başlat
// Apollo Server örneğini oluştur
const server = new ApolloServer({
typeDefs, // Şema tanımlarını Apollo Server'a ilet
resolvers, // Çözümleyicileri Apollo Server'a ilet
// Geliştirme ortamında GraphQL Playground'u etkinleştir
// production ortamında kapatılması güvenlik açısından önerilir
introspection: true,
playground: true,
});
// Apollo Server'ı başlat
await server.start();
// Apollo Server'ı Express uygulamasına middleware olarak uygula
server.applyMiddleware({ app, path: '/graphql' });
const PORT = process.env.PORT || 4000;
// Express sunucusunu dinlemeye başla
app.listen(PORT, () => {
console.log(GraphQL API sunucusu çalışıyor: http://localhost:${PORT}/graphql);
console.log(GraphQL Playground'a erişmek için: http://localhost:${PORT}/graphql);
});
}
startApolloServer();
Şimdi terminalde node index.js komutunu çalıştırarak sunucuyu başlatabilirsiniz. Sunucu başarıyla çalıştığında, http://localhost:4000/graphql adresine giderek GraphQL Playground arayüzünü göreceksiniz. Bu arayüz, API’nizi test etmek, sorgu yazmak ve şema dokümantasyonunu görüntülemek için harika bir araçtır.
Test Etme
GraphQL Playground’da aşağıdaki sorguları ve mutasyonları deneyebilirsiniz:
Tüm kitapları sorgulama:
query {
books {
id
title
author
year
}
}
Belirli bir kitabı ID ile sorgulama:
query {
book(id: "1") {
title
author
}
}
Yeni bir kitap ekleme (Mutasyon):
mutation {
addBook(title: "The Great Gatsby", author: "F. Scott Fitzgerald", year: 1925) {
id
title
author
year
}
}
Bir kitabı güncelleme (Mutasyon):
mutation {
updateBook(id: "4", title: "The Great Gatsby (Updated)", year: 1926) {
id
title
author
year
}
}
Bir kitabı silme (Mutasyon):
mutation {
deleteBook(id: "4")
}
Veri Kaynaklarını Entegre Etme
Data Sources Kavramı
Şu ana kadar geçici bir dizi kullandık. Gerçek dünya uygulamalarında veriler genellikle veritabanlarından (SQL, NoSQL), REST API’lerinden veya diğer mikroservislerden gelir. Apollo Server, bu tür veri kaynaklarını yönetmek için Data Sources adı verilen bir soyutlama katmanı sunar. Data sources, veri alma mantığını çözümleyicilerden ayırarak kodunuzu daha temiz, test edilebilir ve yeniden kullanılabilir hale getirir. Ayrıca, otomatik önbellekleme (caching) gibi özellikler de sunabilirler.
Apollo Data Sources
Apollo Server, apollo-datasource ve apollo-datasource-rest gibi paketlerle çeşitli veri kaynaklarını entegre etmek için kolay bir yol sunar. Örneğin, bir REST API’den veri çekecekseniz RESTDataSource sınıfını kullanabilirsiniz. Bir veritabanı ile çalışırken, genellikle özel bir DataSource sınıfı oluşturur veya doğrudan veritabanı istemcisini context objesi aracılığıyla resolver’lara iletiriz.
Veritabanı Entegrasyonu (Örnek: MongoDB ve Mongoose)
Bir MongoDB veritabanı ile entegrasyonu göstermek için Mongoose ORM’yi kullanalım. İlk olarak, Mongoose’u yükleyin:
npm install mongoose dotenv
.env dosyanızı oluşturun ve MongoDB bağlantı URI’nizi ekleyin:
MONGO_URI="mongodb://localhost:27017/graphql-books-db"
src/models/Book.js adında bir dosya oluşturun:
// src/models/Book.js
const mongoose = require('mongoose');
const bookSchema = new mongoose.Schema({
title: {
type: String,
required: true,
},
author: {
type: String,
required: true,
},
year: {
type: Number,
},
});
module.exports = mongoose.model('Book', bookSchema);
Şimdi src/datasources/BooksAPI.js adında bir veri kaynağı oluşturalım:
// src/datasources/BooksAPI.js
const { DataSource } = require('apollo-datasource');
const Book = require('../models/Book'); // Mongoose modelimizi dahil et
class BooksAPI extends DataSource {
constructor() {
super();
}
/
* Bu fonksiyon, Apollo Server'ın başlangıçta veri kaynağını başlatmak için çağırılır.
* Genellikle context'i burada alırız.
*/
initialize(config) {
this.context = config.context;
}
async getAllBooks() {
return Book.find(); // Tüm kitapları veritabanından çek
}
async getBookById(id) {
return Book.findById(id); // Belirli bir ID'ye sahip kitabı çek
}
async createBook(bookInput) {
const newBook = new Book(bookInput);
await newBook.save();
return newBook;
}
async updateBook(id, bookInput) {
return Book.findByIdAndUpdate(id, bookInput, { new: true }); // Güncellenmiş belgeyi döndür
}
async deleteBook(id) {
await Book.findByIdAndDelete(id);
return id;
}
}
module.exports = BooksAPI;
Resolver’larımızı bu veri kaynağını kullanacak şekilde güncelleyelim (src/resolvers.js):
// src/resolvers.js (Güncellenmiş)
const resolvers = {
Query: {
books: (parent, args, { dataSources }) => dataSources.booksAPI.getAllBooks(),
book: (parent, { id }, { dataSources }) => dataSources.booksAPI.getBookById(id),
},
Mutation: {
addBook: (parent, args, { dataSources }) => dataSources.booksAPI.createBook(args),
updateBook: (parent, { id, ...rest }, { dataSources }) => dataSources.booksAPI.updateBook(id, rest),
deleteBook: (parent, { id }, { dataSources }) => dataSources.booksAPI.deleteBook(id),
},
};
module.exports = resolvers;
Son olarak, index.js dosyasını güncelleyerek veritabanı bağlantısını ve veri kaynaklarını Apollo Server’a ekleyelim:
// index.js (Güncellenmiş)
require('dotenv').config(); // .env dosyasını yükle
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const mongoose = require('mongoose'); // Mongoose'u dahil et
const typeDefs = require('./src/schema');
const resolvers = require('./src/resolvers');
const BooksAPI = require('./src/datasources/BooksAPI'); // Veri kaynağımızı dahil et
async function startApolloServer() {
const app = express();
// MongoDB bağlantısı
try {
await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('MongoDB bağlantısı başarılı.');
} catch (error) {
console.error('MongoDB bağlantı hatası:', error);
process.exit(1); // Hata durumunda uygulamayı sonlandır
}
const server = new ApolloServer({
typeDefs,
resolvers,
// dataSources fonksiyonu, her istek için yeni bir veri kaynağı örneği oluşturur.
dataSources: () => ({
booksAPI: new BooksAPI(),
}),
// Context fonksiyonu, her istekle birlikte resolver'lara iletilecek bir nesne oluşturur.
// Burada kimlik doğrulama bilgileri veya diğer paylaşılan kaynaklar eklenebilir.
context: ({ req }) => {
// Örnek: HTTP başlıklarından token alma
const token = req.headers.authorization || '';
return { token };
},
introspection: true,
playground: true,
});
await server.start();
server.applyMiddleware({ app, path: '/graphql' });
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(GraphQL API sunucusu çalışıyor: http://localhost:${PORT}/graphql);
console.log(GraphQL Playground'a erişmek için: http://localhost:${PORT}/graphql);
});
}
startApolloServer();
Bu değişikliklerle artık API’niz verileri MongoDB’den alıp kaydedecektir.
Gelişmiş Konular
Subscription’lar (Abonelikler)
Subscription’lar, GraphQL API’nizin istemcilere gerçek zamanlı olarak veri akışı sağlamasına olanak tanır. Bir olay (örn. yeni bir kitap eklendiğinde) meydana geldiğinde, sunucu bu olaya abone olan tüm istemcilere bir güncelleme gönderir. Subscription’lar genellikle WebSockets üzerinden çalışır.
Apollo Server, subscription’ları desteklemek için graphql-subscriptions paketiyle entegre edilebilir bir PubSub (Publish/Subscribe) mekanizması sunar.
Kurulum:
npm install graphql-subscriptions ws
Şema Güncelleme (src/schema.js):
// ...
const typeDefs = gql
# ... Book, Query, Mutation türleri ...
# 'Subscription' türü, gerçek zamanlı olayları tanımlar.
type Subscription {
newBook: Book! # Yeni bir kitap eklendiğinde tetiklenir.
}
;
// ...
Resolver Güncelleme (src/resolvers.js):
// ...
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub(); // Bir PubSub örneği oluştur
const BOOK_ADDED = 'BOOK_ADDED'; // Olay tipi için bir sabit tanımla
const resolvers = {
Query: { / ... / },
Mutation: {
addBook: async (parent, args, { dataSources }) => {
const newBook = await dataSources.booksAPI.createBook(args);
pubsub.publish(BOOK_ADDED, { newBook: newBook }); // Yeni kitabı yayınla
return newBook;
},
// ... diğer mutasyonlar ...
},
Subscription: {
newBook: {
subscribe: () => pubsub.asyncIterator([BOOK_ADDED]), // Abone olunan olayları dinle
},
},
};
// ...
Sunucu Güncelleme (index.js):
// ...
const { createServer } = require('http'); // HTTP sunucusu için
const { execute, subscribe } = require('graphql'); // Subscription'lar için
const { SubscriptionServer } = require('subscriptions-transport-ws'); // WebSocket sunucusu için
async function startApolloServer() {
// ... MongoDB bağlantısı ...
const app = express();
const httpServer = createServer(app); // HTTP sunucusunu Express uygulamasıyla oluştur
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
booksAPI: new BooksAPI(),
}),
context: ({ req, connection }) => {
// WebSocket bağlantıları için context yönetimi
if (connection) {
return connection.context;
}
const token = req.headers.authorization || '';
return { token };
},
introspection: true,
playground: true,
});
await server.start();
server.applyMiddleware({ app, path: '/graphql' });
// Subscription sunucusunu başlat
SubscriptionServer.create(
{
schema: server.schema, // Apollo Server'ın şemasını kullan
execute,
subscribe,
onConnect: (connectionParams, websocket, context) => {
console.log('Client connected for subscriptions');
// İsteğe bağlı: Burada kimlik doğrulama yapılabilir
return { currentUser: 'someUser' }; // Context'e eklenecek veri
},
onDisconnect: (websocket, context) => {
console.log('Client disconnected from subscriptions');
},
},
{
server: httpServer, // HTTP sunucusunu kullan
path: '/graphql', // Aynı path'i kullanabilir
}
);
const PORT = process.env.PORT || 4000;
// HTTP sunucusunu dinlemeye başla
httpServer.listen(PORT, () => {
console.log(GraphQL API sunucusu çalışıyor: http://localhost:${PORT}/graphql);
console.log(GraphQL Playground'a erişmek için: http://localhost:${PORT}/graphql);
console.log(Subscription endpoint: ws://localhost:${PORT}/graphql);
});
}
startApolloServer();
Şimdi GraphQL Playground’da “newBook” subscription’ına abone olabilir ve yeni bir kitap eklediğinizde anında bildirim alabilirsiniz.
Bağlam (Context) Kullanımı
context nesnesi, her GraphQL işlemi için oluşturulan ve tüm resolver’lar arasında paylaşılan bir nesnedir. Veritabanı bağlantıları, kimlik doğrulama bilgileri, kullanıcı oturumu veya paylaşılan servisler gibi her isteğe özgü verileri taşımak için idealdir. ApolloServer yapılandırmasında bir context fonksiyonu tanımlayarak bu nesneyi oluşturabilirsiniz:
// index.js içindeki context tanımı
context: ({ req }) => {
// HTTP başlıklarından token'ı al
const token = req.headers.authorization || '';
// Token'ı doğrula ve kullanıcı bilgilerini context'e ekle
// const user = await authService.getUserFromToken(token);
return { token /, user / };
},
Resolver’larınızda bu bilgilere üçüncü argüman olarak erişebilirsiniz:
// src/resolvers.js
const resolvers = {
Query: {
books: (parent, args, context) => {
// console.log(context.token); // Token'a erişim
// if (!context.user) throw new AuthenticationError('Giriş yapılmalı');
return context.dataSources.booksAPI.getAllBooks();
},
},
// ...
};
Kimlik Doğrulama ve Yetkilendirme (Authentication & Authorization)
Güvenli bir API için kimlik doğrulama (kullanıcının kim olduğunu belirleme) ve yetkilendirme (kullanıcının neye erişebileceğini belirleme) çok önemlidir.
- Kimlik Doğrulama: Genellikle
contextfonksiyonu içinde bir JWT (JSON Web Token) veya oturum tabanlı bir mekanizma kullanılarak yapılır. HTTP başlıklarından token alınır, doğrulanır ve kullanıcı bilgilericontextnesnesine eklenir. - Yetkilendirme:
- Resolver Seviyesinde: Her resolver’ın başında kullanıcının yetkisi olup olmadığını kontrol edebilirsiniz.
- Custom Direktifler: GraphQL SDL’de özel direktifler (örn.
@auth,@hasRole(role: "ADMIN")) tanımlayarak yetkilendirme mantığını şemaya taşıyabilirsiniz. Apollo Server, custom direktifleri uygulamak için bir API sunar.
Hata Yönetimi (Error Handling)
GraphQL, hataları belirli bir formatta döndürür. Apollo Server, hataları yakalamak ve özelleştirmek için formatError seçeneği sunar. Kendi özel hata sınıflarınızı (örn. AuthenticationError, ForbiddenError) oluşturabilir ve bunları formatError içinde işleyerek istemciye daha anlamlı hata mesajları gönderebilirsiniz.
// ApolloServer yapılandırmasında
const server = new ApolloServer({
// ...
formatError: (error) => {
// Hata mesajını ve detaylarını özelleştir
if (error.extensions.code === 'BAD_USER_INPUT') {
return new Error('Geçersiz giriş verisi.');
}
// Diğer hataları olduğu gibi döndür
return error;
},
// ...
});
Performans Optimizasyonu
- N+1 Problemi ve DataLoader: Bir sorguda birincil nesneleri (örn. kitaplar) ve ardından her birincil nesnenin ilişkili nesnelerini (örn. her kitabın yazarı) çektiğinizde, her ilişkili nesne için ayrı bir veritabanı sorgusu tetiklenir. Bu, “N+1 problemi” olarak bilinir. DataLoader, bu tür sorguları birleştirerek (batching) ve önbelleğe alarak N+1 problemini çözer ve veritabanı yükünü önemli ölçüde azaltır.
- Caching: Apollo Server, veri kaynakları seviyesinde veya manuel olarak resolver’larda önbellekleme uygulamanıza olanak tanır.
- Persisted Queries: İstemciler tarafından sıkça kullanılan sorguları sunucu tarafında önceden kaydederek, ağ trafiğini azaltabilir ve sorgu doğrulama maliyetini düşürebilirsiniz.
Proje Yapısı ve En İyi Uygulamalar
Modüler Şema Tasarımı
Büyük GraphQL API’lerinde tek bir şema dosyası yönetmek zorlaşabilir. Şemayı mantıksal modüllere (örn. users.graphql, products.graphql) ayırmak ve bunları birleştirmek (schema stitching veya daha modern yaklaşımlar olan Apollo Federation) iyi bir uygulamadır.
// src/index.js veya schema.js içinde
const { mergeTypeDefs, mergeResolvers } = require('@graphql-tools/merge');
const { loadFilesSync } = require('@graphql-tools/load-files');
const path = require('path');
const typesArray = loadFilesSync(path.join(__dirname, './schemas'), { extensions: ['graphql'] });
const resolversArray = loadFilesSync(path.join(__dirname, './resolvers'), { extensions: ['js'] });
const typeDefs = mergeTypeDefs(typesArray);
const resolvers = mergeResolvers(resolversArray);
module.exports = { typeDefs, resolvers };
Bu yapılandırma ile src/schemas ve src/resolvers klasörlerindeki tüm .graphql ve .js dosyalarını otomatik olarak birleştirebilirsiniz.
Klasör Yapısı
Temiz ve düzenli bir klasör yapısı, projenizin ölçeklenebilirliğini artırır:
graphql-api-server/
├── node_modules/
├── src/
│ ├── schemas/ # .graphql uzantılı şema tanımları (örn. book.graphql, user.graphql)
│ ├── resolvers/ # Resolver fonksiyonları (örn. bookResolver.js, userResolver.js)
│ ├── datasources/ # Veri kaynakları (örn. BooksAPI.js, UsersAPI.js)
│ ├── models/ # Veritabanı modelleri (örn. Book.js, User.js)
│ ├── utils/ # Yardımcı fonksiyonlar, kimlik doğrulama vb.
│ └── index.js # Ana uygulama dosyası
├── .env
├── package.json
└── README.md
Güvenlik İpuçları
- Derinlik ve Karmaşıklık Limitleri: Kötü niyetli kullanıcılar, çok derin veya karmaşık sorgular göndererek sunucunuzu yorabilir. Bu tür sorguları engellemek için Apollo Server’da sorgu derinliği ve karmaşıklık limitleri uygulayın.
- Veri Doğrulama: Input türlerini ve resolver’ları kullanarak gelen verileri her zaman doğrulayın.
- CSRF Koruması: Mutasyonlar için CSRF (Cross-Site Request Forgery) koruması uygulayın.
- Hata Mesajlarını Gizleme: Üretim ortamında ayrıntılı hata mesajlarını istemcilere göndermekten kaçının. Bu, saldırganlara sisteminiz hakkında bilgi verebilir.
- Rate Limiting: API’nize gelen istekleri sınırlayarak DDoS saldırılarını önleyin.
Test Etme
GraphQL API’nizi test etmek, uygulamanızın kalitesini ve güvenilirliğini sağlar. Unit testler (resolver’lar için), entegrasyon testleri (Apollo Server instance’ı ile) ve uçtan uca testler (gerçek bir istemci gibi davranarak) yazmak önemlidir. Jest ve Supertest gibi kütüphaneler bu süreçte yardımcı olabilir.
Sonuç
Bu makalede, Node.js ve Apollo Server kullanarak kapsamlı bir GraphQL API sunucusu kurulumunu ele aldık. GraphQL’in temel kavramlarından başlayarak, şema tanımlama, çözümleyicileri yazma, veritabanı entegrasyonu, abonelikler, kimlik doğrulama, hata yönetimi ve performans optimizasyonu gibi gelişmiş konulara değindik. Ayrıca, modüler bir proje yapısı ve en iyi güvenlik uygulamaları hakkında da bilgi verdik.
GraphQL, modern uygulama geliştiricilerine, veriye erişim konusunda benzersiz bir esneklik ve verimlilik sunar. Node.js’in hızlı geliştirme yetenekleri ve zengin paket ekosistemiyle birleştiğinde, güçlü ve ölçeklenebilir API’ler oluşturmak için mükemmel bir kombinasyon oluşturur. Bu rehber, kendi GraphQL API’nizi oluşturmaya başlamanız için sağlam bir temel sağlamalıdır. Daha fazla öğrenmek ve uygulamanızı geliştirmek için Apollo Client, GraphQL Federation ve daha gelişmiş performans optimizasyon tekniklerini araştırmaya devam etmeniz önerilir. GraphQL’in geleceği parlak ve Node.js ile birlikte, web geliştirme dünyasında önemli bir rol oynamaya devam edecektir.