İçeriğe atla

2025-11-05

TypeScript'te Builder Pattern: Modern Uygulamalarda Tip Güvenli Konfigürasyon

Builder pattern'in TypeScript'in tip sistemiyle nasıl güvenli ve keşfedilebilir API'ler oluşturduğunu, serverless, veri katmanı ve test örnekleriyle - AWS CDK, query builder'lar ve daha fazlasıyla keşfet.

Özet

TypeScript’teki Builder pattern, geleneksel nesne yönelimli dillerdekinden farklı bir amaca hizmet eder. Java ve C# builder’ları çok sayıda opsiyonel parametreyi yönetmek için kullanırken, TypeScript implementasyonu generic’ler ve conditional type’ları kullanarak karmaşık kısıtlamaları compile time’da zorunlu kılar ve potansiyel runtime hatalarını IDE’nin yakalayabileceği tip hatalarına dönüştürür. Bu rehber, serverless altyapı, veritabanı katmanı, API konfigürasyonu ve test alanlarında pratik uygulamaları ele alarak, builder’ların production’a ulaşmadan önce yanlış konfigürasyonları nasıl önlediğini gösteriyor.

TypeScript’te Karmaşık Nesnelerin Sorunu

TypeScript projelerinde tekrar eden bir pattern fark ettim: sistemler büyüdükçe, konfigürasyon nesnelerinin karmaşıklığı da artıyor. 3-4 parametreli basit bir Lambda fonksiyonu olarak başlayan şey, 20+ konfigürasyon seçeneği olan bir canavara dönüşüyor - VPC ayarları, environment variable’lar, IAM rolleri, layer’lar, timeout değerleri, memory allocation ve daha fazlası.

AWS CDK kullanarak tipik bir AWS Lambda konfigürasyonu:

new lambda.Function(this, 'ApiHandler', {
  runtime: lambda.Runtime.NODEJS_20_X,
  handler: 'index.handler',
  code: lambda.Code.fromAsset('lambda'),
  timeout: Duration.seconds(30),
  memorySize: 1024,
  environment: {
    TABLE_NAME: table.tableName,
    API_KEY: apiKey.secretValue
  },
  layers: [commonLayer, vendorLayer],
  vpc: vpc,
  vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
  securityGroups: [lambdaSecurityGroup],
  deadLetterQueue: dlq,
  retryAttempts: 2,
  reservedConcurrentExecutions: 10,
  tracing: lambda.Tracing.ACTIVE,
  logRetention: logs.RetentionDays.ONE_WEEK,
  // ... ve daha fazlası
});

Sorunlar hızla birikiyor:

  1. Konfigürasyon cehennemi: Parametreler sıraya bağımlı ve yanlış yerleştirmesi kolay
  2. Rehberlik yok: Hangi parametreler zorunlu? Neye ne bağlı?
  3. Runtime sürprizleri: Birçok konfigürasyon hatası ancak Lambda çalıştığında ortaya çıkıyor
  4. Tekrar: Multi-region deployment’lar bu bloğun tamamını kopyalayıp değiştirmeyi gerektiriyor

TypeScript’in opsiyonel parametreleri biraz yardımcı oluyor, ama “VPC’yi etkinleştirirsen subnet’leri sağlamalısın” veya “dead letter queue, permission konfigürasyonu gerektirir” gibi kuralları ifade edemiyorlar. Fluent builder API’si bu bağımlılıkları method chain’inde zorunlu kılar. Bu tür bağımlılıklar genellikle runtime’da keşfedilir ve pahalıya mal olur.

Serverless API’lerle çalışmak, bunların sadece kolaylık sorunları değil, deployment riskleri olduğunu öğretti. Bir keresinde görünüşte iyi olan ama runtime’da VPC konfigürasyonu eksik olduğu için başarısız olan bir Lambda deploy ettim. TypeScript derleyicisi yardımcı olamadı çünkü teknik olarak tüm tipler doğruydu. Builder pattern bu tür hataları derleme aşamasına taşıyor—örn. VPC + subnet bağımlılığı gibi kurallar fluent API ile compile-time’a kodlanabilir.

TypeScript Builder’larını Farklı Kılan Şey

TypeScript’in tip sistemi, Builder pattern’e temel olarak farklı bir yaklaşımı mümkün kılıyor. Sadece daha temiz bir API sağlamanın ötesinde (bunu da yapıyor tabii), TypeScript builder’ları iş kurallarını doğrudan tiplere kodlayabilir ve geçersiz durumları temsil edilemez hale getirebilir.

İşte kavramsal fark:

Eksik Alanlar

Hepsi Ayarlandı

Konfigürasyona Başla

Zorunlu Alanları Ayarla

