İçeriğe atla

Middy ve Zod: Type-Safe AWS Lambda Middleware Doğrulama

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

Ayhan Sipahi Ayhan Sipahi

Lambda codebase’inde middleware zamanla dağılır. Proje birkaç fonksiyonu geçtiğinde her handler kendi Middy zincirini kuruyor: error handler’lar farklı pozisyonlara düşüyor, bazı endpoint’ler authenticate ediyor bazıları etmiyor ve handler imzasıyla artık uyuşmayan bir schema ancak trafik geldiğinde hata veriyor.

Middy’nin önüne konan ince bir builder bunu çözer. Middleware sırasını tek yerde sabitler, context type’larını ileri taşır (böylece TypeScript handler’ın ne aldığını bilir) ve validation için JSON Schema yerine Zod kullanır. Bedeli birkaç yüz satır glue kod ve biraz daha ağır bir cold start; birkaç endpoint’i geçen her API için bu takas kârlı.

Standart Middleware Kurulumunun Sorunları#

Elle kurulan bir zincir tek dosyada sorunsuz görünür. Yirmi handler’a yayıldığında ise gözden geçirilemez hale gelir: sırayı zorlayan bir şey yoktur ve schema ile handler type’ları arasında bağ kurulmaz.

Eğer Middy’ye yeniysen, temel konseptler ve pattern’lar için Middy ile AWS Lambda middleware yazımıza göz at.

Lambda codebase’lerinde genellikle şunu görüyorum:

// Hata yapmak çok kolay - compile-time checking yok
export const handler = middy(businessLogic)
  .use(httpErrorHandler()) // Bu ilk mi son mu olmalı?
  .use(validator({ eventSchema })) // Schema'nın event type'a uyduğunu doğrulama yok
  .use(httpJsonBodyParser())
  .use(httpCors())
  // Authentication middleware'i unutulmuş!

Yaygın Sorunlar:

  • Middleware sıralamasının zorlanamaması (error handler yanlış pozisyonda)
  • Validation ile handler arasında type safety kopuyor (schema handler type’ları ile eşleşmiyor)
  • Fonksiyonlar arası tutarsız pattern’lar (bazılarında auth var, bazılarında yok)
  • Anlaşılmaz JSON Schema validation hataları
  • Feature flags ve secrets için tekrarlanan kod

Teknik Gereksinimler#

Bu zorlukları ele almak için enterprise middleware sisteminin ihtiyaç duyduğu özellikler:

  1. Compile-time type safety: Configuration hatalarını deployment öncesi yakala
  2. Enforced middleware ordering: Tüm fonksiyonlarda tutarlı execution
  3. Daha iyi validation hataları: Schema validation’dan net, actionable mesajlar
  4. Feature flag integration: Kod deployment olmadan feature toggle
  5. Secrets management: Cache’lenmiş, rotation-aware secret erişimi
  6. Discoverable API: Autocomplete ve type hints ile developer guidance
  7. Testability: Middleware chain’lerini mock etmek ve test etmek kolay olmalı

Runtime Önerisi: Lambda fonksiyonları için Node.js 22.x kullan. Node.js 16 zaten deprecated, Node.js 18 tam deprecation’a 9 Mart 2026’da ulaştı ve Node.js 20 end-of-life’a 30 Nisan 2026’da ulaşacak. Serverless uygulamalarda TypeScript pattern’ları ve best practice’ler için TypeScript ile AWS Serverless yazımıza göz at.

Type-Safe Builder Pattern Kurulumu#

Builder pattern, middleware composition hakkında compile-time guarantee’ler sağlar. Her builder metodu, zenginleştirilmiş context ile yeni bir type döner, TypeScript’in handler’ınızda tam olarak neyin mevcut olduğunu bilmesini sağlar.

Builder’ın Çekirdeği#

interface MiddlewareConfig {
  enableAuth: boolean
  enableCors: boolean
  validationSchema?: z.ZodSchema
  featureFlags?: string[]
  secrets?: string[]
}

class LambdaMiddlewareBuilder<TEvent, TContext = {}> {
  private config: Partial<MiddlewareConfig> = {}

