TypeScript ile Apollo Server Geliştirme Ortamı Kurulumu

Modern web geliştirme dünyasında GraphQL giderek daha fazla tercih edilen bir API tasarım yaklaşımı haline geldi. REST’in getirdiği over-fetching ve under-fetching sorunlarına karşı güçlü bir alternatif sunan GraphQL, Apollo Server ile birleştiğinde gerçekten etkileyici bir geliştirme deneyimi sunuyor. TypeScript’i de bu denkleme eklediğinde, tip güvenliği sayesinde hem geliştirme sürecinde hem de production ortamında çok daha az hatayla karşılaşıyorsun. Bu yazıda sıfırdan başlayarak TypeScript tabanlı bir Apollo Server geliştirme ortamı nasıl kurulur, adım adım ele alacağız.

Neden TypeScript ile Apollo Server?

Bir sysadmin veya backend geliştirici olarak bunu kendinize sormak gayet mantıklı. GraphQL şemaları zaten kendi içinde bir tür sistemi barındırıyor. O zaman neden TypeScript ekleyelim?

Cevap şu: GraphQL şemanız ile TypeScript tipleriniz arasında senkronizasyon sağladığınızda, resolver’larınızda yanlış tip döndürme, yanlış alan adı kullanma gibi hatalar derleme anında yakalanıyor. Production’da patlayan bir API yerine geliştirici ekranında kırmızı underline tercih edilir her zaman.

Pratik faydaları özetlersek:

  • Tip güvenliği: Resolver dönüş tipleri, argument tipleri otomatik kontrol edilir
  • IDE desteği: VS Code ve benzeri editörlerde otomatik tamamlama neredeyse mükemmel çalışır
  • Refactoring kolaylığı: Şema değişikliklerinin etkisi anında görülür
  • Daha az runtime hatası: Özellikle büyük ekiplerde bu fark gerçekten hissedilir
  • Dökümantasyon etkisi: Tipler kendi başına bir dökümantasyon görevi görür

Gereksinimler ve Ortam Hazırlığı

Başlamadan önce sisteminizde aşağıdakilerin kurulu olduğundan emin olun:

  • Node.js 18+ (LTS önerilir)
  • npm veya yarn
  • TypeScript bilgisi temel seviye yeterli

Proje dizinimizi oluşturup başlayalım:

mkdir apollo-ts-server
cd apollo-ts-server
npm init -y

Şimdi gerekli paketleri yükleyelim. Üretim bağımlılıkları ve geliştirme bağımlılıklarını ayrı tutmak iyi bir alışkanlık:

# Production dependencies
npm install @apollo/server graphql

# Development dependencies
npm install -D typescript ts-node nodemon @types/node tsx

Burada tsx paketini özellikle belirtmek gerekiyor. ts-node’a göre çok daha hızlı çalışıyor ve modern TypeScript projelerinde neredeyse standart haline geldi.

TypeScript Yapılandırması

TypeScript’in nasıl davranacağını belirleyen tsconfig.json dosyasını oluşturalım. Apollo Server 4 ile çalışırken bazı ayarlar kritik:

npx tsc --init

Oluşturulan dosyayı aşağıdaki gibi düzenleyin:

cat > tsconfig.json << 'EOF'
{
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist",
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2020"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
EOF

"module": "NodeNext" ayarına dikkat edin. Apollo Server 4, ES Modules yapısına geçti. Bu yüzden package.json dosyanıza da "type": "module" eklemeniz gerekiyor:

# package.json'ı güncelleyelim
npm pkg set type="module"
npm pkg set main="dist/index.js"
npm pkg set scripts.build="tsc"
npm pkg set scripts.start="node dist/index.js"
npm pkg set scripts.dev="tsx watch src/index.ts"
npm pkg set scripts.dev:nodemon="nodemon --exec tsx src/index.ts --ext ts"

Temel Server Yapısı

Proje yapısını organize etmek uzun vadede çok önemli. Şu dizin yapısını kullanacağız:

mkdir -p src/{schema,resolvers,types,datasources,utils}
touch src/index.ts
touch src/schema/index.ts
touch src/resolvers/index.ts
touch src/types/index.ts

Proje ağacı şu şekilde görünmeli:

tree src/
# src/
# ├── datasources/
# ├── index.ts
# ├── resolvers/
# │   └── index.ts
# ├── schema/
# │   └── index.ts
# ├── types/
# │   └── index.ts
# └── utils/

Şimdi ana server dosyasını oluşturalım. src/index.ts:

cat > src/index.ts << 'EOF'
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { typeDefs } from './schema/index.js';
import { resolvers } from './resolvers/index.js';

async function startServer(): Promise<void> {
  const server = new ApolloServer({
    typeDefs,
    resolvers,
    introspection: process.env.NODE_ENV !== 'production',
    formatError: (formattedError, error) => {
      // Production'da hata detaylarını gizle
      if (process.env.NODE_ENV === 'production') {
        console.error('GraphQL Error:', error);
        return {
          message: formattedError.message,
          code: formattedError.extensions?.code,
        };
      }
      return formattedError;
    },
  });

  const { url } = await startStandaloneServer(server, {
    listen: { port: parseInt(process.env.PORT || '4000') },
    context: async ({ req }) => {
      // Context factory - her request için çalışır
      const token = req.headers.authorization || '';
      return {
        token,
        timestamp: new Date().toISOString(),
      };
    },
  });

  console.log(`Apollo Server hazır: ${url}`);
  console.log(`GraphQL Playground: ${url}`);
}

startServer().catch((err) => {
  console.error('Server başlatılamadı:', err);
  process.exit(1);
});
EOF

Şema Tanımlamaları

GraphQL şemasını TypeScript ile birlikte kullanmak için tip tanımlamalarını da oluşturacağız. Önce şemayı yazalım, src/schema/index.ts:

cat > src/schema/index.ts << 'EOF'
export const typeDefs = `#graphql
  type User {
    id: ID!
    username: String!
    email: String!
    role: UserRole!
    createdAt: String!
    posts: [Post!]!
  }

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

  enum UserRole {
    ADMIN
    EDITOR
    VIEWER
  }

  type PaginatedPosts {
    posts: [Post!]!
    totalCount: Int!
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
  }

  input CreateUserInput {
    username: String!
    email: String!
    role: UserRole = VIEWER
  }

  input CreatePostInput {
    title: String!
    content: String!
    authorId: ID!
    tags: [String!] = []
  }

  input PostsFilterInput {
    published: Boolean
    authorId: ID
    tags: [String!]
    limit: Int = 10
    offset: Int = 0
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts(filter: PostsFilterInput): PaginatedPosts!
    post(id: ID!): Post
    me: User
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    createPost(input: CreatePostInput!): Post!
    publishPost(id: ID!): Post!
    deletePost(id: ID!): Boolean!
  }
EOF

TypeScript Tiplerini Tanımlama

Şema ile TypeScript tiplerini senkronize tutmak için src/types/index.ts dosyasını oluşturalım:

cat > src/types/index.ts << 'EOF'
export enum UserRole {
  ADMIN = 'ADMIN',
  EDITOR = 'EDITOR',
  VIEWER = 'VIEWER',
}

export interface User {
  id: string;
  username: string;
  email: string;
  role: UserRole;
  createdAt: string;
}

export interface Post {
  id: string;
  title: string;
  content: string;
  published: boolean;
  authorId: string;
  tags: string[];
  createdAt: string;
  updatedAt: string;
}

export interface CreateUserInput {
  username: string;
  email: string;
  role?: UserRole;
}

export interface CreatePostInput {
  title: string;
  content: string;
  authorId: string;
  tags?: string[];
}

export interface PostsFilterInput {
  published?: boolean;
  authorId?: string;
  tags?: string[];
  limit?: number;
  offset?: number;
}

export interface PaginatedPosts {
  posts: Post[];
  totalCount: number;
  hasNextPage: boolean;
  hasPreviousPage: boolean;
}

export interface GraphQLContext {
  token: string;
  timestamp: string;
  currentUser?: User;
}
EOF

Büyük projelerde şema tiplerini manuel yazmak yerine graphql-codegen kullanmak çok daha verimli. Ama başlangıç için bu yaklaşım gayet yeterli ve neyin ne işe yaradığını anlamak açısından daha öğretici.

Resolver’ları Yazma

Resolver’lar GraphQL’in kalbi. TypeScript ile yazarken tip güvenliğini tam anlamıyla burada hissedeceksiniz. src/resolvers/index.ts:

cat > src/resolvers/index.ts << 'EOF'
import { GraphQLError } from 'graphql';
import type {
  User,
  Post,
  CreateUserInput,
  CreatePostInput,
  PostsFilterInput,
  GraphQLContext,
  PaginatedPosts,
} from '../types/index.js';

// Geçici in-memory veri deposu (gerçek projede DB kullanın)
const users: User[] = [
  {
    id: '1',
    username: 'admin',
    email: '[email protected]',
    role: 'ADMIN' as any,
    createdAt: new Date().toISOString(),
  },
  {
    id: '2',
    username: 'editor1',
    email: '[email protected]',
    role: 'EDITOR' as any,
    createdAt: new Date().toISOString(),
  },
];

const posts: Post[] = [
  {
    id: '1',
    title: 'TypeScript ile Apollo Server',
    content: 'GraphQL geliştirmeyi TypeScript ile nasıl yaparsınız...',
    published: true,
    authorId: '1',
    tags: ['typescript', 'graphql', 'apollo'],
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
  },
];

export const resolvers = {
  Query: {
    users: (_parent: unknown, _args: unknown, context: GraphQLContext) => {
      // Basit yetkilendirme kontrolü
      if (!context.token) {
        throw new GraphQLError('Yetkilendirme gerekli', {
          extensions: { code: 'UNAUTHORIZED' },
        });
      }
      return users;
    },

    user: (_parent: unknown, { id }: { id: string }) => {
      const user = users.find((u) => u.id === id);
      if (!user) {
        throw new GraphQLError(`${id} ID'li kullanici bulunamadi`, {
          extensions: { code: 'NOT_FOUND' },
        });
      }
      return user;
    },

    posts: (
      _parent: unknown,
      { filter }: { filter?: PostsFilterInput }
    ): PaginatedPosts => {
      let filteredPosts = [...posts];

      if (filter?.published !== undefined) {
        filteredPosts = filteredPosts.filter(
          (p) => p.published === filter.published
        );
      }

      if (filter?.authorId) {
        filteredPosts = filteredPosts.filter(
          (p) => p.authorId === filter.authorId
        );
      }

      if (filter?.tags && filter.tags.length > 0) {
        filteredPosts = filteredPosts.filter((p) =>
          filter.tags!.some((tag) => p.tags.includes(tag))
        );
      }

      const limit = filter?.limit ?? 10;
      const offset = filter?.offset ?? 0;
      const totalCount = filteredPosts.length;
      const paginatedPosts = filteredPosts.slice(offset, offset + limit);

      return {
        posts: paginatedPosts,
        totalCount,
        hasNextPage: offset + limit < totalCount,
        hasPreviousPage: offset > 0,
      };
    },

    post: (_parent: unknown, { id }: { id: string }) => {
      return posts.find((p) => p.id === id) || null;
    },

    me: (_parent: unknown, _args: unknown, context: GraphQLContext) => {
      if (!context.currentUser) {
        return null;
      }
      return context.currentUser;
    },
  },

  Mutation: {
    createUser: (
      _parent: unknown,
      { input }: { input: CreateUserInput }
    ): User => {
      const existingUser = users.find((u) => u.email === input.email);
      if (existingUser) {
        throw new GraphQLError('Bu email zaten kayitli', {
          extensions: { code: 'DUPLICATE_EMAIL' },
        });
      }

      const newUser: User = {
        id: String(users.length + 1),
        username: input.username,
        email: input.email,
        role: input.role ?? ('VIEWER' as any),
        createdAt: new Date().toISOString(),
      };

      users.push(newUser);
      return newUser;
    },

    createPost: (
      _parent: unknown,
      { input }: { input: CreatePostInput }
    ): Post => {
      const author = users.find((u) => u.id === input.authorId);
      if (!author) {
        throw new GraphQLError('Yazar bulunamadi', {
          extensions: { code: 'NOT_FOUND' },
        });
      }

      const newPost: Post = {
        id: String(posts.length + 1),
        title: input.title,
        content: input.content,
        published: false,
        authorId: input.authorId,
        tags: input.tags ?? [],
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      };

      posts.push(newPost);
      return newPost;
    },

    publishPost: (_parent: unknown, { id }: { id: string }): Post => {
      const postIndex = posts.findIndex((p) => p.id === id);
      if (postIndex === -1) {
        throw new GraphQLError('Post bulunamadi', {
          extensions: { code: 'NOT_FOUND' },
        });
      }

      posts[postIndex] = {
        ...posts[postIndex],
        published: true,
        updatedAt: new Date().toISOString(),
      };

      return posts[postIndex];
    },

    deletePost: (_parent: unknown, { id }: { id: string }): boolean => {
      const postIndex = posts.findIndex((p) => p.id === id);
      if (postIndex === -1) {
        throw new GraphQLError('Post bulunamadi', {
          extensions: { code: 'NOT_FOUND' },
        });
      }

      posts.splice(postIndex, 1);
      return true;
    },
  },

  // Field Resolvers
  User: {
    posts: (parent: User) => {
      return posts.filter((p) => p.authorId === parent.id);
    },
  },

  Post: {
    author: (parent: Post) => {
      return users.find((u) => u.id === parent.authorId);
    },
  },
};
EOF