Derleme Hatası

Progressif Tip Refinement

Opsiyonel Özellikler Ekle

Tip Güvenli Build

Geçerli Nesne

Ana fikir: builder’lar konfigürasyon durumunu generic tip parametreleri aracılığıyla takip eder. Her method çağrısı neyin konfigüre edildiğini yansıtan yeni bir tip döndürür ve build() metodu ancak tüm zorunlu konfigürasyon tamamlandığında kullanılabilir hale gelir.

Progressive tip güvenliğini gösteren basit bir örnek:

type RequiredFields = 'url' | 'method';

class HttpRequestBuilder<TSet extends string = never> {
  private config: Partial<HttpRequest> = {};

  withUrl(url: string): HttpRequestBuilder<TSet | 'url'> {
    this.config.url = url;
    return this as any;
  }

  withMethod(method: string): HttpRequestBuilder<TSet | 'method'> {
    this.config.method = method;
    return this as any;
  }

  withHeaders(headers: Record<string, string>): this {
    this.config.headers = headers;
    return this;
  }

  // build() sadece zorunlu alanlar ayarlandığında kullanılabilir
  build(this: HttpRequestBuilder<RequiredFields>): HttpRequest {
    return this.config as HttpRequest;
  }
}

// Kullanım
const request = new HttpRequestBuilder()
  .withHeaders({ 'Content-Type': 'application/json' })
  .build(); // Bad: Derleme hatası: 'url' ve 'method' ayarlanmamış

const validRequest = new HttpRequestBuilder()
  .withUrl('https://api.example.com/users')
  .withMethod('GET')
  .withHeaders({ 'Content-Type': 'application/json' })
  .build(); // Good: Başarıyla derlenir

Bu compile-time zorunluluğu, TypeScript builder’larını diğer dillerdeki eşdeğerlerinden ayıran şeydir. Sadece API’yi daha uygun hale getirmiyorsun - belirli bug sınıflarını imkansız kılıyorsun.

Temel İmplementasyon: Tip Güvenli Lambda Builder

Bunun daha önceki Lambda problemine nasıl uygulandığını göstereyim. İşte düzgün konfigürasyonu zorunlu kılan bir builder:

import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Duration } from 'aws-cdk-lib';

interface LambdaConfig {
  runtime: lambda.Runtime;
  handler: string;
  code: lambda.Code;
  timeout?: Duration;
  memorySize?: number;
  environment?: Record<string, string>;
  vpc?: ec2.IVpc;
  vpcSubnets?: ec2.SubnetSelection;
}

class LambdaFunctionBuilder {
  private config: Partial<LambdaConfig> = {
    timeout: Duration.seconds(30),
    memorySize: 1024,
  };

  withRuntime(runtime: lambda.Runtime): this {
    this.config.runtime = runtime;
    return this;
  }

  withHandler(handler: string): this {
    this.config.handler = handler;
    return this;
  }

  fromAssetCode(path: string): this {
    this.config.code = lambda.Code.fromAsset(path);
    return this;
  }

  withTimeout(seconds: number): this {
    if (seconds <= 0 || seconds > 900) {
      throw new Error('Timeout 1 ile 900 saniye arasında olmalı');
    }
    this.config.timeout = Duration.seconds(seconds);
    return this;
  }

  withMemory(mb: number): this {
    const validSizes = [128, 256, 512, 1024, 2048, 4096, 8192, 10240];
    if (!validSizes.includes(mb)) {
      throw new Error(`Memory şunlardan biri olmalı: ${validSizes.join(', ')}`);
    }
    this.config.memorySize = mb;
    return this;
  }

  withEnvironment(vars: Record<string, string>): this {
    this.config.environment = {
      ...this.config.environment,
      ...vars
    };
    return this;
  }

  inVpc(vpc: ec2.IVpc, subnetType: ec2.SubnetType = ec2.SubnetType.PRIVATE_WITH_EGRESS): this {
    this.config.vpc = vpc;
    this.config.vpcSubnets = { subnetType };
    return this;
  }

  build(): lambda.FunctionProps {
    if (!this.config.runtime || !this.config.handler || !this.config.code) {
      throw new Error('Runtime, handler ve code zorunludur');
    }

    return this.config as lambda.FunctionProps;
  }
}

// Kullanım: Temiz, kendini dokümante eden ve tip güvenli
const lambdaProps = new LambdaFunctionBuilder()
  .withRuntime(lambda.Runtime.NODEJS_20_X)
  .withHandler('index.handler')
  .fromAssetCode('lambda')
  .withTimeout(60)
  .withMemory(2048)
  .withEnvironment({
    TABLE_NAME: table.tableName,
    LOG_LEVEL: 'info'
  })
  .inVpc(vpc)
  .build();