  withAuthentication(): LambdaMiddlewareBuilder<TEvent, TContext & { userId: string }> {
    this.config.enableAuth = true
    return this as any
  }

  withValidation<TSchema extends z.ZodSchema>(
    schema: TSchema
  ): LambdaMiddlewareBuilder<z.infer<TSchema>, TContext> {
    this.config.validationSchema = schema
    return this as any
  }

  withFeatureFlags(
    flags: string[]
  ): LambdaMiddlewareBuilder<TEvent, TContext & { features: Record<string, boolean> }> {
    this.config.featureFlags = flags
    return this as any
  }

  withSecrets(
    secrets: string[]
  ): LambdaMiddlewareBuilder<TEvent, TContext & { secrets: Record<string, string> }> {
    this.config.secrets = secrets
    return this as any
  }

  // Not: Bu implementation basitlik için `as any` kullanıyor. Production implementation'lar
  // tam type safety'yi korumak için mapped types veya conditional types gibi daha sofistike
  // TypeScript teknikleri kullanabilir.

  build(handler: (event: TEvent, context: TContext) => Promise<any>) {
    const middlewareChain = middy(handler)

    // Tutarlı sıralamayı zorla
    if (this.config.enableCors) {
      middlewareChain.use(httpCors())
    }

    middlewareChain.use(httpJsonBodyParser())

    if (this.config.validationSchema) {
      middlewareChain.use(zodValidationMiddleware(this.config.validationSchema))
    }

    if (this.config.enableAuth) {
      middlewareChain.use(authenticationMiddleware())
    }

    if (this.config.featureFlags) {
      middlewareChain.use(featureFlagsMiddleware(this.config.featureFlags))
    }

    if (this.config.secrets) {
      middlewareChain.use(secretsMiddleware(this.config.secrets))
    }

    middlewareChain.use(httpErrorHandler())

    return middlewareChain
  }
}

Full Type Safety ile Kullanım#

const requestSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  tenantId: z.string().uuid()
})

export const handler = new LambdaMiddlewareBuilder()
  .withAuthentication()
  .withValidation(requestSchema)
  .withFeatureFlags(['newLoginFlow', 'mfaEnabled'])
  .withSecrets(['DATABASE_URL', 'JWT_SECRET'])
  .build(async (event, context) => {
    // TypeScript biliyor:
    // - event requestSchema ile eşleşiyor (email, password, tenantId)
    // - context'te userId var (auth'dan)
    // - context'te features object var
    // - context'te secrets object var

    if (context.features.mfaEnabled) {
      // MFA flow'u handle et
    }

    const dbUrl = context.secrets.DATABASE_URL
    // Business logic tam type safety ile
  })

Temel Faydalar:

  • Context type’larının compile-time checking’i
  • Zorlanmış middleware sıralaması
  • Autocomplete ile discoverable API
  • Middleware configuration için tek gerçek kaynak

withAuthentication

withValidation

withFeatureFlags

withSecrets

build

LambdaMiddlewareBuilder

Builder + Auth Context

Builder + Validated Event Type

Builder + Features Context

Builder + Secrets Context

Typed Handler Function

Zod Validation Middleware#

@middy/validator JSON Schema kullanır, bu da TypeScript entegrasyonundan yoksundur ve anlaşılmaz hata mesajları verir. Zod her iki problemi de zarif bir şekilde çözer.

Zod’u Lambda ile kullanmak ve OpenAPI entegrasyonu için kapsamlı bir rehber için Zod + OpenAPI + AWS Lambda yazımıza göz at.

Custom Zod Middleware#

import { z } from 'zod'
import createHttpError from 'http-errors'

const zodValidationMiddleware = <T extends z.ZodSchema>(schema: T) => {
  return {
    before: async (request: middy.Request) => {
      const body = request.event.body

      const result = schema.safeParse(body)

      if (!result.success) {
        // Zod hatalarını kullanıcı dostu mesajlara dönüştür
        const errors = result.error.errors.map(err => ({
          field: err.path.join('.'),
          message: err.message,
          code: err.code
        }))

        throw createHttpError(400, 'Validation failed', { errors })
      }

      // event.body'yi validate edilmiş, typed data ile değiştir
      request.event.body = result.data
    }
  }
}

