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: false yapın
  • Stack trace gizlenmeli: includeStacktraceInErrorResponses: false ayarı production’da mutlaka kapalı olmalı
  • Rate limiting: express-rate-limit veya benzeri bir middleware ekleyin
  • Query depth limiting: Çok derin nested sorgular sunucunuzu yorabilir, graphql-depth-limit paketi işinize yarar
  • Query complexity: Karmaşık sorgulara karşı graphql-query-complexity ile koruma ekleyin
  • CORS ayarları: cors() yerine izin verilen origin listesini açıkça belirtin
  • Environment variables: Port, introspection flag ve diğer ayarlar .env dosyası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.

Bir yanıt yazın

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