const apiFunction = new lambda.Function(this, 'ApiHandler', lambdaProps);

İyileştirmelere dikkat et:

  • Erken validasyon: Geçersiz timeout veya memory değerleri deployment’ta değil hemen yakalanıyor
  • Net varsayılanlar: Yaygın konfigürasyonlar (30s timeout, 1024MB memory) otomatik ayarlanıyor
  • Okunabilir: Fluent interface neredeyse dokümantasyon gibi okunuyor
  • Yeniden kullanılabilir: Temel konfigürasyonlar oluşturup spesifik kullanım durumları için genişletebilirsin

Bu pattern, birden fazla region’da düzinelerce Lambda fonksiyonunu yönetirken daha da güçlü hale geliyor. Region’a özgü VPC ve security group farklarını kapsülleyen builder’lar oluşturabilirsin.

Gerçek Dünya Uygulaması: Multi-Region Serverless API

Multi-region serverless mimaride karmaşıklığı yönetmek için builder’ları nasıl kullandım:

// Tüm region'larda paylaşılan temel konfigürasyon
const baseBuilder = new LambdaFunctionBuilder()
  .withRuntime(lambda.Runtime.NODEJS_20_X)
  .withHandler('index.handler')
  .fromAssetCode('lambda')
  .withTimeout(30)
  .withEnvironment({
    LOG_LEVEL: 'info',
    POWERTOOLS_SERVICE_NAME: 'api'
  });

// Region'a özgü konfigürasyonlar
const usEastFunction = new lambda.Function(this, 'UsEastApi',
  baseBuilder
    .withEnvironment({ REGION: 'us-east-1' })
    .inVpc(usEastVpc)
    .build()
);

const euWestFunction = new lambda.Function(this, 'EuWestApi',
  baseBuilder
    .withEnvironment({ REGION: 'eu-west-1' })
    .inVpc(euWestVpc)
    .build()
);

Bu yaklaşım CDK kodumuzu yaklaşık %40 azaltırken, regional farkları açık ve kolayca fark edilebilir hale getirdi. Yeni bir region eklememiz gerektiğinde, tam olarak neyin farklı konfigüre edilmesi gerektiği açıktı.

Database Query Builder’lar: Şemadan Sonuçlara Tip Güvenliği

Query builder’lar muhtemelen TypeScript’te Builder pattern’in en ikna edici kullanım durumunu temsil ediyor. Kysely gibi kütüphaneler, builder’ların veritabanı şemasından sorgu sonuçlarına kadar uçtan uca tip güvenliği nasıl sağlayabileceğini gösteriyor.

İşte tip güvenliği akışı:

Tip Üretimi

Tip Çıkarımı

Otomatik Tamamlama

Validasyon

Sonuç Eşleştirme

Veritabanı Şeması

TypeScript Tipleri

Query Builder

Builder Metodları

Developer IDE

Derleme Zamanı Hataları

Çalıştırılan Sorgu

Tiplendirilmiş Sonuçlar

Tip güvenli query builder pattern kullanan pratik bir örnek:

interface Database {
  users: {
    id: string;
    email: string;
    name: string;
    role: 'admin' | 'user';
    createdAt: Date;
  };
  posts: {
    id: string;
    authorId: string;
    title: string;
    content: string;
    publishedAt: Date | null;
  };
}

class QueryBuilder<TTable extends keyof Database, TResult = Database[TTable]> {
  constructor(
    private table: TTable,
    private query: Partial<{
      select: (keyof Database[TTable])[];
      where: Partial<Database[TTable]>;
      limit: number;
    }> = {}
  ) {}

  select<K extends keyof Database[TTable]>(
    ...columns: K[]
  ): QueryBuilder<TTable, Pick<Database[TTable], K>> {
    return new QueryBuilder(this.table, {
      ...this.query,
      select: columns as any
    });
  }

  where(conditions: Partial<Database[TTable]>): this {
    this.query.where = { ...this.query.where, ...conditions };
    return this;
  }

  limit(count: number): this {
    this.query.limit = count;
    return this;
  }

  async execute(): Promise<TResult[]> {
    // Gerçek implementasyonda bu sorguyu çalıştırır
    // Burada sadece tip güvenliğini gösteriyoruz
    console.log(`${this.table} üzerinde sorgu çalıştırılıyor:`, this.query);
    return [] as TResult[];
  }
}

// Temiz API için factory fonksiyonu
function from<T extends keyof Database>(table: T) {
  return new QueryBuilder(table);
}

// Tam tip güvenliği ile kullanım
const users = await from('users')
  .select('id', 'email', 'name')  // Good: Otomatik tamamlama çalışıyor
  .where({ role: 'admin' })  // Good: Sadece geçerli alanlar izin veriliyor
  .limit(10)
  .execute();