Zengin Hata Mesajları#

const userSchema = z.object({
  email: z.string().email('Lütfen geçerli bir email adresi girin'),
  age: z.number().int().min(18, 'En az 18 yaşında olmalısınız'),
  phone: z.string().regex(/^\+?[1-9]\d{1,14}$/, 'Geçersiz telefon numarası formatı'),
  acceptedTerms: z.boolean().refine(val => val === true, {
    message: 'Kullanım koşullarını kabul etmelisiniz'
  })
})

// Örnek hata response'u:
// {
//  "statusCode": 400,
//  "message": "Validation failed",
//  "errors": [
//  {
//  "field": "email",
//  "message": "Lütfen geçerli bir email adresi girin",
//  "code": "invalid_string"
//  },
//  {
//  "field": "age",
//  "message": "En az 18 yaşında olmalısınız",
//  "code": "too_small"
//  }
//  ]
// }

Advanced Validation Pattern’ları#

Zod karmaşık validation senaryolarında mükemmel:

// Cross-field validation
const orderSchema = z.object({
  items: z.array(z.object({
    productId: z.string().uuid(),
    quantity: z.number().int().positive()
  })).min(1, 'Sipariş en az bir ürün içermelidir'),
  total: z.number().positive()
}).refine(data => {
  // Total'ın ürünlerin toplamıyla eşleştiğini doğrula
  const calculatedTotal = data.items.reduce((sum, item) =>
    sum + (item.quantity * getPriceForProduct(item.productId)), 0
  )
  return Math.abs(calculatedTotal - data.total) < 0.01
}, {
  message: 'Sipariş toplamı ürün fiyatları ile eşleşmiyor',
  path: ['total']
})

// Polymorphic input'lar için discriminated unions
const notificationSchema = z.discriminatedUnion('type', [
  z.object({
    type: z.literal('email'),
    recipient: z.string().email(),
    subject: z.string(),
    body: z.string()
  }),
  z.object({
    type: z.literal('sms'),
    phoneNumber: z.string(),
    message: z.string().max(160)
  }),
  z.object({
    type: z.literal('push'),
    deviceToken: z.string(),
    title: z.string(),
    body: z.string()
  })
])

Discriminated union, type alanına göre type narrowing sağlar, her variant için tam type safety verir.

Feature Flags Middleware#

Feature flags, kod redeploy olmadan dinamik davranış değişikliklerini mümkün kılar. AWS AppConfig, proper caching ile enterprise-grade feature flag yönetimi sağlar.

AppConfig ile Implementation#

import axios from 'axios'

interface FeatureFlagsContext {
  features: Record<string, boolean>
}

const featureFlagsMiddleware = (flagNames: string[]) => {
  // Lambda container seviyesinde cache
  let cachedFlags: Record<string, boolean> | null = null
  let lastFetchTime = 0
  const CACHE_TTL_MS = 30000 // 30 saniye

  return {
    before: async (request: middy.Request<any, FeatureFlagsContext>) => {
      const now = Date.now()

      // Cache hala fresh ise kullan
      if (cachedFlags && (now - lastFetchTime) < CACHE_TTL_MS) {
        request.context.features = cachedFlags
        return
      }

      try {
        // AppConfig Lambda Extension'dan fetch et (localhost endpoint)
        const response = await axios.get(
          `http://localhost:2772/applications/${process.env.APPCONFIG_APP}/environments/${process.env.APPCONFIG_ENV}/configurations/${process.env.APPCONFIG_CONFIG}`,
          { timeout: 3000 }
        )

        const allFlags = response.data

        // Sadece istenen flag'leri çıkar
        const features: Record<string, boolean> = {}
        flagNames.forEach(name => {
          features[name] = allFlags[name] ?? false
        })

        cachedFlags = features
        lastFetchTime = now
        request.context.features = features
      } catch (error) {
        console.error('Feature flags fetch edilemedi:', error)
        // Tüm flag'ler disabled olarak fail open
        request.context.features = Object.fromEntries(
          flagNames.map(name => [name, false])
        )
      }
    }
  }
}

