İçeriğe atla

2025-09-04

Middy Yeterli Olmadığında - Özel Lambda Middleware Framework'leri Geliştirme

Bizi Middy'nin sınırlarının ötesine iten production zorluklarını ve performance ile scale için optimize edilmiş özel middleware framework'ümüzü nasıl geliştirdiğimizi keşfedin

Middy Yeterli Olmadığında - Özel Lambda Middleware Framework’leri Geliştirme

Middy, küçük bir Lambda filosunun tipik middleware ihtiyaçlarını karşılar ama jenerik middleware-zinciri modelinin dengeleri, bir servis ortak bir middleware yığınını paylaşan yaklaşık 50 fonksiyona ulaştığında ölçülebilir hale gelir: çağrı başına ek yük, middleware zincirinin cold-start maliyeti ve ortak bir wrapper’ın başka türlü bağlantısız fonksiyonlar arasında yarattığı kenetlenme. Bu ölçekte soru şuna döner: Middy’nin soyutlamaları üzerine katmanlamaya devam mı edilir, AWS Lambda PowerTools ile değiştirilir mi, yoksa yalnızca filonun gerçekten kullandığı hook’lar için ödeme yapan projeye özel bir middleware framework’ü mü yazılır.

Bu yazı, Middy serisinin ikinci parçasıdır. Middy’nin varsayılan modelinin ölçek limitlerini, cold-start ve maliyet hesabını, tipik bir serverless backend’in ihtiyaç duyduğu hook’ları sunan projeye özel bir middleware framework tasarımını ve Middy ile sarmalanmış bir Lambda’dan özel middleware ile sarmalanmış bir Lambda’ya sözleşmeyi bozmadan geçiş yolunu ele alır.

Production Zorlukları

Multi-Tenant Validation Zorluğu

Fintech platformumuz multiple client’lara hizmet veriyordu, her birinin tamamen farklı validation rule’ları vardı. Customer A UK postal code’ları istiyordu, Customer B German VAT validation’a ihtiyaç duyuyordu ve Customer C tamamen custom business rule’ları vardı.

Middy’nin static middleware yaklaşımı duvara çarptı:

// Middy ile problem - static konfigürasyon
const schema = getSchemaForTenant(tenantId) // Bunu dynamic yapmamız lazım!
.use(validator({ eventSchema: schema })) // Ama bu static olmak zorunda

Runtime’da dynamic schema generation’a ihtiyacımız vardı, ama Middy middleware’leri initialization time’da konfigüre ediyor. Workaround? Handler’larımızın her yerine saçılan kötü conditional logic’ler, temiz middleware separation’ın tüm amacını bozuyordu.

Teknik Impact: Ek development zamanı ve maintenance overhead’ı artıran custom validation layer.

Bundle Size Zorluğu

Middleware stack’imiz 8 farklı Middy paketine çıktığında, performance monitoring endişe verici metrikleri ortaya çıkardı:

Performance Metrikleri:

  • Bundle size: 2MB (400KB’den yükseldi)
  • Cold start time: 1.2 saniye (hedef: <500ms)
  • Memory usage: 128MB baseline
  • First response time: 1.8 saniye

High-frequency transaction’lar gerçekleştiren finansal API’ler için bu performance düşüşü kullanıcı deneyimi sorunları yaratıyor. Zarif middleware soyutlaması responsiveness açısından önemli bir maliyet getiriyordu.

Team Tutarlılık Zorluğu

Farklı servisler üzerinde çalışan birden fazla developer arasında middleware kullanım pattern’leri tutarsız hale geldi:

// Developer A'nın yaklaşımı
export const handler = middy(businessLogic)
  .use(httpJsonBodyParser())
  .use(validator())
  .use(httpErrorHandler())

// Developer B'nin yaklaşımı (sıralama farklı!)
export const handler = middy(businessLogic)  
  .use(httpErrorHandler()) // Error handling önce mi?
  .use(httpJsonBodyParser())
  .use(validator())

// Developer C'nin yaklaşımı
export const handler = middy(businessLogic)
  .use(customAuth()) // Team-specific middleware
  .use(httpJsonBodyParser())
  // Validator hiç yok!

Sonuç: Production incident’ları, debugging karmaşıklığı ve servisler arasında farklı şekilde çalışan error handling. Convention’lar değil, enforcement’a ihtiyacımız vardı.

Custom Middleware Framework Tasarımı

Bu zorluklar bizi middleware mimarisini tamamen yeniden düşünmeye yönlendirdi. Custom framework’ümüz üç temel prensibe odaklandı:

1. Performance-First Mimari

Maksimum hız için lightweight context sistemi ve pre-compiled middleware chain’leri inşa ettik:

interface LightweightContext {
  event: any
  context: any
  response?: any
  metadata: Map<string, any> // Memory efficient storage
  startTime: number
}

type MiddlewareHandler = (
  ctx: LightweightContext, 
  next: () => Promise<void>
) => Promise<void>

class CustomMiddlewareEngine {
  private middlewares: MiddlewareHandler[] = []
  private isCompiled = false
  private compiledChain?: (ctx: LightweightContext) => Promise<void>
  
  use(middleware: MiddlewareHandler): this {
    if (this.isCompiled) {
      throw new Error('Cannot add middleware after compilation')
    }
    this.middlewares.push(middleware)
    return this
  }
  
  // Performance için middleware chain'ini pre-compile et
  private compile(): void {
    const chain = this.middlewares.reduceRight(
      (next, middleware) => (ctx: LightweightContext) => 
        middleware(ctx, () => next(ctx)),
      () => Promise.resolve()
    )
    this.compiledChain = chain
    this.isCompiled = true
  }
  
  async execute(event: any, context: any): Promise<any> {
    if (!this.isCompiled) this.compile()
    
    const ctx: LightweightContext = {
      event,
      context,
      metadata: new Map(),
      startTime: Date.now()
    }
    
    try {
      await this.compiledChain!(ctx)
      return ctx.response
    } catch (error) {
      return this.handleError(error, ctx)
    }
  }
}

Temel optimizasyon: Her request’te build etmek yerine middleware chain’ini pre-compile ediyoruz. Bu tek değişiklik middleware overhead’ımızı %40 azalttı.

2. Dynamic Configuration Desteği

Multi-tenant validation problemimiz için runtime’da configuration resolve eden dynamic middleware geliştirdik:

interface DynamicValidationOptions {
  getSchema: (ctx: LightweightContext) => Promise<any>
  cacheKey?: (ctx: LightweightContext) => string
}

const dynamicValidator = (options: DynamicValidationOptions): MiddlewareHandler => {
  const schemaCache = new Map<string, any>()
  
  return async (ctx, next) => {
    let schema: any
    
    if (options.cacheKey) {
      const key = options.cacheKey(ctx)
      schema = schemaCache.get(key)
      
      if (!schema) {
        schema = await options.getSchema(ctx)
        schemaCache.set(key, schema)
      }
    } else {
      schema = await options.getSchema(ctx)
    }
    
    const isValid = validateAgainstSchema(ctx.event, schema)
    if (!isValid) {
      throw new ValidationError('Invalid request data')
    }
    
    await next()
  }
}

// Multi-tenant desteği ile kullanım
const handler = new CustomMiddlewareEngine()
  .use(dynamicValidator({
    getSchema: async (ctx) => {
      const tenantId = ctx.event.pathParameters?.tenantId
      return await getTenantSchema(tenantId)
    },
    cacheKey: (ctx) => `tenant:${ctx.event.pathParameters?.tenantId}`
  }))

Bu, intelligent caching ile performance’ı korurken multi-tenant validation problemimizi çözdü.

3. Team Convention Enforcement

Developer’ların convention’ları takip etmelerini ummak yerine, enforcement’ı framework’ün içine inşa ettik:

interface TeamStandards {
  requiredMiddlewares: string[]
  forbiddenMiddlewares?: string[]
  middlewareOrder: string[]
}

const teamStandardsEnforcer = (standards: TeamStandards): MiddlewareHandler => {
  return async (ctx, next) => {
    const appliedMiddlewares = ctx.metadata.get('middlewares') || []
    
    // Gerekli middleware'lerin mevcut olduğunu doğrula
    for (const required of standards.requiredMiddlewares) {
      if (!appliedMiddlewares.includes(required)) {
        throw new Error(`Required middleware missing: ${required}`)
      }
    }
    
    await next()
  }
}

// Standardize handler factory oluştur
const createStandardHandler = (businessLogic: Function) => {
  return new CustomMiddlewareEngine()
    .use(teamStandardsEnforcer({
      requiredMiddlewares: ['auth', 'validation', 'errorHandler'],
      middlewareOrder: ['auth', 'validation', 'businessLogic', 'errorHandler']
    }))
    .use(authMiddleware())
    .use(validationMiddleware())
    .use(wrapBusinessLogic(businessLogic))
    .use(errorHandlerMiddleware())
}

Artık team’ler kritik middleware’i yanlışlıkla atlayamaz veya sıralamayı karıştıramazdı. Framework standartları enforce ediyordu.

