TypeScript'te Builder Pattern: Modern Uygulamalarda Tip Güvenli Konfigürasyon
Builder pattern'in TypeScript tip sistemiyle serverless, veri katmanı ve testte güvenli, keşfedilebilir API'leri nasıl kurduğu, çalışan örneklerle.
TypeScript’teki Builder pattern, geleneksel nesne yönelimli dillerdekinden farklı bir amaca hizmet eder. Java ve C# builder’ları çoğunlukla uzun opsiyonel parametre listelerini yönetmek için kullanır. TypeScript daha ileri gidebiliyor: generic’ler ve conditional type’lar sayesinde builder, kısıtlamaları compile time’da zorunlu kılabiliyor. Yanlış konfigürasyon deployment’ta patlamak yerine IDE’de tip hatası olarak görünüyor.
Bu da pattern’in işe yaradığı alanı daraltıyor. Konfigürasyon nesnesi birbirine bağımlı kısıtlamalar taşıyorsa builder’a geç; pratikte bu grup altyapı tanımları, query builder’lar ve test verisi fabrikalarından oluşuyor. Geri kalan her yerde düz nesne literali veya options parametresi yeterli.
TypeScript’te Karmaşık Nesnelerin Sorunu#
TypeScript projelerinde tekrar eden bir pattern ortaya çıkar: sistemler büyüdükçe, konfigürasyon nesnelerinin karmaşıklığı da artar. 3-4 parametreli basit bir Lambda fonksiyonu olarak başlayan şey, 20+ konfigürasyon seçeneği olan bir canavara dönüşür: 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:
- Konfigürasyon cehennemi: Parametreler sıraya bağımlı ve yanlış yerleştirmesi kolay
- Rehberlik yok: Hangi parametreler zorunlu? Neye ne bağlı?
- Runtime sürprizleri: Birçok konfigürasyon hatası ancak Lambda çalıştığında ortaya çıkıyor
- 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.
Serverless API’lerde bunlar sadece kolaylık sorunları değil, deployment riskleridir. Bir Lambda görünüşte iyi olabilir ama runtime’da VPC konfigürasyonu eksik olduğu için başarısız olabilir. TypeScript derleyicisi yardımcı olamaz çünkü teknik olarak tüm tipler doğrudur.
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. TypeScript builder’ı iş kurallarını doğrudan tiplere kodluyor ve geçersiz durumları temsil edilemez hale getiriyor. Daha temiz API de cabası.
Kavramsal fark şöyle:
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.
Aşamalı tip güvenliğini gösteren küçük 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
Asıl mesele, bu zorunluluğun TypeScript’te ne kadar ucuza gelmesi. Java ve C# da aynı eksik zinciri reddedebiliyor: staged builder ya da generic type-state bunu yapıyor. İkisi de fazladan interface veya tip parametresi istiyor, staged varyant ise çağrı sırasını da sabitliyor. O tarafta yaygın varsayılanın build() içinde runtime validasyon olmasının sebebi bu. TypeScript’te aynı garanti tek bir sınıf ve tiplenmiş bir this parametresiyle, üstelik istediğin çağrı sırasıyla geliyor.
Temel İmplementasyon: Tip Güvenli Lambda Builder#
Yukarıdaki Lambda problemine uygulandığında aynı fikir, düzgün konfigürasyonu zorunlu kılan bir builder üretiyor:
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.
Multi-Region Konfigürasyon Paylaşımı#
Builder’lar, multi-region serverless mimaride ortak kısmı tek yerde tutar:
// 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()
);
Region’lar arasındaki farklar açık ve kolayca fark edilebilir kalır. Yeni bir region eklendiğinde, tam olarak neyin farklı konfigüre edilmesi gerektiği açıktır. Bir uyarı: LambdaFunctionBuilder kendi state’ini değiştiriyor, yani baseBuilder önceki region’ın VPC ve environment değerlerini taşıyor. Ya her region için yeni bir builder oluştur ya da aşağıdaki immutable varyantı kullan.
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.
Tip güvenliği tek yönde, şemadan sonuca doğru akıyor:
Tip güvenli bir query builder’ın pratik örneği:
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: 'test@example.com' }).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. Bir sütun yeniden adlandırıldığında, eskide kalan her referans boş sonuç kümesi yerine derleme hatası veriyor.
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 Çok Getiri Sağlayan Alan#
Test data builder’lar Builder pattern için en iyi yatırım getirisini sağlar. 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);
});
});
Asıl kazanç model değiştiğinde ortaya çıkıyor. User tipine zorunlu bir alan eklendiğinde, kullanıcı oluşturan her test dosyasını değil, sadece builder’ın varsayılanlarını güncellemek yetiyor.
İleri Düzey TypeScript Teknikleri#
İki teknik, düz method chaining’in ötesine geçiyor ve ek tip makinesine değiyor.
Conditional Type’larla Aşamalı Tip Daraltma#
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.
Kullanım Senaryoları ve Sınırları#
Builder pattern her zaman doğru seçim değildir. Pratik bir karar çerçevesi:
Basit Alternatiflerin Yettiği Durumlar#
1. Nesne Basitse (2-3 özellik)
// Bad: Gereksiz karmaşıklık
new UserBuilder()
.withName('John')
.withEmail('john@example.com')
.build();
// Good
const user = { name: 'John', email: 'john@example.com' };
2. TypeScript’in Opsiyonel Parametreleri Yeterli
// Good: karmaşık kısıtlama yok
function createLogger(options?: {
level?: 'debug' | 'info' | 'warn' | 'error';
format?: 'json' | 'text';
}) {
return new Logger(options);
}
Builder’ın Kazandığı Durumlar#
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#
Sık tekrarlanan beş hata ve bunlardan nasıl kaçınılacağı:
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 processed = 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;
}
Varsayılanın Sınırları#
Varsayılan; bir kez yazılıp çok kez okunan, birbirine bağımlı kısıtlamalar taşıyan konfigürasyon nesneleri için geçerli: altyapı tanımları, query builder’lar ve test verisi fabrikaları. Buralarda compile-time kontrolü fazladan sınıfın maliyetini karşılıyor, çünkü alternatif kısıtlamayı deployment sırasında veya patlayan bir sorguda öğrenmek.
Üç durumda varsayılanı bırak. Nesnenin iki üç bağımsız özelliği varsa nesne literali daha anlaşılır. TypeScript’in opsiyonel parametreleri önemsediğin her kuralı zaten ifade ediyorsa options parametresi yeterli. Nesne oluşturma performans kritik bir path’teyse, her eleman için builder ayırmak kazandırdığından fazlasını götürüyor.
Makul bir başlangıç noktası build() içinde runtime validasyon; generic state takibini de sadece sürekli bozulan kısıtlamalar için ekle. Gelişmiş generic tipler derleme maliyetini her yerde değil, birkaç yerde hak ediyor.
Kaynaklar#
- TypeScript El Kitabı: Generic’ler (yeni sekmede açılır) - Tip güvenli builder uygulamalarının temelini oluşturan generic tipler hakkında resmi TypeScript belgesi.
- TypeScript El Kitabı: Koşullu Tipler (yeni sekmede açılır) - Gelişmiş builder kalıplarında aşamalı tip iyileştirmesi için kullanılan koşullu tipler hakkında resmi belge.
- TypeScript El Kitabı: Eşlenmiş Tipler (yeni sekmede açılır) - Builder API’lerinde konfigürasyon arayüzlerini dönüştürmek için kullanılan eşlenmiş tipler hakkında resmi referans.
- Builder Kalıbı: Refactoring.Guru (yeni sekmede açılır) - Amaç, yapı ve uygulanabilirliği diyagramlarla ele alan Builder yaratıcı kalıbının net açıklaması.
- Tasarım Kalıpları: Yeniden Kullanılabilir Nesne Yönelimli Yazılımın Öğeleri (Gamma ve diğerleri, 1994) (yeni sekmede açılır) - Builder kalıbını ve diğer 22 temel kalıbı tanımlayan orijinal Dörtlü Çete kitabı.
- TypeScript El Kitabı: Tiplerden Tip Oluşturma (yeni sekmede açılır) - Builder generic birikimine uygulanan tip dönüşüm tekniklerine (keyof, typeof, infer) genel bakış sunan resmi TypeScript belgesi.
İlgili yazılar
AppSync subscription'ları yalnızca mutation ile tetiklenir. Downstream BFF olaylarını NONE veri kaynaklı bir mutation'a EventBridge ve CDK ile köprülemeyi inceliyorum.
aws · graphql · serverless +4
AWS AppSync ile ölçeklenebilir real-time API'ler: JavaScript resolver'lar, subscription filtering, caching stratejileri ve infrastructure as code pattern'leri.
aws · graphql · serverless +4
CDK TypeScript Lambda için 9 bundler ve 3 cdk synth runner'ının ölçümlü karşılaştırması; her katman için varsayılan ve onu seçtiren kural.
aws-cdk · lambda · typescript +3
Mimari ağırlığını runtime'ın init-amortismanına göre seç: single-purpose Lambda'da yalın handler, Lambdalith'te orta, tam OOP/DI yalnızca uzun ömürlü runtime'da.
architecture · lambda · serverless +3
Gelişmiş Amazon Cognito teknik rehberi: özel auth akışları, federation, multi-tenancy, migration stratejileri ve CDK ile production-grade güvenlik.
aws · authentication · serverless +6