Advanced Pattern: User-Specific Flags#

Daha sofistike senaryolar için percentage rollout ve user targeting implement edebilirsiniz:

interface FeatureFlagConfig {
  enabled: boolean
  rolloutPercentage?: number
  targetUserIds?: string[]
  targetTenants?: string[]
}

const advancedFeatureFlagsMiddleware = (flagNames: string[]) => {
  return {
    before: async (request: middy.Request) => {
      const allFlags = await fetchFlags()
      const userId = request.context.userId // Auth middleware'den
      const tenantId = request.event.body?.tenantId

      const features: Record<string, boolean> = {}

      for (const flagName of flagNames) {
        const config: FeatureFlagConfig = allFlags[flagName]

        if (!config?.enabled) {
          features[flagName] = false
          continue
        }

        // User targeting kontrolü
        if (config.targetUserIds?.includes(userId)) {
          features[flagName] = true
          continue
        }

        // Tenant targeting kontrolü
        if (config.targetTenants?.includes(tenantId)) {
          features[flagName] = true
          continue
        }

        // Percentage rollout kontrolü
        if (config.rolloutPercentage) {
          const hash = hashString(`${flagName}:${userId}`)
          const userPercentage = (hash % 100) + 1
          features[flagName] = userPercentage <= config.rolloutPercentage
          continue
        }

        features[flagName] = config.enabled
      }

      request.context.features = features
    }
  }
}

Lambda Extension Setup#

Serverless configuration’da AppConfig Lambda Extension’ı yapılandır:

# serverless.yml veya SAM template
provider:
  environment:
    AWS_APPCONFIG_EXTENSION_POLL_INTERVAL_SECONDS: 30
    AWS_APPCONFIG_EXTENSION_POLL_TIMEOUT_MILLIS: 3000
    APPCONFIG_APP: MyApplication
    APPCONFIG_ENV: ${opt:stage}
    APPCONFIG_CONFIG: feature-flags

  iamRoleStatements:
    - Effect: Allow
      Action:
        - appconfig:GetConfiguration
        - appconfig:GetLatestConfiguration
        - appconfig:StartConfigurationSession
      Resource: '*'

functions:
  api:
    handler: handler.main
    layers:
      # AppConfig Lambda Extension (region-specific ARN)
      # Region'ınızdaki en son version için kontrol edin:
      # https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions-versions.html
      - arn:aws:lambda:us-east-1:027255383542:layer:AWS-AppConfig-Extension:207

Secrets Management Middleware#

AWS Secrets Manager entegrasyonu, API throttling’i önlemek ve zero-downtime rotation’ı desteklemek için proper caching ve rotation handling gerektirir.

Temel Secrets Middleware#

import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'

interface SecretsContext {
  secrets: Record<string, string>
}

const secretsMiddleware = (secretNames: string[]) => {
  // Lambda container-level cache
  const secretCache = new Map<string, { value: string; fetchedAt: number }>()
  const CACHE_TTL_MS = 300000 // 5 dakika

  // Client'ı invocation'lar arası yeniden kullan
  const client = new SecretsManagerClient({ region: process.env.AWS_REGION })

  return {
    before: async (request: middy.Request<any, SecretsContext>) => {
      const secrets: Record<string, string> = {}
      const now = Date.now()

      // Secret'ları parallel fetch et
      await Promise.all(
        secretNames.map(async (secretName) => {
          // Önce cache kontrolü
          const cached = secretCache.get(secretName)
          if (cached && (now - cached.fetchedAt) < CACHE_TTL_MS) {
            secrets[secretName] = cached.value
            return
          }

          try {
            const command = new GetSecretValueCommand({ SecretId: secretName })
            const response = await client.send(command)
            const secretValue = response.SecretString || ''

            secrets[secretName] = secretValue
            secretCache.set(secretName, { value: secretValue, fetchedAt: now })
          } catch (error) {
            console.error(`Secret fetch edilemedi ${secretName}:`, error)
            // Stale olsa bile cache'lenmiş değeri kullan veya fail
            const cached = secretCache.get(secretName)
            if (cached) {
              console.warn(`Stale cache'lenmiş secret kullanılıyor ${secretName}`)
              secrets[secretName] = cached.value
            } else {
              throw new Error(`Gerekli secret mevcut değil ${secretName}`)
            }
          }
        })
      )

      request.context.secrets = secrets
    }
  }
}