// users'ın tipi: Array<{ id: string, email: string, name: string }>

const posts = await from('posts')
  .select('title', 'publishedAt')
  .where({ authorId: 'user-123' })
  .execute();
// posts'un tipi: Array<{ title: string, publishedAt: Date | null }>

// Bad: Bu derlenmez - 'invalid' bir sütun değil
// const invalid = await from('users').select('invalid').execute();

// Bad: Bu derlenmez - 'posts' tablosunda 'email' yok
// const invalidWhere = await from('posts').where({ email: '[email protected]' }).execute();

Buradaki güç, yazım hatalarının ve yanlış sütun referanslarının compile time’da yakalanması, production’da sorgunun başarısız olmasında değil. Bu yaklaşımın code review’dan kaçabilecek düzinelerce bug’ı yakaladığını gördüm.

API Konfigürasyonu: Express Middleware Builder’lar

Express veya Fastify’daki middleware chain’leri, sıranın önemli olduğu ve hataların maliyetli olduğu başka bir alan. Authentication, authorization’dan önce gelmeli, logging request ID’leri içermeli ve error handler’lar en sonda olmalı.

Bu kuralları kodlayan bir builder:

import { RequestHandler, ErrorRequestHandler, Router } from 'express';

class RouterBuilder {
  private middlewares: RequestHandler[] = [];
  private errorHandlers: ErrorRequestHandler[] = [];
  private router = Router();
  private hasAuth = false;

  withRequestId(): this {
    this.middlewares.push((req, res, next) => {
      res.locals.requestId = crypto.randomUUID();
      next();
    });
    return this;
  }

  withLogging(): this {
    this.middlewares.push((req, res, next) => {
      console.log(`${res.locals.requestId} ${req.method} ${req.path}`);
      next();
    });
    return this;
  }

  withRateLimiting(options: { requestsPerMinute: number }): this {
    // Rate limiting implementasyonu
    this.middlewares.push((req, res, next) => {
      // Rate limit kontrolü
      next();
    });
    return this;
  }

  withAuth(validator: RequestHandler): this {
    this.hasAuth = true;
    this.middlewares.push(validator);
    return this;
  }

  withRoleCheck(allowedRoles: string[]): this {
    if (!this.hasAuth) {
      throw new Error('withRoleCheck() çağrılmadan önce withAuth() çağrılmalı');
    }
    this.middlewares.push((req, res, next) => {
      const userRole = (req as any).user?.role;
      if (!allowedRoles.includes(userRole)) {
        return res.status(403).json({ error: 'Forbidden' });
      }
      next();
    });
    return this;
  }

  route(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, handler: RequestHandler): this {
    const allMiddleware = [...this.middlewares, handler];
    this.router[method.toLowerCase() as 'get'](path, ...allMiddleware);
    return this;
  }

  withErrorHandler(handler?: ErrorRequestHandler): this {
    this.errorHandlers.push(handler || ((err, req, res, next) => {
      console.error('Error:', err);
      res.status(500).json({ error: 'Internal server error' });
    }));
    return this;
  }

  build(): Router {
    // Express'te error handler'lar en sona eklenmeli
    this.errorHandlers.forEach(handler => {
      this.router.use(handler as any);
    });
    return this.router;
  }
}

// Kullanım
const apiRouter = new RouterBuilder()
  .withRequestId()
  .withLogging()
  .withRateLimiting({ requestsPerMinute: 100 })
  .withAuth(jwtAuthMiddleware)
  .withRoleCheck(['admin', 'editor'])
  .route('POST', '/users', createUserHandler)
  .route('GET', '/users/:id', getUserHandler)
  .withErrorHandler()
  .build();

app.use('/api', apiRouter);

Bu pattern middleware sırasını açık hale getirir ve bağımlılık ihlallerini (auth olmadan role check gibi) build time’da yakalar.

Test Data Builder’lar: En Yüksek ROI Uygulaması

Deneyimime göre, test data builder’lar Builder pattern için en iyi yatırım getirisini sağlıyor. Testler çeşitli veri senaryolarına ihtiyaç duyar, ama her test için nesneleri manuel oluşturmak can sıkıcı ve kırılgandır.

Mantıklı varsayılanlara sahip test data builder:

import { faker } from '@faker-js/faker';

interface User {
  id: string;
  email: string;
  name: string;
  role: 'admin' | 'user' | 'guest';
  isVerified: boolean;
  createdAt: Date;
  permissions: string[];
}

class UserBuilder {
  private user: User = {
    id: faker.string.uuid(),
    email: faker.internet.email(),
    name: faker.person.fullName(),
    role: 'user',
    isVerified: false,
    createdAt: new Date(),
    permissions: []
  };