Ortam Değişkenleri ve Konfigürasyon

Gerçek dünya projelerinde ortam değişkenleri olmadan çalışmak düşünülemez. .env dosyası ve tip güvenli konfigürasyon:

# .env dosyası
cat > .env << 'EOF'
NODE_ENV=development
PORT=4000
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=supersecretkey-change-in-production
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001
EOF

# .env.example de ekleyelim (git'e bunu commit edin)
cp .env .env.example
sed -i 's/password@localhost/password@localhost/g' .env.example

# .gitignore
cat > .gitignore << 'EOF'
node_modules/
dist/
.env
*.log
.DS_Store
EOF

Tip güvenli konfigürasyon modülü oluşturalım, src/utils/config.ts:

cat > src/utils/config.ts << 'EOF'
function getEnvVar(key: string, defaultValue?: string): string {
  const value = process.env[key] || defaultValue;
  if (value === undefined) {
    throw new Error(`Ortam degiskeni eksik: ${key}`);
  }
  return value;
}

export const config = {
  nodeEnv: getEnvVar('NODE_ENV', 'development'),
  port: parseInt(getEnvVar('PORT', '4000'), 10),
  databaseUrl: getEnvVar('DATABASE_URL', ''),
  jwtSecret: getEnvVar('JWT_SECRET', 'dev-secret'),
  isProduction: process.env.NODE_ENV === 'production',
  isDevelopment: process.env.NODE_ENV === 'development',
  allowedOrigins: getEnvVar('ALLOWED_ORIGINS', 'http://localhost:3000')
    .split(',')
    .map((origin) => origin.trim()),
} as const;
EOF