Parsing ile Structured Secrets#

Birçok secret JSON object’tir. Type safety’yi korumak için parsing desteği ekle:

interface DatabaseConfig {
  host: string
  port: number
  username: string
  password: string
  database: string
}

const secretsWithParsingMiddleware = (secretConfigs: Array<{
  name: string
  parser?: (raw: string) => any
}>) => {
  return {
    before: async (request: middy.Request) => {
      const rawSecrets = await fetchSecrets(secretConfigs.map(c => c.name))

      const secrets: Record<string, any> = {}

      for (const config of secretConfigs) {
        const rawValue = rawSecrets[config.name]
        secrets[config.name] = config.parser
          ? config.parser(rawValue)
          : rawValue
      }

      request.context.secrets = secrets
    }
  }
}

// Kullanım
export const handler = new LambdaMiddlewareBuilder()
  .withSecrets([
    {
      name: 'prod/database/credentials',
      parser: (raw) => JSON.parse(raw) as DatabaseConfig
    },
    {
      name: 'prod/api/keys',
      parser: (raw) => JSON.parse(raw)
    }
  ])
  .build(async (event, context) => {
    const dbConfig = context.secrets['prod/database/credentials'] as DatabaseConfig
    const connection = await createConnection({
      host: dbConfig.host,
      port: dbConfig.port,
      // TypeScript structure'ı biliyor!
    })
  })

Bütünleşik Örnek: Sipariş Endpoint’i#

Hadi her şeyi bir e-commerce API endpoint’inde birleştirelim:

// schemas/order.schema.ts
import { z } from 'zod'

export const createOrderSchema = z.object({
  items: z.array(z.object({
    productId: z.string().uuid(),
    quantity: z.number().int().positive(),
    price: z.number().positive()
  })).min(1),
  shippingAddress: z.object({
    street: z.string().min(1),
    city: z.string().min(1),
    postalCode: z.string(),
    country: z.string().length(2)
  }),
  paymentMethodId: z.string()
})

// handlers/orders.ts
export const createOrder = new LambdaMiddlewareBuilder()
  .withCors()
  .withAuthentication()
  .withValidation(createOrderSchema)
  .withFeatureFlags(['expressFulfillment', 'fraudDetection', 'loyaltyProgram'])
  .withSecrets(['database-credentials', 'payment-api-key'])
  .build(async (event, context) => {
    // TypeScript tüm bu type'ları biliyor!
    const { items, shippingAddress, paymentMethodId } = event.body
    const { userId } = context
    const { expressFulfillment, fraudDetection, loyaltyProgram } = context.features
    const dbCreds = JSON.parse(context.secrets['database-credentials'])
    const paymentKey = context.secrets['payment-api-key']

    // Fraud detection enabled ise uygula
    if (fraudDetection) {
      const riskScore = await checkFraudRisk(userId, items, shippingAddress)
      if (riskScore > 0.8) {
        return {
          statusCode: 400,
          body: JSON.stringify({ error: 'Sipariş manuel incelemeye alındı' })
        }
      }
    }

    // Loyalty program enabled ise puan hesapla
    let loyaltyPoints = 0
    if (loyaltyProgram) {
      loyaltyPoints = calculateLoyaltyPoints(items)
    }

    // Express fulfillment opsiyonu ile sipariş oluştur
    const order = await createOrderInDatabase(dbCreds, {
      userId,
      items,
      shippingAddress,
      paymentMethodId,
      expressDelivery: expressFulfillment,
      loyaltyPoints
    })

    // Ödemeyi işle
    await processPayment(paymentKey, {
      amount: order.total,
      paymentMethodId
    })

    return {
      statusCode: 201,
      body: JSON.stringify({
        orderId: order.id,
        estimatedDelivery: expressFulfillment
          ? addDays(new Date(), 1)
          : addDays(new Date(), 5),
        loyaltyPointsEarned: loyaltyPoints
      })
    }
  })

