İçeriğe geç
Node.js

Node.js GraphQL API: Apollo Server Rehberi

Tarık Tunç
Node.js GraphQL API: Apollo Server Rehberi

Node.js GraphQL API: Apollo Server Rehberi

GraphQL, API'lerden veri sorgulama ve manipüle etme için Facebook tarafından geliştirilen güçlü bir query dilidir. REST API'lerin over-fetching ve under-fetching sorunlarını çözer, istemcinin tam olarak ihtiyacı olan veriyi tek istekte almasını sağlar. Bu rehberde Apollo Server ile Node.js'te GraphQL API oluşturmayı adım adım inceleyeceğiz.

1. GraphQL vs REST

ÖzellikRESTGraphQL
EndpointBirden fazla (/users, /posts)Tek endpoint (/graphql)
Veri kontrolüSunucu belirlerİstemci belirler
Over-fetchingYaygınYok
VersiyonlamaURL bazlı (v1, v2)Schema evolüsyonu
Tip güvenliğiYok (varsayılan)Dahili tip sistemi
DökümantasyonHarici (Swagger)Otomatik (introspection)

2. Apollo Server Kurulumu

# Kurulum
npm install @apollo/server graphql
npm install --save-dev @graphql-tools/merge
// server.js
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');

// Type tanımlamaları
const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
    createdAt: String!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    comments: [Comment!]!
    published: Boolean!
    createdAt: String!
  }

  type Comment {
    id: ID!
    text: String!
    author: User!
    post: Post!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts(published: Boolean): [Post!]!
    post(id: ID!): Post
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    createPost(input: CreatePostInput!): Post!
    updatePost(id: ID!, input: UpdatePostInput!): Post!
    deletePost(id: ID!): Boolean!
    addComment(postId: ID!, text: String!): Comment!
  }

  input CreateUserInput {
    name: String!
    email: String!
  }

  input CreatePostInput {
    title: String!
    content: String!
    authorId: ID!
  }

  input UpdatePostInput {
    title: String
    content: String
    published: Boolean
  }