Build ve Çalıştırma

Geliştirme ortamını başlatalım:

# Geliştirme modunda çalıştır (hot reload ile)
npm run dev

# Veya nodemon ile
npm run dev:nodemon

# Production build
npm run build

# Build edilen versiyonu çalıştır
npm start

Server ayağa kalktığında http://localhost:4000 adresine gidebilir, Apollo Sandbox’ı kullanabilirsiniz. İlk test sorgunuzu çalıştıralım:

# curl ile test
curl -X POST http://localhost:4000 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer test-token" 
  -d '{
    "query": "{ users { id username email role } }"
  }'

# Post listesi için filtreli sorgu
curl -X POST http://localhost:4000 
  -H "Content-Type: application/json" 
  -d '{
    "query": "query GetPosts($filter: PostsFilterInput) { posts(filter: $filter) { posts { id title published tags } totalCount hasNextPage } }",
    "variables": { "filter": { "published": true, "limit": 5, "offset": 0 } }
  }'

Geliştirme Ortamı Optimizasyonları

Gerçek dünya projelerinde birkaç şeye daha dikkat etmek gerekiyor. Hata ayıklama için src/utils/logger.ts ekleyelim:

cat > src/utils/logger.ts << 'EOF'
import { config } from './config.js';

type LogLevel = 'debug' | 'info' | 'warn' | 'error';