Yukarıdaki endpoint parçaları bir araya getiriyor:

  • Zod ile type-safe validation
  • Gradual rollout için dinamik feature flags
  • Güvenli secrets management
  • Her yerde tam TypeScript type inference
DatabaseSecretsManagerAppConfigMiddlewareLambdaClientDatabaseSecretsManagerAppConfigMiddlewareLambdaClientPOST /ordersRequest'i işleCORS & JSON ParseZod ile ValidateUser authenticateFeature flags fetch (cached)Flag'leri döndürSecrets fetch (cached)Credential'ları döndürHandler'ı execute etSipariş oluşturSipariş oluşturuldu201 Created

Test Stratejileri#

Builder pattern, composition ve injection yoluyla test etmeyi önemli ölçüde kolaylaştırır.

Middleware Context Mock’lama#

// tests/orders.test.ts
import { createOrder } from '../handlers/orders'

describe('Create Order Handler', () => {
  it('flag enabled olduğunda express fulfillment ile sipariş oluşturmalı', async () => {
    const mockEvent = {
      body: {
        items: [{ productId: '123', quantity: 2, price: 29.99 }],
        shippingAddress: {
          street: '123 Main St',
          city: 'Seattle',
          postalCode: '98101',
          country: 'US'
        },
        paymentMethodId: 'pm_123'
      }
    }

    const mockContext = {
      userId: 'user-123',
      features: {
        expressFulfillment: true,
        fraudDetection: false,
        loyaltyProgram: true
      },
      secrets: {
        'database-credentials': JSON.stringify({
          host: 'localhost',
          port: 5432,
          username: 'test',
          password: 'test'
        }),
        'payment-api-key': 'test-key'
      }
    }

    const response = await createOrder.handler(mockEvent, mockContext)

    expect(response.statusCode).toBe(201)
    const body = JSON.parse(response.body)
    expect(body.loyaltyPointsEarned).toBeGreaterThan(0)
  })
})

Test Builder Pattern#

Builder pattern’ı yansıtan bir test helper oluştur:

class TestMiddlewareBuilder {
  private features: Record<string, boolean> = {}
  private secrets: Record<string, string> = {}
  private userId = 'test-user'

  withFeature(name: string, enabled: boolean): this {
    this.features[name] = enabled
    return this
  }

  withSecret(name: string, value: string): this {
    this.secrets[name] = value
    return this
  }

  withUserId(id: string): this {
    this.userId = id
    return this
  }

  buildContext() {
    return {
      userId: this.userId,
      features: this.features,
      secrets: this.secrets
    }
  }
}

// Testlerde kullanım
const context = new TestMiddlewareBuilder()
  .withFeature('expressFulfillment', true)
  .withFeature('fraudDetection', false)
  .withSecret('database-credentials', '{"host":"localhost"}')
  .withUserId('test-123')
  .buildContext()

Bu yaklaşım test setup için aynı fluent API’yi sağlar, testleri okunabilir ve sürdürülebilir kılar.

Performans Etkileri#

Performans etkilerini anlamak, bilinçli trade-off’lar yapmanıza yardımcı olur.

Cold Start Etkisi#

Zincirdeki her middleware, ilk invocation dönmeden önce yüklenip initialize olan modül kodudur. Bu yığında maliyeti belirleyen üç şey var: Zod schema kurulumu, oluşturduğun AWS SDK v3 client’ları ve AppConfig extension’ının kendi açılışı. Builder’ın runtime maliyeti yok; sadece .use() çağrılarını sabit bir sırayla yapıyor.

Bu yükü bir kerelik container initialization maliyeti olarak gör ve genel bir rakama güvenmek yerine kendi bundle’ında ölç; sonucu paket boyutu ve SDK client sayısı belirliyor. Detaylı cold start optimizasyon stratejileri için AWS Lambda Cold Start Optimizasyonu yazımıza göz at.

Memory Kullanımı#