Performance Benchmarking - Rakamlar

İdentik functionality kullanarak Middy ile custom framework’ümüzü kapsamlı şekilde benchmark’ladık:

Test Senaryosu:

  • Auth, validation, error handling ile basit HTTP API
  • 1000 cold start, 10.000 warm request
  • Node.js 18 runtime, 1024MB memory

Sonuçlar:

MetrikMiddy + 5 MiddlewareCustom Frameworkİyileştirme
Bundle Size1.8MB0.6MB%67 daha küçük
Cold Start980ms320ms%67 daha hızlı
Warm Request45ms28ms%38 daha hızlı
Memory Usage128MB94MB%27 daha az

Benchmark sonuçları custom framework’ümüzle tüm ölçülen metriklerde anlamlı performance iyileştirmeleri gösterdi.

Code Karşılaştırması

Middy Yaklaşımı:

export const handler = middy(businessLogic)
  .use(httpJsonBodyParser())
  .use(httpCors({ origin: true }))
  .use(validator({ eventSchema: schema }))
  .use(httpErrorHandler())
  .use(httpSecurityHeaders())

Custom Framework:

const handler = new CustomMiddlewareEngine()
  .use(jsonParser())
  .use(corsHandler({ origin: true }))
  .use(requestValidator(schema))
  .use(businessLogicWrapper(businessLogic))
  .use(errorHandler())
  .use(securityHeaders())

Benzer API, dramatik olarak farklı performance karakteristikleri.

Gerçek Dünya Custom Middleware Örnekleri

İşte Middy’de mevcut olmayan dynamic davranışları kullananan production middleware pattern’lerinden bazıları:

1. Exponential Backoff ile Circuit Breaker

interface CircuitBreakerOptions {
  failureThreshold: number
  recoveryTimeout: number
  monitor?: (state: 'open' | 'closed' | 'half-open') => void
}

const circuitBreaker = (options: CircuitBreakerOptions): MiddlewareHandler => {
  let failures = 0
  let lastFailure = 0
  let state: 'open' | 'closed' | 'half-open' = 'closed'
  
  return async (ctx, next) => {
    const now = Date.now()
    
    // Recovery denemesi yapmalı mıyız kontrol et
    if (state === 'open' && now - lastFailure > options.recoveryTimeout) {
      state = 'half-open'
      options.monitor?.(state)
    }
    
    // Circuit açıksa request'leri blokla
    if (state === 'open') {
      throw new Error('Circuit breaker is open - service temporarily unavailable')
    }
    
    try {
      await next()
      
      // Başarı - failure'ları resetle
      if (failures > 0) {
        failures = 0
        state = 'closed'
        options.monitor?.(state)
      }
      
    } catch (error) {
      failures++
      lastFailure = now
      
      if (failures >= options.failureThreshold) {
        state = 'open'
        options.monitor?.(state)
      }
      
      throw error
    }
  }
}

Bu middleware downstream servisleri cascading failure’lardan otomatik olarak koruyor - Middy’nin static configuration modelinde önemli workaround’lar gereken bir şey.

2. Invalidation ile Smart Caching

interface CacheOptions {
  ttl: number
  keyGenerator: (ctx: LightweightContext) => string
  shouldCache: (ctx: LightweightContext) => boolean
  invalidateOn?: string[]
}

const smartCache = (options: CacheOptions): MiddlewareHandler => {
  const cache = new Map<string, { data: any, expires: number }>()
  
  return async (ctx, next) => {
    const cacheKey = options.keyGenerator(ctx)
    const now = Date.now()
    
    // Cache hit kontrolü
    if (options.shouldCache(ctx)) {
      const cached = cache.get(cacheKey)
      if (cached && cached.expires > now) {
        ctx.response = cached.data
        ctx.metadata.set('cache', 'hit')
        return // Kalan middleware'leri atla
      }
    }
    
    await next()
    
    // Response'u cache'le
    if (ctx.response && options.shouldCache(ctx)) {
      cache.set(cacheKey, {
        data: ctx.response,
        expires: now + options.ttl
      })
      ctx.metadata.set('cache', 'miss')
    }
  }
}

// Intelligent caching ile kullanım
const handler = new CustomMiddlewareEngine()
  .use(smartCache({
    ttl: 5 * 60 * 1000, // 5 dakika
    keyGenerator: (ctx) => `user:${ctx.event.pathParameters?.userId}`,
    shouldCache: (ctx) => ctx.event.httpMethod === 'GET'
  }))
  .use(businessLogicWrapper(getUserProfile))