  withId(id: string): this {
    this.user.id = id;
    return this;
  }

  withEmail(email: string): this {
    this.user.email = email;
    return this;
  }

  withRole(role: User['role']): this {
    this.user.role = role;
    return this;
  }

  asAdmin(): this {
    this.user.role = 'admin';
    this.user.permissions = ['read', 'write', 'delete', 'manage'];
    return this;
  }

  asVerified(): this {
    this.user.isVerified = true;
    return this;
  }

  withPermissions(...perms: string[]): this {
    this.user.permissions = perms;
    return this;
  }

  build(): User {
    return { ...this.user };
  }

  // Birden fazla user oluşturmak için yardımcı
  static buildList(count: number, customize?: (builder: UserBuilder, index: number) => UserBuilder): User[] {
    return Array.from({ length: count }, (_, i) => {
      const builder = new UserBuilder();
      return customize ? customize(builder, i).build() : builder.build();
    });
  }
}

// Testlerde kullanım
describe('User API', () => {
  it('pagination ile kullanıcıları listeler', async () => {
    const users = UserBuilder.buildList(15, (builder, i) =>
      builder.withEmail(`user${i}@example.com`)
    );
    await db.users.insertMany(users);

    const response = await request(app)
      .get('/api/users?page=1&limit=10')
      .expect(200);

    expect(response.body.data).toHaveLength(10);
    expect(response.body.total).toBe(15);
  });

  it('sadece admin kullanıcıları silmeye izin verir', async () => {
    const admin = new UserBuilder().asAdmin().build();
    const regularUser = new UserBuilder().build();
    const targetUser = new UserBuilder().build();

    await db.users.insertMany([admin, regularUser, targetUser]);

    // Admin silebilir
    await request(app)
      .delete(`/api/users/${targetUser.id}`)
      .set('Authorization', `Bearer ${generateToken(admin)}`)
      .expect(200);

    // Normal kullanıcı silemez
    await request(app)
      .delete(`/api/users/${targetUser.id}`)
      .set('Authorization', `Bearer ${generateToken(regularUser)}`)
      .expect(403);
  });

  it('hassas işlemler için email doğrulaması gerektirir', async () => {
    const unverifiedUser = new UserBuilder().build();
    const verifiedUser = new UserBuilder().asVerified().build();

    await db.users.insertMany([unverifiedUser, verifiedUser]);

    // Doğrulanmamış kullanıcı engellenir
    await request(app)
      .post('/api/sensitive-action')
      .set('Authorization', `Bearer ${generateToken(unverifiedUser)}`)
      .expect(403);

    // Doğrulanmış kullanıcı izin verilir
    await request(app)
      .post('/api/sensitive-action')
      .set('Authorization', `Bearer ${generateToken(verifiedUser)}`)
      .expect(200);
  });
});

Bu yaklaşım bir projede test setup kodunu yaklaşık %60 azalttı ve daha önemlisi, User modeline yeni bir zorunlu alan eklediğimizde, düzinelerce test dosyası yerine sadece builder’ın varsayılanlarını güncellememiz gerekti.

İleri Düzey TypeScript Teknikleri

Daha da güçlü builder’lar için TypeScript’in gelişmiş özelliklerinden nasıl yararlanabileceğimizi keşfedelim.

Conditional Type’larla Progressive Tip Refinement

Belirli metodları ancak diğerleri çağrıldıktan sonra açığa çıkaran builder’lar oluşturabilirsin:

type ConfigState = {
  hasDatabase: boolean;
  hasCache: boolean;
  hasAuth: boolean;
};

class AppConfigBuilder<TState extends Partial<ConfigState> = {}> {
  private config: any = {};

  withDatabase(url: string): AppConfigBuilder<TState & { hasDatabase: true }> {
    this.config.database = url;
    return this as any;
  }

  // Cache config sadece database konfigüre edildikten sonra kullanılabilir
  withCache<T extends TState>(
    this: T extends { hasDatabase: true } ? AppConfigBuilder<T> : never,
    options: CacheOptions
  ): AppConfigBuilder<TState & { hasCache: true }> {
    this.config.cache = options;
    return this as any;
  }

  // Auth için database gerekli
  withAuth<T extends TState>(
    this: T extends { hasDatabase: true } ? AppConfigBuilder<T> : never,
    config: AuthConfig
  ): AppConfigBuilder<TState & { hasAuth: true }> {
    this.config.auth = config;
    return this as any;
  }

  build(): AppConfig {
    return this.config;
  }
}

