Node.js Üzerinde Apollo Server Kurulumu ve İlk GraphQL API
GraphQL öğrenmeye karar verdiğinizde karşınıza çıkan ilk isim genellikle Apollo Server oluyor. Ve bu şaşırtıcı değil çünkü Apollo, Node.js ekosisteminde GraphQL sunucusu kurmanın en yaygın ve en olgun yollarından biri haline geldi. Bu yazıda sıfırdan başlayıp çalışan bir Apollo Server kurulumu yapacağız, şema tanımlayacağız, resolver yazacağız ve gerçek dünyada kullanabileceğiniz bir API iskeletine ulaşacağız.
Neden Apollo Server?
REST API’lere alışkın bir sysadmin veya backend developer olarak GraphQL’e geçişte en çok sorulan soru şu oluyor: “Neden Apollo?” Çünkü Express, Fastify veya başka bir framework’le de GraphQL sunabilirsiniz. Ancak Apollo Server şu avantajları beraberinde getiriyor:
- Apollo Sandbox: Geliştirme sırasında tarayıcı üzerinden sorgularınızı test edebileceğiniz yerleşik bir araç geliyor
- Schema-first yaklaşım: SDL (Schema Definition Language) ile şemanızı açıkça tanımlıyorsunuz
- Middleware uyumluluğu: Express, Koa, Fastify gibi mevcut framework’lerle kolayca entegre oluyor
- Plugin sistemi: Loglama, cache, tracing gibi özellikleri plugin olarak ekleyebiliyorsunuz
- Aktif ekosistem: Dokümantasyonu güçlü, community büyük, sorunlarınıza çözüm bulmak kolay
Ortam Hazırlığı
Başlamadan önce sistemimizde Node.js’in kurulu olduğundan emin olalım. Apollo Server 4 için Node.js 14.x ve üzeri yeterli ama ben her zaman LTS sürümünü öneririm.
node --version
npm --version
# Eğer Node.js yoksa Node Version Manager ile kuralım
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install --lts
nvm use --lts
Şimdi projemizi oluşturalım:
mkdir apollo-blog-api
cd apollo-blog-api
npm init -y
package.json dosyasında bir değişiklik yapmamız gerekiyor. Apollo Server 4, ES Modules kullanıyor ve bunu açıkça belirtmemiz lazım:
# package.json dosyasını düzenleyelim
# "type": "module" satırını ekleyeceğiz
cat package.json
package.json dosyanızı açıp şu şekilde güncelleyin:
npm pkg set type="module"
npm pkg set scripts.start="node src/index.js"
npm pkg set scripts.dev="node --watch src/index.js"
--watch flag’i Node.js 18 ile geldi ve dosya değişikliklerinde sunucuyu otomatik yeniden başlatıyor. Nodemon’a alternatif olarak kullanabilirsiniz.
Bağımlılıkları Yükleyelim
npm install @apollo/server graphql
# Geliştirme bağımlılıkları
npm install --save-dev @types/node
Burada sadece iki temel paket var: @apollo/server ve graphql. GraphQL.js, Apollo Server’ın çekirdeğini oluşturuyor. Gereksiz bağımlılıktan kaçınmak adına başlangıçta sade tutuyoruz.
Proje Yapısını Oluşturalım
Gerçek dünya projelerinde kod organizasyonu kritik önem taşıyor. Baştan doğru bir yapı kurmak sonradan büyük acıları önlüyor:
mkdir -p src/{schema,resolvers,data}
touch src/index.js
touch src/schema/typeDefs.js
touch src/resolvers/index.js
touch src/data/db.js
Bu yapıyla şemalarımızı, resolver’larımızı ve veri katmanımızı birbirinden ayırıyoruz. Küçük bir projede bile bu ayrımı yapmak iyi bir alışkanlık.
Mock Veri Katmanı
Gerçek bir veritabanına bağlanmadan önce, bir blog API’si için mock veri oluşturalım. Bu sayede GraphQL mantığına odaklanabileceğiz:
cat > src/data/db.js << 'EOF'
export const users = [
{
id: "1",
username: "ahmet_yilmaz",
email: "[email protected]",
role: "ADMIN",
createdAt: "2024-01-15T08:00:00Z"
},
{
id: "2",
username: "fatma_kaya",
email: "[email protected]",
role: "AUTHOR",
createdAt: "2024-02-20T10:30:00Z"
},
{
id: "3",
username: "mehmet_demir",
email: "[email protected]",
role: "READER",
createdAt: "2024-03-05T14:15:00Z"
}
];
export const posts = [
{
id: "1",
title: "Apollo Server ile GraphQL API Kurulumu",
content: "Bu yazıda Apollo Server kurulumunu ele alıyoruz...",
published: true,
authorId: "1",
tags: ["graphql", "nodejs", "apollo"],
viewCount: 1250,
createdAt: "2024-04-10T09:00:00Z"
},
{
id: "2",
title: "PostgreSQL Optimizasyon Teknikleri",
content: "Veritabanı performansını artırmak için...",
published: true,
authorId: "2",
tags: ["postgresql", "database", "performance"],
viewCount: 830,
createdAt: "2024-04-18T11:00:00Z"
},
{
id: "3",
title: "Docker ile Microservice Mimarisi",
content: "Container tabanlı servis mimarisinde...",
published: false,
authorId: "1",
tags: ["docker", "microservices", "devops"],
viewCount: 0,
createdAt: "2024-05-01T16:00:00Z"
}
];
export const comments = [
{
id: "1",
content: "Çok faydalı bir yazı, teşekkürler!",
postId: "1",
authorId: "2",
createdAt: "2024-04-11T10:00:00Z"
},
{
id: "2",
content: "PostgreSQL kısmını genişletebilir misiniz?",
postId: "2",
authorId: "3",
createdAt: "2024-04-19T09:30:00Z"
}
];
EOF
Type Definitions (Şema Tanımı)
GraphQL’in kalbi şema. Her şey burada başlıyor. Şemamızı SDL ile tanımlayalım:
cat > src/schema/typeDefs.js << 'EOF'
export const typeDefs = `#graphql
enum Role {
ADMIN
AUTHOR
READER
}
type User {
id: ID!
username: String!
email: String!
role: Role!
posts: [Post!]!
createdAt: String!
}
type Post {
id: ID!
title: String!
content: String!
published: Boolean!
author: User!
tags: [String!]!
viewCount: Int!
comments: [Comment!]!
createdAt: String!
}
type Comment {
id: ID!
content: String!
post: Post!
author: User!
createdAt: String!
}
type Query {
# Kullanıcı sorguları
users: [User!]!
user(id: ID!): User
# Yazı sorguları
posts(published: Boolean): [Post!]!
post(id: ID!): Post
# Yorum sorguları
comments(postId: ID!): [Comment!]!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post
deletePost(id: ID!): Boolean!
publishPost(id: ID!): Post
addComment(input: AddCommentInput!): Comment!
}
input CreatePostInput {
title: String!
content: String!
authorId: ID!
tags: [String!]
}
input UpdatePostInput {
title: String
content: String
tags: [String!]
}
input AddCommentInput {
content: String!
postId: ID!
authorId: ID!
}
`;
EOF
Şema tasarımında dikkat ettiğim birkaç nokta var. ! işareti null olamaz anlamına geliyor ve mümkün olduğunca kullanmak tip güvenliğini artırıyor. Input type’lar mutation’larda argümanları organize etmek için harika bir yapı sunuyor. Düz argüman listesi yerine input type kullanmak hem okunabilirliği hem de genişletilebilirliği artırıyor.
Resolver’ları Yazalım
Şemayı tanımladık, şimdi her field için veriyi nasıl çekeceğimizi belirtelim:
cat > src/resolvers/index.js << 'EOF'
import { users, posts, comments } from '../data/db.js';
// Basit ID üretici
const generateId = (arr) => String(Math.max(...arr.map(i => Number(i.id))) + 1);
export const resolvers = {
Query: {
users: () => users,
user: (_, { id }) => {
const user = users.find(u => u.id === id);
if (!user) return null;
return user;
},
posts: (_, { published }) => {
if (published !== undefined && published !== null) {
return posts.filter(p => p.published === published);
}
return posts;
},
post: (_, { id }) => posts.find(p => p.id === id) || null,
comments: (_, { postId }) => comments.filter(c => c.postId === postId),
},
Mutation: {
createPost: (_, { input }) => {
const newPost = {
id: generateId(posts),
title: input.title,
content: input.content,
published: false,
authorId: input.authorId,
tags: input.tags || [],
viewCount: 0,
createdAt: new Date().toISOString()
};
posts.push(newPost);
return newPost;
},
updatePost: (_, { id, input }) => {
const postIndex = posts.findIndex(p => p.id === id);
if (postIndex === -1) return null;
const updated = {
...posts[postIndex],
...Object.fromEntries(
Object.entries(input).filter(([_, v]) => v !== undefined)
)
};
posts[postIndex] = updated;
return updated;
},
deletePost: (_, { id }) => {
const postIndex = posts.findIndex(p => p.id === id);
if (postIndex === -1) return false;
posts.splice(postIndex, 1);
return true;
},
publishPost: (_, { id }) => {
const post = posts.find(p => p.id === id);
if (!post) return null;
post.published = true;
return post;
},
addComment: (_, { input }) => {
const newComment = {
id: generateId(comments),
content: input.content,
postId: input.postId,
authorId: input.authorId,
createdAt: new Date().toISOString()
};
comments.push(newComment);
return newComment;
}
},
// İlişkisel field resolver'lar
User: {
posts: (parent) => posts.filter(p => p.authorId === parent.id)
},
Post: {
author: (parent) => users.find(u => u.id === parent.authorId),
comments: (parent) => comments.filter(c => c.postId === parent.id)
},
Comment: {
author: (parent) => users.find(u => u.id === parent.authorId),
post: (parent) => posts.find(p => p.id === parent.postId)
}
};
EOF
Burada önemli bir kavram var: parent resolver (ya da root değeri olarak da bilinen parent parametresi). Örneğin Post.author resolver’ında parent o anki Post nesnesini temsil ediyor. Bu sayede ilişkisel verileri çekebiliyoruz. Gerçek bir projede bu noktada veritabanı sorgusu yaparsınız.
Ana Sunucu Dosyası
Her şeyi bir araya getirelim:
cat > src/index.js << 'EOF'
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { typeDefs } from './schema/typeDefs.js';
import { resolvers } from './resolvers/index.js';
const server = new ApolloServer({
typeDefs,
resolvers,
// Geliştirme ortamında hata detaylarını göster
includeStacktraceInErrorResponses: process.env.NODE_ENV !== 'production',
// Introspection - production'da kapatmak güvenlik açısından önerilir
introspection: process.env.NODE_ENV !== 'production',
});
const PORT = process.env.PORT || 4000;
const { url } = await startStandaloneServer(server, {
listen: { port: Number(PORT) },
context: async ({ req }) => {
// İlerleyen aşamalarda authentication buraya gelecek
const token = req.headers.authorization || '';
return {
token,
// currentUser: getUserFromToken(token)
};
},
});
console.log(`Apollo Server hazır: ${url}`);
console.log(`Sandbox: ${url} adresini tarayıcıda açın`);
EOF
Sunucuyu Başlatalım ve Test Edelim
npm run dev
Terminal çıktısında şunu görmeniz gerekiyor:
Apollo Server hazır: http://localhost:4000/
Sandbox: http://localhost:4000/ adresini tarayıcıda açın
Tarayıcıda http://localhost:4000 adresini açtığınızda Apollo Sandbox karşınıza çıkıyor. Bu araçla şemanızı keşfedebilir ve sorgularınızı test edebilirsiniz.
Komut satırından da test edebilirsiniz:
# Tüm kullanıcıları çek
curl -X POST http://localhost:4000
-H "Content-Type: application/json"
-d '{
"query": "{ users { id username email role } }"
}'
# Yayınlanmış yazıları çek
curl -X POST http://localhost:4000
-H "Content-Type: application/json"
-d '{
"query": "{ posts(published: true) { id title author { username } tags viewCount } }"
}'
# Yeni yazı oluştur
curl -X POST http://localhost:4000
-H "Content-Type: application/json"
-d '{
"query": "mutation { createPost(input: { title: "Test Yazısı", content: "İçerik buraya", authorId: "1", tags: ["test"] }) { id title published createdAt } }"
}'
Context ile Veri Paylaşımı
Context, tüm resolver’lara erişilebilen paylaşılan bir nesne. Authentication bilgileri, veritabanı bağlantısı veya logger gibi şeyleri buraya koyarsınız. Şu an token’ı context’e aktarıyoruz, ilerleyen aşamalarda bunu genişletebilirsiniz:
# Context kullanım örneği - resolvers/index.js içinde
# Query resolver'ınızı şöyle güncelleyebilirsiniz:
# posts: (_, { published }, context) => {
# console.log('İstek token:', context.token);
# // authentication kontrolü yapılabilir
# ...
# }
Hata Yönetimi
GraphQL’de hata yönetimi REST’ten biraz farklı. Apollo Server, GraphQLError sınıfını sunuyor:
cat > src/resolvers/index.js << 'EOF'
import { GraphQLError } from 'graphql';
import { users, posts, comments } from '../data/db.js';
// Mevcut kodunuza şu şekilde hata yönetimi ekleyebilirsiniz:
// user: (_, { id }) => {
// const user = users.find(u => u.id === id);
// if (!user) {
// throw new GraphQLError(`${id} ID'li kullanıcı bulunamadı`, {
// extensions: {
// code: 'USER_NOT_FOUND',
// http: { status: 404 }
// }
// });
// }
// return user;
// },
EOF
extensions.code alanı client tarafında hata tipini anlamak için kullanılıyor. USER_NOT_FOUND, UNAUTHENTICATED, FORBIDDEN gibi kodlar tanımlamak iyi bir pratik.
Express ile Entegrasyon
Production ortamında genellikle Apollo Server’ı Express ile birlikte kullanmak istersiniz. Rate limiting, custom middleware veya REST endpoint’leri ile birlikte çalışması gerektiğinde bu kaçınılmaz oluyor:
npm install express cors body-parser
npm install --save-dev @types/express @types/cors
cat > src/server-express.js << 'EOF'
import express from 'express';
import cors from 'cors';
import { json } from 'body-parser';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { typeDefs } from './schema/typeDefs.js';
import { resolvers } from './resolvers/index.js';
const app = express();
const PORT = process.env.PORT || 4000;
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
});
await server.start();
app.use('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.use(
'/graphql',
cors(),
json(),
expressMiddleware(server, {
context: async ({ req }) => ({
token: req.headers.authorization || '',
userAgent: req.headers['user-agent']
})
})
);
app.listen(PORT, () => {
console.log(`Express + Apollo Server ayakta: http://localhost:${PORT}/graphql`);
console.log(`Health check: http://localhost:${PORT}/health`);
});
EOF
Bu yapıyla /health endpoint’iniz dümdüz bir REST route olarak çalışırken /graphql Apollo tarafından yönetiliyor. Load balancer health check’leri için bu çok işe yarıyor.
Production Öncesi Kontrol Listesi
Sunucunuzu production’a almadan önce şunlara dikkat edin:
- Introspection kapalı olmalı: Şema yapınız dışarıya sızmış olur,
introspection: falseyapın - Stack trace gizlenmeli:
includeStacktraceInErrorResponses: falseayarı production’da mutlaka kapalı olmalı - Rate limiting:
express-rate-limitveya benzeri bir middleware ekleyin - Query depth limiting: Çok derin nested sorgular sunucunuzu yorabilir,
graphql-depth-limitpaketi işinize yarar - Query complexity: Karmaşık sorgulara karşı
graphql-query-complexityile koruma ekleyin - CORS ayarları:
cors()yerine izin verilen origin listesini açıkça belirtin - Environment variables: Port, introspection flag ve diğer ayarlar
.envdosyasından gelmeli
# .env dosyası oluşturun
cat > .env << 'EOF'
NODE_ENV=development
PORT=4000
EOF
# dotenv paketini ekleyin
npm install dotenv
Sonuç
Bu yazıda sıfırdan başlayıp çalışan bir Apollo Server kurulumu yaptık. Şema tanımladık, resolver yazdık, ilişkisel verileri çözdük ve Express entegrasyonuna baktık. Artık elinizde gerçek bir projeye taşıyabileceğiniz sağlam bir iskelet var.
Bir sonraki adım olarak veritabanı entegrasyonunu ele alabiliriz. Mock veri yerine PostgreSQL veya MongoDB bağlamak, Prisma ORM kullanmak, DataLoader ile N+1 sorununu çözmek ve JWT tabanlı authentication eklemek konularına geçmek mantıklı olur. GraphQL’in REST’e göre gerçek avantajları da tam o noktalarda kendini gösteriyor: İstemci tam olarak ne istediğini söylüyor, gereksiz veri taşınmıyor ve şema bir sözleşme olarak görev yapıyor.
Apollo Server kurulumu aslında en kolay kısım. Asıl mesele şemanızı doğru tasarlamak ve resolver’larınızı performanslı yazmak. Ama bu temeli sağlam atmadan ilerisi gelmiyor, bu yüzden buradaki adımları iyice sindirestmenizi öneririm.