Middy’nin çekirdeği küçük. Asıl memory’yi Zod, import ettiğin AWS SDK v3 client’ları ve fonksiyonun yanında çalışan extension’lar tüketiyor. Middleware ekledikten sonra Lambda REPORT satırındaki Max Memory Used değerini oku ve fonksiyonu ona göre boyutlandır; birkaç SDK client bellekte dururken 128 MB tabanı göründüğünden daha az pay bırakıyor.

Optimizasyon Stratejileri#

1. Connection Reuse

// AWS SDK client'larını module scope'da tut
const secretsClient = new SecretsManagerClient({ region: process.env.AWS_REGION })

const secretsMiddleware = (names: string[]) => {
  return {
    before: async (request) => {
      // Client'ı invocation'lar arası yeniden kullan
      const secrets = await fetchSecretsWithClient(secretsClient, names)
      request.context.secrets = secrets
    }
  }
}

2. Selective Middleware Sadece ihtiyaç duyduğun middleware’i dahil et:

// Hafif public endpoint
const publicHandler = new LambdaMiddlewareBuilder()
  .withCors()
  .withValidation(schema)
  .build(handler)

// Full-featured authenticated endpoint
const privateHandler = new LambdaMiddlewareBuilder()
  .withCors()
  .withAuthentication()
  .withValidation(schema)
  .withFeatureFlags(['feature1', 'feature2'])
  .withSecrets(['secret1'])
  .build(handler)

3. Cache Warming Container initialization sırasında pre-fetch:

// module-level initialization
let warmCache: Promise<void> | null = null

if (!warmCache) {
  warmCache = (async () => {
    await Promise.all([
      prefetchFeatureFlags(),
      prefetchSecrets()
    ])
  })()
}

Maliyet Analizi#

Bu zincirdeki iki managed servis Lambda’dan ayrı faturalanıyor. Aşağıdaki birim fiyatlar AWS liste fiyatları (kaynaklarda bağlantılı); request sayıları ise yukarıdaki koddaki cache ayarlarından çıkıyor, dolayısıyla bir TTL’i değiştirirsen hesabı yeniden yap.

AWS Servis Maliyetleri#

AppConfig (Feature Flags):

  • API request’ler: $0.20 per 1M request
  • Alınan configuration’lar: $0.0008 per configuration ($800 per 1M)
  • 30 saniyede bir poll eden bir container, warm kaldığı sürece günde yaklaşık 2.880 request yapar
  • Sürekli warm 10 fonksiyon: API request’leri için kabaca $0.17/ay
  • Configuration başına ücret her poll’de değil, configuration gerçekten değiştiğinde işler; haftada birkaç kez değişen flag’lerde sıfıra yakın kalır
  • Değerlendirme: Poll aralığını saniyelere indirmediğin sürece, kazandırdığı operasyonel esneklik yanında ihmal edilebilir

Secrets Manager:

  • Secret storage: $0.40/ay per secret
  • API request’ler: $0.05 per 10,000 request
  • 5 dakikalık caching ile: warm her fonksiyonda secret başına ~288 request/gün, çünkü cache secret adına göre tutuluyor
  • 10 fonksiyonda 5 secret: ayda yaklaşık 432.000 request, yani $2.00 storage ve üstüne kabaca $2.16 request maliyeti
  • Trade-off: Parameter Store’dan daha yüksek maliyet, ancak otomatik rotation desteği

Lambda Extension Overhead:

  • Extension’lar aynı execution environment’ta çalışır ve fonksiyonun memory allocation’ından pay alır
  • Execution cost üzerinde minimal etki
  • External API call’ları önemli ölçüde azaltır

Yaygın Tuzaklar ve Çözümler#

Implementation’ların ters gittiği durumlardan çıkan dersler aşağıda ele alınıyor.

1. Feature Flag Cache Staleness#

Problem: Lambda container’lar saatlerce yaşayabilir, stale feature flag değerleri kullanır.

Çözüm: Emergency override ile TTL-based cache refresh implement et:

const CACHE_TTL = process.env.FEATURE_FLAG_TTL
  ? parseInt(process.env.FEATURE_FLAG_TTL)
  : 30000 // 30 saniye default

// Emergency override sağla
if (process.env.BYPASS_FLAG_CACHE === 'true') {
  // Her zaman fresh flag'leri fetch et (kritik güncellemeler için)
}