// Kullanım
const config = new AppConfigBuilder()
  .withDatabase('postgres://localhost/db')
  .withCache({ ttl: 3600 })  // Good: Database konfigüre edildi
  .withAuth({ provider: 'jwt' })  // Good: Database konfigüre edildi
  .build();

// Bad: Bu derlenmez - database olmadan cache kullanamazsın
// const invalid = new AppConfigBuilder().withCache({ ttl: 3600 }).build();

Bu teknik tiplerde kodlanmış bir state machine oluşturur ve konfigürasyon adımlarının doğru sırada gerçekleşmesini sağlar.

Generic Accumulation ile Immutable Builder’lar

Fonksiyonel programlama bağlamları için, state’i değiştirmeyen builder’lar istiyorsun:

class ImmutableQueryBuilder<
  TTable extends keyof Database,
  TSelected extends keyof Database[TTable] = keyof Database[TTable]
> {
  constructor(
    private readonly table: TTable,
    private readonly config: {
      select?: TSelected[];
      where?: Partial<Database[TTable]>;
      limit?: number;
    } = {}
  ) {}

  select<K extends keyof Database[TTable]>(
    ...columns: K[]
  ): ImmutableQueryBuilder<TTable, K> {
    return new ImmutableQueryBuilder(this.table, {
      ...this.config,
      select: columns as any
    });
  }

  where(conditions: Partial<Database[TTable]>): ImmutableQueryBuilder<TTable, TSelected> {
    return new ImmutableQueryBuilder(this.table, {
      ...this.config,
      where: { ...this.config.where, ...conditions }
    });
  }

  limit(count: number): ImmutableQueryBuilder<TTable, TSelected> {
    return new ImmutableQueryBuilder(this.table, {
      ...this.config,
      limit: count
    });
  }

  toSQL(): string {
    const columns = this.config.select?.join(', ') || '*';
    const conditions = this.config.where
      ? ' WHERE ' + Object.entries(this.config.where)
          .map(([k, v]) => `${k} = ${JSON.stringify(v)}`)
          .join(' AND ')
      : '';
    const limitClause = this.config.limit ? ` LIMIT ${this.config.limit}` : '';

    return `SELECT ${columns} FROM ${this.table}${conditions}${limitClause}`;
  }
}

// Her method çağrısı yeni bir instance döndürür
const baseQuery = new ImmutableQueryBuilder('users');
const adminQuery = baseQuery.where({ role: 'admin' });
const userQuery = baseQuery.where({ role: 'user' });

// baseQuery değişmedi - gerçek immutability
console.log(baseQuery.toSQL());  // SELECT * FROM users
console.log(adminQuery.toSQL()); // SELECT * FROM users WHERE role = "admin"
console.log(userQuery.toSQL());  // SELECT * FROM users WHERE role = "user"

Bu pattern, orijinali etkilemeden temel bir konfigürasyonun varyasyonlarını oluşturman gerektiğinde değerlidir.

Builder’ları Ne Zaman Kullanmalı (Ne Zaman Kullanmamalı)

Builder pattern her zaman doğru seçim değil. Öğrendiğim şeylere dayalı bir karar çerçevesi:

1-3

4-8

9+

Hayır

Evet

Temel

Gelişmiş

Nesne Oluşturma İhtiyacı

Kaç Özellik?

Constructor/Object Literal Kullan

Karmaşık Kısıtlamalar?

Builder Düşünerek

Options Object Kullan

Tip Güvenliği Gerekli?

Basit Builder

Generic'lerle Builder

Basit Çözüm

TypeScript Optional Params

Standart Builder

Gelişmiş Builder Pattern

Basit Alternatifleri Ne Zaman Kullanmalı:

1. Nesne Basitse (2-3 özellik)

// Bad: Gereksiz karmaşıklık
new UserBuilder()
  .withName('John')
  .withEmail('[email protected]')
  .build();

// Good: Daha iyi
const user = { name: 'John', email: '[email protected]' };

2. TypeScript’in Opsiyonel Parametreleri Yeterli

// Good: İyi - karmaşık kısıtlama yok
function createLogger(options?: {
  level?: 'debug' | 'info' | 'warn' | 'error';
  format?: 'json' | 'text';
}) {
  return new Logger(options);
}

Builder’ları Ne Zaman Kullanmalı:

1. Çok Sayıda Opsiyonel Parametre (5+)

// Çok sayıda seçeneğe sahip config nesneleri fluent API'lerden faydalanır
const server = new ServerBuilder()
  .withPort(3000)
  .withHost('localhost')
  .withCors({ origins: ['https://example.com'] })
  .withRateLimit({ requestsPerMinute: 100 })
  .withCompression()
  .withLogging({ level: 'info' })
  .build();

2. Karmaşık Validasyon veya Kısıtlamalar