`;

// Resolver fonksiyonları
const resolvers = {
  Query: {
    users: (_, __, { dataSources }) => dataSources.userAPI.getAll(),
    user: (_, { id }, { dataSources }) => dataSources.userAPI.getById(id),
    posts: (_, { published }, { dataSources }) => dataSources.postAPI.getAll(published),
    post: (_, { id }, { dataSources }) => dataSources.postAPI.getById(id),
  },

  Mutation: {
    createUser: (_, { input }, { dataSources }) => dataSources.userAPI.create(input),
    createPost: (_, { input }, { dataSources }) => dataSources.postAPI.create(input),
    updatePost: (_, { id, input }, { dataSources }) => dataSources.postAPI.update(id, input),
    deletePost: (_, { id }, { dataSources }) => dataSources.postAPI.delete(id),
  },

  // Field resolver'lar — ilişkili veriler
  User: {
    posts: (user, _, { dataSources }) => dataSources.postAPI.getByAuthor(user.id),
  },

  Post: {
    author: (post, _, { dataSources }) => dataSources.userAPI.getById(post.authorId),
    comments: (post, _, { dataSources }) => dataSources.commentAPI.getByPost(post.id),
  },
};

async function startServer() {
  const server = new ApolloServer({ typeDefs, resolvers });

  const { url } = await startStandaloneServer(server, {
    listen: { port: 4000 },
    context: async ({ req }) => ({
      token: req.headers.authorization,
      dataSources: {
        userAPI: new UserAPI(),
        postAPI: new PostAPI(),
        commentAPI: new CommentAPI(),
      },
    }),
  });

  console.log(`GraphQL Server: ${url}`);
}

startServer();

3. Express ile Apollo Server

// Express entegrasyonu
const express = require('express');
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const cors = require('cors');

const app = express();

const server = new ApolloServer({ typeDefs, resolvers });
await server.start();

app.use(
  '/graphql',
  cors({ origin: ['https://www.example.com'] }),
  express.json(),
  expressMiddleware(server, {
    context: async ({ req }) => ({
      user: await authenticateUser(req.headers.authorization),
    }),
  })
);

app.listen(4000);

4. DataSource ve Veritabanı Entegrasyonu

// dataSources/UserAPI.js
const { PrismaClient } = require('@prisma/client');

class UserAPI {
  constructor() {
    this.prisma = new PrismaClient();
  }

  async getAll() {
    return this.prisma.user.findMany({ orderBy: { createdAt: 'desc' } });
  }

  async getById(id) {
    return this.prisma.user.findUnique({ where: { id: parseInt(id) } });
  }

  async create(input) {
    return this.prisma.user.create({ data: input });
  }
}

// dataSources/PostAPI.js
class PostAPI {
  constructor() {
    this.prisma = new PrismaClient();
  }

  async getAll(published) {
    const where = published !== undefined ? { published } : {};
    return this.prisma.post.findMany({ where, orderBy: { createdAt: 'desc' } });
  }

  async getByAuthor(authorId) {
    return this.prisma.post.findMany({ where: { authorId: parseInt(authorId) } });
  }

  async create(input) {
    return this.prisma.post.create({
      data: {
        title: input.title,
        content: input.content,
        authorId: parseInt(input.authorId),
      },
    });
  }

  async update(id, input) {
    return this.prisma.post.update({
      where: { id: parseInt(id) },
      data: input,
    });
  }

  async delete(id) {
    await this.prisma.post.delete({ where: { id: parseInt(id) } });
    return true;
  }
}

5. Authentication ve Authorization

// Auth middleware
const jwt = require('jsonwebtoken');

async function authenticateUser(token) {
  if (!token) return null;

  try {
    const decoded = jwt.verify(token.replace('Bearer ', ''), process.env.JWT_SECRET);
    return decoded;
  } catch {
    return null;
  }
}

// Auth directive veya resolver guard
function requireAuth(resolverFn) {
  return (parent, args, context, info) => {
    if (!context.user) {
      throw new Error('Yetkilendirme gerekli');
    }
    return resolverFn(parent, args, context, info);
  };
}

// Kullanım
const resolvers = {
  Mutation: {
    createPost: requireAuth((_, { input }, { user, dataSources }) => {
      return dataSources.postAPI.create({ ...input, authorId: user.id });
    }),
  },
};

6. N+1 Problemi ve DataLoader

GraphQL'de en yaygın performans sorunu N+1 problemidir. DataLoader ile çözülür.

# Kurulum
npm install dataloader
const DataLoader = require('dataloader');

// DataLoader oluştur
function createLoaders() {
  return {
    userLoader: new DataLoader(async (userIds) => {
      const users = await prisma.user.findMany({
        where: { id: { in: userIds.map(Number) } },
      });
      // ID sırasıyla eşleştir
      const userMap = new Map(users.map(u => [u.id, u]));
      return userIds.map(id => userMap.get(Number(id)) || null);
    }),
  };
}

// Context'e ekle
context: async ({ req }) => ({
  user: await authenticateUser(req.headers.authorization),
  loaders: createLoaders(),
}),

// Resolver'da kullan
Post: {
  author: (post, _, { loaders }) => loaders.userLoader.load(post.authorId),
},
İpucu: DataLoader, her istek için yeni oluşturulmalıdır. Böylece cache istekler arasında sızmaz ve her istek izole kalır.

7. GraphQL Sorgu Örnekleri

# Temel sorgu
query {
  users {
    id
    name
    email
  }
}

# İç içe sorgu
query {
  post(id: "1") {
    title
    content
    author {
      name
    }
    comments {
      text
      author {
        name
      }
    }
  }
}

# Mutation
mutation {
  createPost(input: {
    title: "GraphQL Rehberi"
    content: "GraphQL harika!"
    authorId: "1"
  }) {
    id
    title
    author {
      name
    }
  }
}

# Variables ile
mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
  }
}

8. Error Handling

const { GraphQLError } = require('graphql');

// Custom error
throw new GraphQLError('Kullanıcı bulunamadı', {
  extensions: {
    code: 'USER_NOT_FOUND',
    http: { status: 404 },
  },
});

// Validation error
throw new GraphQLError('Geçersiz email formatı', {
  extensions: {
    code: 'VALIDATION_ERROR',
    field: 'email',
  },
});
Uyarı: Production'da introspection ve landing page özelliklerini kapatın. Bu, API şemanızın dışarıdan keşfedilmesini engeller.

Sonuç

Apollo Server ile Node.js'te GraphQL API oluşturmak, istemci odaklı, tip güvenli ve esnek bir API deneyimi sunar. Schema-first yaklaşım ile tip tanımlamalarınızı netleştirin, DataLoader ile N+1 problemini çözün ve authentication/authorization katmanlarını ekleyin. GraphQL'in tek endpoint yapısı, API versiyonlama sorunlarını ortadan kaldırır. Production'da introspection'ı kapatmayı, query depth limiting ve cost analysis uygulamayı unutmayın.

Kaynaklar

İlgili Yazılar