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.
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:
- Compile-time type safety: Configuration hatalarını deployment öncesi yakala
- Enforced middleware ordering: Tüm fonksiyonlarda tutarlı execution
- Daha iyi validation hataları: Schema validation’dan net, actionable mesajlar
- Feature flag integration: Kod deployment olmadan feature toggle
- Secrets management: Cache’lenmiş, rotation-aware secret erişimi
- Discoverable API: Autocomplete ve type hints ile developer guidance
- 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
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
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#
- Middy Dokümantasyonu (yeni sekmede açılır) - AWS Lambda ile Node.js kullanan resmi middleware motoru dokümantasyonu
- Zod Dokümantasyonu (yeni sekmede açılır) - Statik tip çıkarımı ile TypeScript öncelikli şema doğrulama kütüphanesi
- Builder Tasarım Kalıbı (yeni sekmede açılır) - refactoring.guru’nun Builder yaratıcı kalıbı açıklaması
- AWS Secrets Manager En İyi Pratikler (yeni sekmede açılır) - Kimlik bilgilerini depolama ve döndürme için resmi AWS rehberi
- AWS Lambda Uzantıları (yeni sekmede açılır) - Lambda yürütme ortamı uzantıları için resmi dokümantasyon
- AWS Systems Manager Fiyatlandırması (yeni sekmede açılır) - AWS AppConfig API request’leri ve alınan configuration’lar için liste fiyatları
- AWS Secrets Manager Fiyatlandırması (yeni sekmede açılır) - Yukarıdaki maliyet modelinde kullanılan secret başına depolama ve API request ücretleri
İlgili yazılar
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
lambda · middleware · serverless +5
Effect'i adım adım öğrenmek ve AWS Lambda ile entegre etmek için pratik bir rehber: gerçek kod örnekleri, yaygın hatalar ve üretim desenleri.
typescript · functional-programming · lambda +4
AWS CDK, DynamoDB ve Lambda ile production-grade link kısaltıcı kurulumu. Mimari kararlar, proje yapısı ve ölçekte ayakta kalan şema seçimleri.
aws-cdk · lambda · dynamodb +5
Bir Lambda filosu Middy'nin statik middleware modelini ne zaman aşar, projeye özel bir motor istek başına konfigürasyonu nasıl çözer, bakımı neye mal olur
lambda · middleware · performance +6
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