2. Secret Rotation Timing#

Problem: Secrets Manager secret’ları rotate eder, ancak Lambda’daki cache’lenmiş değerler auth failure’lara neden olur.

Çözüm: Retry logic ile rotation-aware caching implement et:

const secretsMiddleware = () => {
  return {
    before: async (request) => {
      try {
        request.context.secrets = await fetchSecrets()
      } catch (error) {
        if (isAuthError(error)) {
          // Cache'i temizle ve bir kez retry et
          clearSecretCache()
          request.context.secrets = await fetchSecrets()
        } else {
          throw error
        }
      }
    }
  }
}

3. Middleware Sıralama Sorunları#

Problem: Error handler son olmalı, ancak builder pattern middleware’i yanlış sırayla eklemeyi kolaylaştırır.

Çözüm: Builder sıralamayı internal olarak zorlar:

class SafeBuilder {
  build(handler: any) {
    const chain = middy(handler)

    // Core middleware belirli sırada
    chain.use(httpJsonBodyParser())  // 1. Body'yi parse et
    // ... validation, auth, etc
    chain.use(httpErrorHandler())  // Son: Hataları handle et

    return chain
  }
}

Alternatif Yaklaşımlar#

Bilinçli kararlar vermek için alternatifleri anlamak önemli.

Middleware execution üzerinde daha fazla kontrole veya özel performans gereksinimlerine ihtiyaç duyduğun senaryolar için, Middy’nin yeteneklerinin ötesine geçen custom middleware framework’leri inşa etme yazımızı oku.

vs. AWS Lambda Powertools#

AWS Lambda Powertools:

import { Logger, Tracer, Metrics } from '@aws-lambda-powertools/logger'
import { parser } from '@aws-lambda-powertools/parser'

@parser({ schema: mySchema })
export const handler = async (event, context) => {
  logger.info('Request işleniyor', { event })
}

Karşılaştırma:

  • Powertools: Daha iyi observability, AWS-maintained, comprehensive feature’lar
  • Custom Builder: Middleware composition ile daha fazla esneklik, daha küçük bundle
  • Öneri: Her ikisini birleştir - logging/tracing için Powertools, business middleware için custom builder kullan

vs. Pure Functional Middleware#

Functional Yaklaşım:

type Middleware<T> = (next: Handler<T>) => Handler<T>

const compose = <T>(...middlewares: Middleware<T>[]) =>
  (handler: Handler<T>) =>
    middlewares.reduceRight((next, middleware) => middleware(next), handler)

export const handler = compose(
  withAuth,
  withValidation(schema),
  withFeatureFlags(['flag1'])
)(businessLogic)

Trade-off: Functional composition zarif ama context enrichment için daha az TypeScript desteği sağlıyor. Takım tercihine göre seç.

Builder Ne Zaman Karşılığını Verir#

Builder, middleware sırası ve context type’ları tek bir kişinin aklında tutulamayacak kadar çok handler biriktiğinde karşılığını verir; en çok da birkaç fonksiyon aynı authentication, validation ve secret bağlantısını paylaştığında. Sıralama kurallarını build() içinde tut ki hiçbir çağrı tarafı yanlış yapamasın, flag ve secret’ları açık bir TTL ile cache’le ve ikisi için de fallback davranışı tanımla; configuration servisinin kötü bir günü bir endpoint’i yanında götürmemeli.

Tek işi olan bir fonksiyonda, zamanlanmış bir job’da veya HTTP yüzeyi olmayan bir yerde bu soyutlamayı kurma: oralarda düz bir middy() zinciri onu saran wrapper’dan kısa kalır. Takım zaten uçtan uca Lambda Powertools kullanıyorsa ve kendi yazdığı bir araç zincirini sürdürmek yerine tek bir vendor-maintained zinciri tercih ediyorsa da gerek yok. Makul bir orta yol, builder’ı önce sadece validation ve error handling için kurmak; feature flag ve secret’ları ikinci ya da üçüncü handler aynı bağlantıyı isteyince eklemek.

Kaynaklar#

İlgili yazılar