Apollo Server Resolver Yazımı: Tip Güvenli Yaklaşım
GraphQL API geliştirmeye başladığınızda en büyük sorunlardan biri tip güvenliğini sağlamak oluyor. Özellikle büyük projelerde resolver’lar arasında geçen veriler tip kontrolü olmadan kaos haline gelebiliyor. Apollo Server ile çalışırken TypeScript’i doğru kullanmak, gece yarısı production hatalarını önemli ölçüde azaltıyor. Bu yazıda Apollo Server resolver’larını tip güvenli bir şekilde nasıl yazacağınızı gerçek dünya senaryolarıyla ele alacağım.
Neden Tip Güvenli Resolver Yazmalıyız?
Bir e-ticaret projesinde çalıştığınızı düşünün. User tipinden Order tipine geçerken, Order içinde Product listesi dönerken tip hatalarını runtime’da yakalamak yerine derleme aşamasında yakalamak istiyorsunuz. JavaScript ile yazılmış resolver’larda undefined is not a function hatası production’da patlıyor ve müşteri siparişlerini göremez hale geliyor. TypeScript ile bu hataların büyük çoğunluğunu daha kodu çalıştırmadan görürsünüz.
Tip güvenli resolver yazımının faydaları şunlardır:
- Erken hata tespiti: Derleme aşamasında tip uyumsuzlukları yakalanır
- Otomatik tamamlama: IDE desteği ile resolver içinde hangi alanların mevcut olduğunu görürsünüz
- Refactoring güvenliği: Şema değişikliklerinde etkilenen tüm resolver’lar hemen belli olur
- Dokümantasyon: Tipler, kodun kendisi kadar açıklayıcı belgeler sunar
- Takım içi iletişim: Yeni ekip üyeleri veri yapısını hızla kavrar
Temel Kurulum ve Yapı
Önce projeyi kuralım. Apollo Server 4 ve TypeScript kullanacağız:
mkdir graphql-tipli-api && cd graphql-tipli-api
npm init -y
npm install @apollo/server graphql
npm install -D typescript @types/node ts-node nodemon
npm install graphql-codegen @graphql-codegen/cli
npx tsc --init
tsconfig.json dosyanızı şöyle ayarlayın:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Şimdi temel şema tanımlamasını yapalım. GraphQL şeması ile TypeScript tipleri arasındaki köprüyü kurmak için önce şemayı yazıyoruz:
# src/schema/typeDefs.ts
import { gql } from 'graphql-tag';
export const typeDefs = gql`
type User {
id: ID!
email: String!
username: String!
createdAt: String!
orders: [Order!]!
}
type Product {
id: ID!
name: String!
price: Float!
stock: Int!
category: String!
}
type OrderItem {
product: Product!
quantity: Int!
unitPrice: Float!
}
type Order {
id: ID!
user: User!
items: [OrderItem!]!
totalAmount: Float!
status: OrderStatus!
createdAt: String!
}
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
type Query {
user(id: ID!): User
users: [User!]!
product(id: ID!): Product
products(category: String): [Product!]!
order(id: ID!): Order
}
type Mutation {
createUser(input: CreateUserInput!): User!
createOrder(input: CreateOrderInput!): Order!
updateOrderStatus(orderId: ID!, status: OrderStatus!): Order!
}
input CreateUserInput {
email: String!
username: String!
password: String!
}
input CreateOrderInput {
userId: ID!
items: [OrderItemInput!]!
}
input OrderItemInput {
productId: ID!
quantity: Int!
}
`;
TypeScript Tiplerini Manuel Tanımlama
GraphQL Codegen kullanmadan önce manuel tip tanımlamasını anlayalım. Bu sayede otomatik araçların arka planda ne yaptığını kavramış olursunuz:
# src/types/index.ts
export enum OrderStatus {
PENDING = 'PENDING',
PROCESSING = 'PROCESSING',
SHIPPED = 'SHIPPED',
DELIVERED = 'DELIVERED',
CANCELLED = 'CANCELLED'
}
export interface User {
id: string;
email: string;
username: string;
passwordHash: string; // GraphQL şemasında yok ama DB'de var
createdAt: string;
}
export interface Product {
id: string;
name: string;
price: number;
stock: number;
category: string;
}
export interface OrderItem {
productId: string;
quantity: number;
unitPrice: number;
}
export interface Order {
id: string;
userId: string;
items: OrderItem[];
totalAmount: number;
status: OrderStatus;
createdAt: string;
}
// Resolver context tipi
export interface GraphQLContext {
userId?: string;
isAuthenticated: boolean;
dataSources: {
userService: UserService;
productService: ProductService;
orderService: OrderService;
};
}
// Input tipleri
export interface CreateUserInput {
email: string;
username: string;
password: string;
}
export interface CreateOrderInput {
userId: string;
items: Array<{
productId: string;
quantity: number;
}>;
}
Resolver Tiplerini Tanımlama
Apollo Server’ın sağladığı Resolvers tipini kullanarak resolver’larınızı tip güvenli yapabilirsiniz:
# src/resolvers/types.ts
import { GraphQLResolveInfo } from 'graphql';
import { GraphQLContext, User, Product, Order, OrderItem } from '../types';
// Parent tiplerini eşleştiriyoruz
export type UserParent = User;
export type ProductParent = Product;
export type OrderParent = Order;
export type OrderItemParent = OrderItem & { productId: string };
// Resolver fonksiyon tipi
export type ResolverFn<TResult, TParent, TContext, TArgs> = (
parent: TParent,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo
) => Promise<TResult> | TResult;
// Query resolver tipleri
export interface QueryResolvers {
user: ResolverFn<
User | null,
{},
GraphQLContext,
{ id: string }
>;
users: ResolverFn<User[], {}, GraphQLContext, {}>;
product: ResolverFn<
Product | null,
{},
GraphQLContext,
{ id: string }
>;
products: ResolverFn<
Product[],
{},
GraphQLContext,
{ category?: string }
>;
order: ResolverFn<
Order | null,
{},
GraphQLContext,
{ id: string }
>;
}
Gerçek Resolver Implementasyonu
Şimdi gerçek e-ticaret senaryosunda resolver’ları yazalım. Her resolver’ın tip güvenli olduğuna dikkat edin:
# src/resolvers/queries.ts
import { GraphQLResolveInfo } from 'graphql';
import { GraphQLContext, User, Product, Order } from '../types';
export const queryResolvers = {
Query: {
user: async (
_parent: unknown,
args: { id: string },
context: GraphQLContext,
_info: GraphQLResolveInfo
): Promise<User | null> => {
if (!context.isAuthenticated) {
throw new Error('Kimlik doğrulama gerekli');
}
return context.dataSources.userService.findById(args.id);
},
products: async (
_parent: unknown,
args: { category?: string },
context: GraphQLContext
): Promise<Product[]> => {
if (args.category) {
return context.dataSources.productService.findByCategory(
args.category
);
}
return context.dataSources.productService.findAll();
},
order: async (
_parent: unknown,
args: { id: string },
context: GraphQLContext
): Promise<Order | null> => {
if (!context.isAuthenticated || !context.userId) {
throw new Error('Kimlik doğrulama gerekli');
}
const order = await context.dataSources.orderService.findById(
args.id
);
// Kullanıcı kendi siparişini görebilir
if (order && order.userId !== context.userId) {
throw new Error('Bu siparişe erişim yetkiniz yok');
}
return order;
}
}
};
İlişkisel Resolver’lar ve N+1 Problemi
GraphQL’in en yaygın performans sorunlarından biri N+1 problemidir. Tip güvenli resolver yazarken bu sorunu da ele almak gerekir:
# src/resolvers/typeResolvers.ts
import { GraphQLContext, User, Order, OrderItem, Product } from '../types';
import DataLoader from 'dataloader';
// DataLoader ile N+1 çözümü
export const createProductLoader = (context: GraphQLContext) => {
return new DataLoader<string, Product | null>(
async (productIds: readonly string[]) => {
const products = await context.dataSources.productService
.findByIds([...productIds]);
const productMap = new Map(
products.map((p: Product) => [p.id, p])
);
return productIds.map(id => productMap.get(id) || null);
}
);
};
export const typeResolvers = {
User: {
orders: async (
parent: User,
_args: {},
context: GraphQLContext
): Promise<Order[]> => {
return context.dataSources.orderService.findByUserId(parent.id);
}
},
Order: {
user: async (
parent: Order,
_args: {},
context: GraphQLContext
): Promise<User> => {
const user = await context.dataSources.userService.findById(
parent.userId
);
if (!user) {
throw new Error(`Kullanıcı bulunamadı: ${parent.userId}`);
}
return user;
},
items: async (
parent: Order,
_args: {},
_context: GraphQLContext
): Promise<OrderItem[]> => {
// Order zaten items içeriyor, dönüştürme işlemi yapıyoruz
return parent.items;
}
},
OrderItem: {
product: async (
parent: OrderItem & { productId: string },
_args: {},
context: GraphQLContext
): Promise<Product> => {
// DataLoader kullanarak N+1 önleme
const product = await context.productLoader.load(parent.productId);
if (!product) {
throw new Error(`Ürün bulunamadı: ${parent.productId}`);
}
return product;
}
}
};
Mutation Resolver’ları ve Validasyon
Mutation resolver’larında tip güvenliği özellikle önemlidir. Gelen input verilerini validate ederken TypeScript’ten maksimum fayda sağlayalım:
# src/resolvers/mutations.ts
import { GraphQLContext, CreateUserInput, CreateOrderInput, Order } from '../types';
import { validateEmail, hashPassword, calculateTotal } from '../utils';
export const mutationResolvers = {
Mutation: {
createUser: async (
_parent: unknown,
args: { input: CreateUserInput },
context: GraphQLContext
) => {
const { email, username, password } = args.input;
// Tip güvenli validasyon
if (!validateEmail(email)) {
throw new Error('Geçersiz email formatı');
}
if (password.length < 8) {
throw new Error('Şifre en az 8 karakter olmalıdır');
}
const existingUser = await context.dataSources.userService
.findByEmail(email);
if (existingUser) {
throw new Error('Bu email adresi zaten kayıtlı');
}
const passwordHash = await hashPassword(password);
return context.dataSources.userService.create({
email,
username,
passwordHash,
createdAt: new Date().toISOString()
});
},
createOrder: async (
_parent: unknown,
args: { input: CreateOrderInput },
context: GraphQLContext
): Promise<Order> => {
if (!context.isAuthenticated) {
throw new Error('Kimlik doğrulama gerekli');
}
const { userId, items } = args.input;
// Stok kontrolü - tip güvenli
const productChecks = await Promise.all(
items.map(async (item) => {
const product = await context.dataSources.productService
.findById(item.productId);
if (!product) {
throw new Error(`Ürün bulunamadı: ${item.productId}`);
}
if (product.stock < item.quantity) {
throw new Error(
`Yetersiz stok: ${product.name} (mevcut: ${product.stock})`
);
}
return { product, quantity: item.quantity };
})
);
const orderItems = productChecks.map(({ product, quantity }) => ({
productId: product.id,
quantity,
unitPrice: product.price
}));
const totalAmount = calculateTotal(orderItems);
const order = await context.dataSources.orderService.create({
userId,
items: orderItems,
totalAmount,
status: 'PENDING' as const,
createdAt: new Date().toISOString()
});
// Stok güncelleme
await Promise.all(
productChecks.map(({ product, quantity }) =>
context.dataSources.productService.decreaseStock(
product.id,
quantity
)
)
);
return order;
},
updateOrderStatus: async (
_parent: unknown,
args: { orderId: string; status: string },
context: GraphQLContext
): Promise<Order> => {
if (!context.isAuthenticated) {
throw new Error('Kimlik doğrulama gerekli');
}
const order = await context.dataSources.orderService
.findById(args.orderId);
if (!order) {
throw new Error(`Sipariş bulunamadı: ${args.orderId}`);
}
return context.dataSources.orderService.updateStatus(
args.orderId,
args.status
);
}
}
};
GraphQL Codegen ile Otomatik Tip Üretimi
Büyük projelerde tipleri manuel yazmak yerine GraphQL Codegen kullanmak çok daha verimlidir:
# codegen.yml
overwrite: true
schema: "src/schema/typeDefs.ts"
generates:
src/generated/graphql.ts:
plugins:
- "typescript"
- "typescript-resolvers"
config:
contextType: "../types#GraphQLContext"
mappers:
User: "../types#User"
Product: "../types#Product"
Order: "../types#Order"
OrderItem: "../types#OrderItem"
useIndexSignature: true
enumsAsTypes: true
avoidOptionals: true
Codegen’i çalıştırmak için:
# package.json scripts bölümüne ekleyin
npx graphql-codegen --config codegen.yml
# Geliştirme sırasında izleme modunda çalıştırma
npx graphql-codegen --config codegen.yml --watch
Codegen çalıştıktan sonra üretilen tipler şu şekilde kullanılır:
# src/resolvers/index.ts - Codegen tipleriyle
import { Resolvers } from '../generated/graphql';
import { GraphQLContext } from '../types';
export const resolvers: Resolvers<GraphQLContext> = {
Query: {
user: async (_parent, args, context) => {
// args.id otomatik olarak string tipinde
// dönüş tipi User | null olarak zorunlu
return context.dataSources.userService.findById(args.id);
},
products: async (_parent, args, context) => {
// args.category otomatik olarak string | null | undefined
return args.category
? context.dataSources.productService.findByCategory(args.category)
: context.dataSources.productService.findAll();
}
},
Mutation: {
createUser: async (_parent, args, context) => {
// args.input.email, args.input.username, args.input.password
// hepsi otomatik tip güvenliği ile geliyor
const { email, username, password } = args.input;
const passwordHash = await hashPassword(password);
return context.dataSources.userService.create({
email,
username,
passwordHash,
createdAt: new Date().toISOString()
});
}
}
};
Hata Yönetimi ve Özel Error Tipleri
Tip güvenli hata yönetimi de resolver kalitesini artırır:
# src/errors/index.ts
import { GraphQLError } from 'graphql';
export class AuthenticationError extends GraphQLError {
constructor(message: string = 'Kimlik doğrulama başarısız') {
super(message, {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 }
}
});
}
}
export class AuthorizationError extends GraphQLError {
constructor(message: string = 'Bu işlem için yetkiniz yok') {
super(message, {
extensions: {
code: 'FORBIDDEN',
http: { status: 403 }
}
});
}
}
export class NotFoundError extends GraphQLError {
constructor(resource: string, id: string) {
super(`${resource} bulunamadı: ${id}`, {
extensions: {
code: 'NOT_FOUND',
resource,
id,
http: { status: 404 }
}
});
}
}
export class ValidationError extends GraphQLError {
constructor(field: string, message: string) {
super(`Validasyon hatası - ${field}: ${message}`, {
extensions: {
code: 'BAD_USER_INPUT',
field,
http: { status: 400 }
}
});
}
}
Bu özel error tiplerini resolver’larınızda kullanmak hem tip güvenliği hem de istemci tarafında anlamlı hata mesajları sağlar.
Yaygın Hatalar ve Çözümleri
Tip güvenli resolver yazarken sık karşılaşılan sorunlar şunlardır:
- Circular dependency: User Order’a, Order User’a referans verdiğinde sonsuz döngü oluşabilir. Bunu lazy loading veya ayrı tip dosyaları ile çözün
- any kullanımı: Tip güvenliğini bozan
anyyerineunknownkullanın ve tip guard’larla daraltın - Parent tipini yanlış tanımlamak: Field resolver’larında parent tipi yanlış belirtmek runtime hatalarına yol açar
- Async/await tutarsızlığı: Bazı resolver’lar Promise dönerken bazıları senkron değer döndürüyorsa tip uyumsuzluğu çıkabilir
- Codegen’i güncel tutmamak: Şema değiştiğinde codegen’i çalıştırmayı unutmak tiplerin eski kalmasına neden olur
- Context tipini gevşek tanımlamak: Context içindeki servisleri tip güvenli tanımlamak resolver kalitesini doğrudan etkiler
- Input validasyonunu atlamak: Tip güvenliği runtime validasyonunun yerini tutmaz, ikisi birlikte çalışmalıdır
Sonuç
Apollo Server ile tip güvenli resolver yazmak başlangıçta fazladan çaba gibi görünse de orta ve uzun vadede production kararlılığını ciddi ölçüde artırıyor. Manuel tip tanımlamayı anlayarak başlayın, sonra GraphQL Codegen ile otomatikleştirin. Özellikle ekip ortamında çalışırken tipler, kodu belgeleyen ve hataları önceden yakalayan en iyi araç haline geliyor.
Gerçek projelerden edindiğim en önemli ders şu: Şema değişikliği yaptığınızda Codegen otomatik çalışacak şekilde CI/CD pipeline’ınıza entegre edin. Böylece tip uyumsuzlukları merge request aşamasında yakalanır, production’a ulaşmaz. DataLoader ile N+1 sorununu çözmek, özel error tipleri kullanmak ve context’i sıkı tiplerle tanımlamak, sürdürülebilir bir GraphQL API’nin temel taşlarıdır.