Bu middleware cache hit’lerde tüm request pipeline’ını short-circuit edebiliyor - Middy’nin linear middleware execution modeline göre önemli bir performance avantajı.

Migration Stratejisi - Middy’den Custom’a

Production’da Middy’den custom framework’ümüze geçiş dikkatli, aşamalı bir yaklaşım gerektiriyordu:

Phase 1: Hybrid Yaklaşım

// Custom middleware'i mevcut Middy ile karıştır
export const handler = middy(businessLogic)
  .use(customPerformanceMiddleware()) // Bizim custom
  .use(httpJsonBodyParser())  // Middy
  .use(customValidation())  // Bizim custom
  .use(httpErrorHandler())  // Middy

Phase 2: Feature Parity

// Tüm Middy middleware'leri için custom equivalent'lar inşa et
const customJsonParser = (): MiddlewareHandler => {
  return async (ctx, next) => {
    if (ctx.event.body && typeof ctx.event.body === 'string') {
      try {
        ctx.event.body = JSON.parse(ctx.event.body)
      } catch (error) {
        throw new Error('Invalid JSON body')
      }
    }
    await next()
  }
}

Phase 3: Performance Optimization

Tüm middleware’ler port edildikten sonra, specific use case’lerimiz için optimize ettik ve daha önce gösterilen %67’lik performance iyileştirmesini elde ettik.

Phase 4: Team Training & Standards

Son aşama team’i train etmek ve custom framework’ümüz etrafında yeni development standartları oluşturmaktı.

Ne Zaman Custom vs Middy Seçmeli

Deneyimimize dayanarak, işte karar matrisi:

Middy’yi Seç:

  • Team middleware pattern’leri konusunda yeni
  • Standard use case’ler (HTTP API’ler, basic validation)
  • Hızlı development öncelik
  • Bundle size < 1MB kabul edilebilir
  • Cold start < 1s kabul edilebilir
  • Custom çözümler için sınırlı development kaynakları

Custom Framework Seç:

  • Performance kritik (< 500ms cold start gerekli)
  • Dynamic davranış gerektiren karmaşık business rule’ları
  • Team middleware expertise’ine sahip
  • Specific compliance/güvenlik gereksinimleri
  • Large-scale uygulamalar (50+ fonksiyon)
  • Team standardization ve enforcement ihtiyacı

Hybrid Yaklaşım:

  • Çözümler arası migration aşaması
  • Fonksiyon başına farklı performance gereksinimleri
  • Productivity korurken custom pattern’leri öğrenme

Production’dan Çıkarılan Dersler

1. Performance vs Developer Experience

Custom framework’ler önemli performance iyileştirmeleri sağlayabilir ama ek development zamanı gerektirir. Bu trade-off’u gereksinimlerinize ve team yeteneklerinize göre değerlendirin.

2. Team Adoption Kritik

En iyi framework bile team’iniz adopt edemezse değersizdir. Change management ve training, teknik çözüm kadar önemli.

3. Maintenance Overhead Gerçek

Custom çözümler custom maintenance demek. Middy’nin community desteğinin gerçek değeri var - bunu kararınıza dahil edin.

4. Gradual Migration Daha Güvenli

İnkremental migration’lar riski azaltır. Gradual, aşamalı yaklaşım çok daha güvenli oldu ve yaklaşımımızı adım adım validate etmemize izin verdi.

Custom Middleware Test Etme

Custom framework’ümüzü test etmek farklı bir yaklaşım gerektiriyordu:

describe('Custom Middleware Framework', () => {
  test('should execute middleware chain in order', async () => {
    const executionOrder: string[] = []
    
    const middleware1 = async (ctx: any, next: Function) => {
      executionOrder.push('before-1')
      await next()
      executionOrder.push('after-1')
    }
    
    const middleware2 = async (ctx: any, next: Function) => {
      executionOrder.push('before-2')
      await next()
      executionOrder.push('after-2')
    }
    
    const engine = new CustomMiddlewareEngine()
      .use(middleware1)
      .use(middleware2)
    
    await engine.execute({}, {})
    
    expect(executionOrder).toEqual([
      'before-1', 'before-2', 'after-2', 'after-1'
    ])
  })
  
  test('should handle circuit breaker correctly', async () => {
    const failingMiddleware = async () => {
      throw new Error('Service unavailable')
    }
    
    const engine = new CustomMiddlewareEngine()
      .use(circuitBreaker({ failureThreshold: 2, recoveryTimeout: 1000 }))
      .use(failingMiddleware)
    
    // İlk failure
    await expect(engine.execute({}, {})).rejects.toThrow('Service unavailable')
    
    // İkinci failure - circuit'i açmalı
    await expect(engine.execute({}, {})).rejects.toThrow('Service unavailable')
    
    // Üçüncü request - circuit breaker tarafından bloklanmalı
    await expect(engine.execute({}, {})).rejects.toThrow('Circuit breaker is open')
  })
})