// Builder, S3 bucket'in region ve encryption config gerektirdiğini zorlar
const bucket = new S3BucketBuilder()
  .withName('my-bucket')
  .inRegion('us-east-1')
  .withEncryption({ type: 'AES256' })  // Region ayarlandığında zorunlu
  .build();

3. Adım Adım Oluşturma Netliği Artırıyor

// Pipeline oluşturma açık adımlardan faydalanır
const pipeline = new DataPipelineBuilder()
  .readFrom(source)
  .transform(cleanData)
  .filter(isValid)
  .aggregate(byCategory)
  .writeTo(destination)
  .build();

4. Fluent, Keşfedilebilir API’ler Oluşturma

// IDE'ler her adımda mevcut seçenekleri gösterebilir
const query = db.from('users')
  .select('id', 'name')  // IDE mevcut sütunları gösterir
  .where({ status: 'active' })  // IDE geçerli alanları gösterir
  .orderBy('createdAt', 'desc')
  .limit(10);

Yaygın Tuzaklar ve Öğrenilen Dersler

Karşılaştığım hatalar ve bunlardan nasıl kaçınılır:

Tuzak 1: Aşırı Karmaşık Tipler

Sorun: Derlemeyi yavaşlatan ve şifreli hatalar üreten aşırı karmaşık generic tipler.

// Bad: Çok karmaşık - derleme süreleri zarar görüyor, hatalar okunamıyor
class Builder<
  T,
  S extends keyof T,
  R extends Required<Pick<T, S>>,
  O extends Omit<T, S>
> { /* ... */ }

Çözüm: Tip güvenliği ile pragmatizm arasında denge kur. Basit başla ve sadece gerektiğinde karmaşıklık ekle.

// Good: Daha basit, hala yararlı
class Builder<T> {
  private data: Partial<T> = {};

  set<K extends keyof T>(key: K, value: T[K]): this {
    this.data[key] = value;
    return this;
  }

  build(): T {
    // Zorunlu alanlar için runtime validasyon
    return this.data as T;
  }
}

Tuzak 2: Takip Olmadan Mutable State

Sorun: Geleneksel mutable builder’lar eksik konfigürasyonla build() çağrılmasına izin verir.

// Bad: Geçersiz nesne oluşturabilir
class RequestBuilder {
  private url?: string;
  private method?: string;

  build(): Request {
    return { url: this.url!, method: this.method! };  // Undefined olabilir!
  }
}

Çözüm: Generic tip takibi kullan ya da build() içinde validate et.

// Good: Runtime validasyon
build(): Request {
  if (!this.url || !this.method) {
    throw new Error('URL ve method zorunludur');
  }
  return { url: this.url, method: this.method };
}

Tuzak 3: Hot Path’lerde Performans Etkisi

Sorun: Performans kritik döngülerde builder oluşturma.

// Bad: Her veri elemanı için builder oluşturuyor
const results = largeDataset.map(item =>
  new ObjectBuilder()
    .withId(item.id)
    .withValue(item.value)
    .build()
);

Çözüm: Veri dönüşümü için değil, konfigürasyon için builder’ları kullan.

// Good: Veri işleme için düz nesne oluşturma
const results = largeDataset.map(item => ({
  id: item.id,
  value: item.value
}));

// Setup/konfigürasyon için builder kullan
const processor = new DataProcessorBuilder()
  .withBatchSize(1000)
  .withConcurrency(4)
  .withErrorHandler(logError)
  .build();

const results = processor.process(largeDataset);

Tuzak 4: Tutarsız Method İsimlendirme

Sorun: İsimlendirme konvansiyonlarını karıştırmak keşfedilebilirliği azaltır.

// Bad: Tutarsız
new ConfigBuilder()
  .setUrl('...')  // set*
  .withTimeout(30)  // with*
  .addHeader('...')  // add*
  .enableCache()  // enable*

Çözüm: İsimlendirme konvansiyonları belirle ve takip et.

// Good: Tutarlı
new ConfigBuilder()
  .withUrl('...')  // with* tek değerler için
  .withTimeout(30)
  .addHeader('name', 'val') // add* koleksiyonlar için
  .enableCache()  // enable*/disable* boolean'lar için

Tuzak 5: Sadece Build Time’da Validasyon

Sorun: Geçersiz konfigürasyon build() çağrılana kadar yakalanmıyor, potansiyel olarak hatanın kaynağından uzakta.

// Bad: Geç validasyon
class Builder {
  private timeout?: number;

  withTimeout(seconds: number): this {
    this.timeout = seconds;  // Validasyon yok
    return this;
  }

  build() {
    if (this.timeout && this.timeout > 900) {
      throw new Error('Timeout çok büyük');  // Hata kaynaktan uzak
    }
  }
}