interface LogEntry {
  level: LogLevel;
  message: string;
  timestamp: string;
  data?: unknown;
}

function formatLog(entry: LogEntry): string {
  const prefix = `[${entry.timestamp}] [${entry.level.toUpperCase()}]`;
  const message = `${prefix} ${entry.message}`;
  if (entry.data) {
    return `${message}n${JSON.stringify(entry.data, null, 2)}`;
  }
  return message;
}

export const logger = {
  debug: (message: string, data?: unknown) => {
    if (!config.isProduction) {
      const entry: LogEntry = {
        level: 'debug',
        message,
        timestamp: new Date().toISOString(),
        data,
      };
      console.debug(formatLog(entry));
    }
  },
  info: (message: string, data?: unknown) => {
    const entry: LogEntry = {
      level: 'info',
      message,
      timestamp: new Date().toISOString(),
      data,
    };
    console.info(formatLog(entry));
  },
  warn: (message: string, data?: unknown) => {
    const entry: LogEntry = {
      level: 'warn',
      message,
      timestamp: new Date().toISOString(),
      data,
    };
    console.warn(formatLog(entry));
  },
  error: (message: string, error?: unknown) => {
    const entry: LogEntry = {
      level: 'error',
      message,
      timestamp: new Date().toISOString(),
      data: error instanceof Error
        ? { message: error.message, stack: error.stack }
        : error,
    };
    console.error(formatLog(entry));
  },
};
EOF

Sık Karşılaşılan Sorunlar

Geliştirme sürecinde birkaç tipik sorunla karşılaşılıyor:

  • ES Module import hatası: .js uzantısını import yollarına eklemeyi unutmayın. TypeScript dosyalarında bile .js uzantısı kullanılır çünkü derleme sonrası .js dosyaları oluşur
  • tsconfig module uyumsuzluğu: "module": "NodeNext" ile "moduleResolution": "NodeNext" birlikte kullanılmalı
  • nodemon TypeScript desteği: tsx paketi nodemon’dan çok daha iyi performans veriyor, onu tercih edin
  • GraphQL şema tip uyuşmazlıkları: Şema değişikliklerinde TypeScript tiplerini de güncellemeyi unutmayın
  • Context tip hatası: Apollo Server 4’te context factory dönüş tipi açıkça belirtilmeli

Sonuç

TypeScript ile Apollo Server kurulumu ilk bakışta karmaşık görünebilir, özellikle ES Modules geçişiyle birlikte bazı ek yapılandırmalar gerekiyor. Ama bir kez doğru kurulum yaptıktan sonra elde ettiğiniz geliştirme deneyimi gerçekten değer. Tip güvenli resolver’lar, otomatik tamamlama, derleme anında hata tespiti, bunların hepsi uzun vadede ciddi zaman kazandırıyor.

Projenizi büyütürken graphql-codegen entegrasyonunu kesinlikle araştırın. Şemanızdan otomatik TypeScript tipleri üretmek, manuel tip yazmaktan çok daha güvenli ve verimli. Ayrıca DataLoader entegrasyonu N+1 sorgu sorununu çözmek için şart, onu da sonraki adım olarak ele alabilirsiniz.

Paylaştığımız kod örnekleri production-ready olmaktan ziyade konseptleri öğretmeye yönelik. Gerçek bir projede in-memory diziler yerine bir ORM ve veritabanı kullanacaksınız, JWT tabanlı gerçek bir authentication mekanizması kuracaksınız. Ama temel yapı tam olarak bu şekilde.

Bir yanıt yazın

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