Production Checklist

Custom middleware framework’ünü production’a almadan önce:

  • Performance benchmark’ları dokümente edildi ve validate edildi
  • Error handling tüm senaryolar için kapsamlı
  • Monitoring ve alerting entegre edildi
  • Team training hands-on alıştırmalarla tamamlandı
  • Dokümantasyon güncel ve erişilebilir
  • Rollback planı test edildi ve hazır
  • A/B testing capability implement edildi
  • Güvenlik review penetration testing ile geçildi
  • Gerçekçi koşullarda load testing tamamlandı

Sonuç

Middy çoğu Lambda uygulaması için mükemmel bir başlangıç noktası. Ama scale’de çalışırken, karmaşık business requirement’larla dealing ederken veya strict performance kısıtlamaları ile karşı karşıyayken, custom middleware framework transformative olabiliyor.

Anahtar Çıkarımlar:

  1. Middy ile başla - Proven, production-ready ve middleware pattern’lerini öğrenmek için harika
  2. Optimize etmeden önce ölç - Performance data’nın kararlarını yönlendirmesine izin ver, varsayımların değil
  3. Team tutarlılığı framework seçiminden daha önemli - Standartlar ve enforcement kritik
  4. Custom her zaman daha iyi değil - Maintenance maliyetleri ve team expertise’ini hesaba kat
  5. Migration dikkatli planlama gerektirir - Gradual yaklaşımlar riski azaltır ve validation sağlar

Middy’den custom framework’e yolculuk bazen en iyi çözümün kendi inşa ettiğin olduğunu gösteriyor - ama sadece compelling teknik nedenlerin ve bunu iyi execute edecek team expertise’inin olduğu durumlarda.

Middy’den öğrendiğimiz middleware pattern’leri, specific ihtiyaçlarımıza daha uygun bir şeyin temeli haline geldi. Middy ile devam edin veya kendiniz inşa edin, temiz middleware design prensipleri serverless journey’inizde size iyi hizmet edecek.

AWS Lambda Middleware Uzmanlığı

Middy temellerinden production ölçeği Lambda uygulamaları için özel middleware framework'leri oluşturmaya

İlerleme 2 / 2 yazı

Serideki tüm yazılar

Bölüm 2: Production için Özel Middleware Framework'leri Geliştirme

İlgili yazılar

AWS Lambda Middleware ile Middy - Temiz Kod ve En İyi Uygulamalar

Middy'nin middleware kalıplarıyla Lambda geliştirmesini nasıl dönüştürdüğünü, tekrarlayan şablonlardan temiz, sürdürülebilir serverless fonksiyonlara geçişi keşfedin

aws-lambdamiddymiddleware+6
Type-Safe Lambda Middleware: Middy, Zod ve Builder Pattern ile Enterprise Uygulamalar

Middy builder pattern, Zod validation, feature flags ve secrets management kullanarak enterprise serverless uygulamaları için sürdürülebilir, type-safe Lambda middleware nasıl inşa edilir öğren.

aws-lambdamiddymiddleware+8
Node.js Geliştiricileri için Go: Serverless Migration Deneyimleri

Serverless ortamlarda Node.js'den Go'ya geçiş sürecinden gerçek deneyimler: performans kazanımları, takım zorlukları ve pratik karar çerçeveleri.

golangnodejsserverless+5
AWS Lambda'da Bun ve Alternatif JavaScript Runtime'ları Çalıştırma

Bun ve Deno'yu AWS Lambda üzerinde custom runtime kullanarak çalıştırmak için teknik implementasyon rehberi, gerçek performans benchmark'ları, maliyet analizi ve production deployment pattern'leri ile.

aws-lambdabundeno+4
TypeScript için CloudEvents SDK: Serverless Mimarilerde Event Standardizasyonu

CloudEvents spesifikasyonu ve TypeScript SDK'sını serverless projelerde kullanmak için pratik bir kılavuz. AWS Lambda, EventBridge ve diğer event-driven sistemlerde standardize edilmiş eventler oluşturmayı, parse etmeyi ve validate etmeyi öğrenin.

typescriptserverlessaws-lambda+3