Çözüm: Setter metodlarında erken validate et.

// Good: Erken validasyon
withTimeout(seconds: number): this {
  if (seconds <= 0) {
    throw new Error('Timeout pozitif olmalı');
  }
  if (seconds > 900) {
    throw new Error('Timeout 900 saniyeyi geçemez');
  }
  this.timeout = seconds;
  return this;
}

Sonuç: Builder Pattern’in Tatlı Noktası

TypeScript’teki Builder pattern belirli bir problem setini istisnai bir şekilde iyi çözüyor. Constructor’ları güzelleştirmekle ilgili değil - konfigürasyon hatalarını compile time’da yakalamak için tip sisteminden yararlanmak ve hem güçlü hem de keşfedilmesi kolay API’ler oluşturmakla ilgili.

Pattern şu durumlarda parlıyor:

  • Infrastructure-as-code oluşturuyorsan (AWS CDK, Terraform CDK)
  • Tip güvenli query builder’lara veya API client’larına ihtiyaç duyuyorsan
  • Mantıklı varsayılanlarla test verisi üretiyorsan
  • Karmaşık middleware veya plugin sistemleri dikkatli sıralama gerektiriyorsa
  • Konfigürasyon nesnelerinin birbirine bağımlı kısıtlamaları varsa

Şu durumlarda gereksiz:

  • Nesneler basitse (2-3 özellik)
  • TypeScript’in opsiyonel parametreleri işi hallediyor
  • Performans kritik path’lerde veri işliyorsan

Deneyimime göre, en büyük değer üç alandan geliyor:

  1. Infrastructure konfigürasyonu: AWS CDK builder’lar yanlış konfigürasyondan kaynaklanan deployment hatalarını önlüyor
  2. Database query builder’lar: Tip güvenli SQL yazım hatalarından kaynaklanan runtime hatalarını önlüyor
  3. Test verisi üretimi: Test boilerplate’ini azaltıyor ve testleri daha sürdürülebilir yapıyor

Ana fikir, TypeScript’in tip sisteminin iş kurallarını ve kısıtlamaları doğrudan API’ye kodlamanı sağlamasıdır. Gerekli adımlar tamamlanana kadar derlenmeyen bir builder gördüğünde, sadece daha uygun kod yazmıyorsun - belirli bug sınıflarını imkansız kılıyorsun.

En karmaşık konfigürasyon nesnelerin için basit builder’larla başla ve hangi kısıtlamaların kodlamaya değer olduğunu keşfettikçe kademeli olarak tip güvenliği ekle. Her builder’ın gelişmiş generic tiplere ihtiyacı yok, ama ihtiyaç duyduğunda TypeScript sana gerçekten sağlam API’ler oluşturman için araçlar veriyor.

İlgili yazılar

AWS AppSync & GraphQL: Production-Ready Real-time API'ler Geliştirmek

AWS AppSync ile ölçeklenebilir real-time API'ler geliştirmek için kapsamlı bir rehber: JavaScript resolver'lar, subscription filtering, caching stratejileri ve infrastructure as code pattern'leri.

awsappsyncgraphql+5
SNS/SQS Cross-Account Fan-Out: AWS'de Multi-Account Event Dağıtımı

Amazon SNS ve SQS kullanarak güvenli cross-account event dağıtımı nasıl yapılır öğrenin. IAM policy'leri, KMS şifreleme, AWS CDK implementasyonu ve production'da karşılaşılan yaygın sorunları kapsıyor.

awsaws-snsaws-sqs+6
AWS Step Functions Derinlemesine: Dayanıklı Workflow Orchestration Geliştirme

Production-ready serverless workflow'lar için AWS Step Functions'ı öğren. Standard vs Express workflow'lar, Distributed Map processing, error handling pattern'leri, callback entegrasyonu ve CDK örnekleriyle maliyet optimizasyonu stratejilerini keşfet.

aws-step-functionsaws-cdkserverless+4
AWS CDK Link Kısaltıcı Bölüm 1: Proje Kurulumu & Temel Altyapı

AWS CDK, DynamoDB ve Lambda ile production-grade link kısaltıcı kurulumu. Gerçek mimari kararlar, ilk kurulum ve büyük ölçekte URL kısaltıcıları inşa etmenin dersleri.

aws-cdklambdadynamodb+6
AWS Bedrock ve CDK ile RAG Agent Kurmak

AWS Bedrock + Knowledge Bases + OpenSearch Serverless üstüne CDK ile TypeScript kullanarak RAG agent kurmak — mimari, IAM bağlantısı, otomatik ingestion ve chat UI.

aws-bedrockaws-cdkrag+3