AWS Lambda Middleware mit Middy - Sauberer Code und bewährte Praktiken
Entdecke, wie Middy die Lambda-Entwicklung mit Middleware-Mustern transformiert und von repetitiven Boilerplates zu sauberen, wartbaren Serverless-Funktionen führt
AWS Lambda Middleware mit Middy - Sauberer Code und bewährte Praktiken#
Stell dir vor: Du reviewst Lambda-Funktionen in deinem Team und jede einzelne beginnt mit denselben 40 Zeilen Validierung, Error Handling und CORS-Setup. Kommt dir bekannt vor? Ich war schon dort und starrte auf das, was sich wie ein völlig entgleistes Copy-Paste-Festival anfühlte.
Das war unsere Realität beim Management von 30+ Lambda-Funktionen für eine Fintech-Anwendung. Jeder Endpoint brauchte Authentication, Input-Validierung, ordentliche Error-Responses und Security-Header. Diesen Boilerplate wiederholt zu schreiben war nicht nur langweilig – es wurde zu einem Maintenance-Albtraum und Brutstätte für subtile Bugs.
Dann entdeckten wir Middy, und ehrlich gesagt veränderte es komplett, wie wir Lambda-Funktionen schreiben.
Was ist Middy?#
Denk an Middy wie an das Middleware-System, das du von Express oder Koa kennst, aber speziell für AWS Lambda entwickelt. Es nutzt den Zwiebelschalen-Ansatz, bei dem deine Business Logic im Zentrum sitzt, umgeben von wiederverwendbarer Middleware, die das langweilige aber wichtige Zeug erledigt.
Anstatt alles in deine Handler-Funktion zu stopfen, lässt dich Middy saubere, fokussierte Funktionen zusammenstellen:
// Ohne Middy - Der alte Weg
export const handler = async (event: APIGatewayProxyEvent) => {
try {
// JSON Body parsen
let body
try {
body = JSON.parse(event.body || '{}')
} catch (e) {
return {
statusCode: 400,
headers: { 'Access-Control-Allow-Origin': '*' },
body: JSON.stringify({ error: 'Invalid JSON' })
}
}
// Input validieren
if (!body.name || typeof body.name !== 'string') {
return {
statusCode: 400,
headers: { 'Access-Control-Allow-Origin': '*' },
body: JSON.stringify({ error: 'Name is required' })
}
}
// Security Headers hinzufügen
const headers = {
'Access-Control-Allow-Origin': '*',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY'
}
// Endlich, deine Business Logic
const greeting = `Hello, ${body.name}!`
return {
statusCode: 200,
headers,
body: JSON.stringify({ message: greeting })
}
} catch (error) {
console.error('Error:', error)
return {
statusCode: 500,
headers: { 'Access-Control-Allow-Origin': '*' },
body: JSON.stringify({ error: 'Internal server error' })
}
}
}
// Mit Middy - Sauber und fokussiert
import middy from '@middy/core'
import httpJsonBodyParser from '@middy/http-json-body-parser'
import httpErrorHandler from '@middy/http-error-handler'
import httpCors from '@middy/http-cors'
import httpSecurityHeaders from '@middy/http-security-headers'
import validator from '@middy/validator'
import { transpileSchema } from '@middy/validator/transpile'
// Pure Business Logic
const baseHandler = async (event: APIGatewayProxyEvent) => {
const { name } = event.body as { name: string }
return {
statusCode: 200,
body: JSON.stringify({
message: `Hello, ${name}!`,
timestamp: new Date().toISOString()
})
}
}
const schema = {
type: 'object',
properties: {
body: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 }
},
required: ['name']
}
}
}
export const handler = middy(baseHandler)
.use(httpJsonBodyParser())
.use(validator({ eventSchema: transpileSchema(schema) }))
.use(httpCors({ origin: '*' }))
.use(httpSecurityHeaders())
.use(httpErrorHandler())
Der Unterschied ist beeindruckend. Deine Business Logic wird zum Star der Show, während alle HTTP-Concerns konsistent von kampferprobter Middleware gehandhabt werden.
Wesentliche Middy-Middlewares#
Nach der Arbeit mit Dutzenden von Lambda-Funktionen sind hier die Middlewares, die ich als essentiell betrachte:
HTTP-Grundlagen#
import httpJsonBodyParser from '@middy/http-json-body-parser' // Parst JSON Bodies
import httpErrorHandler from '@middy/http-error-handler' // Konvertiert Errors zu HTTP Responses
import httpEventNormalizer from '@middy/http-event-normalizer' // Normalisiert API Gateway Events
import httpResponseSerializer from '@middy/http-response-serializer' // Handhabt Response-Serialisierung
Sicherheit & CORS#
import httpSecurityHeaders from '@middy/http-security-headers' // Fügt Security Headers hinzu
import httpCors from '@middy/http-cors' // Handhabt CORS
Validierung#
import validator from '@middy/validator' // JSON Schema Validierung
AWS Service Integration#
import ssm from '@middy/ssm' // AWS Systems Manager Parameter
import secretsManager from '@middy/secrets-manager' // AWS Secrets Manager
import warmup from '@middy/warmup' // Lambda Warmup Handling
Real-World Beispiel: Benutzerregistrierungs-API#
Lass mich zeigen, wie diese in einem Production-Szenario zusammenkommen. Hier ist ein Benutzerregistrierungs-Endpoint, den wir gebaut haben und der Validierung, Sicherheit und Error-Cases elegant handhabt:
import middy from '@middy/core'
import httpJsonBodyParser from '@middy/http-json-body-parser'
import httpErrorHandler from '@middy/http-error-handler'
import httpSecurityHeaders from '@middy/http-security-headers'
import httpCors from '@middy/http-cors'
import validator from '@middy/validator'
import { transpileSchema } from '@middy/validator/transpile'
import { createError } from '@middy/util'
interface UserRegistration {
email: string
password: string
firstName: string
lastName: string
}
const registerUser = async (event: APIGatewayProxyEvent) => {
const userData = event.body as UserRegistration
// Prüfen ob Benutzer bereits existiert
const existingUser = await getUserByEmail(userData.email)
if (existingUser) {
throw createError(409, 'User already exists', {
type: 'UserAlreadyExists'
})
}
// Neuen Benutzer erstellen
const hashedPassword = await hashPassword(userData.password)
const newUser = await createUser({
...userData,
password: hashedPassword
})
// Willkommens-Email senden (fire and forget)
sendWelcomeEmail(newUser.email, newUser.firstName).catch(
error => console.error('Failed to send welcome email:', error)
)
return {
statusCode: 201,
body: JSON.stringify({
id: newUser.id,
email: newUser.email,
firstName: newUser.firstName,
lastName: newUser.lastName,
createdAt: newUser.createdAt
})
}
}
const registrationSchema = {
type: 'object',
properties: {
body: {
type: 'object',
properties: {
email: {
type: 'string',
format: 'email',
maxLength: 254
},
password: {
type: 'string',
minLength: 8,
maxLength: 128,
pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]'
},
firstName: {
type: 'string',
minLength: 1,
maxLength: 50
},
lastName: {
type: 'string',
minLength: 1,
maxLength: 50
}
},
required: ['email', 'password', 'firstName', 'lastName']
}
}
}
export const handler = middy(registerUser)
.use(httpJsonBodyParser())
.use(validator({ eventSchema: transpileSchema(registrationSchema) }))
.use(httpCors({
origin: process.env.ALLOWED_ORIGINS?.split(',') ?? ['http://localhost:3000'],
credentials: true
}))
.use(httpSecurityHeaders({
hsts: {
maxAge: 31536000,
includeSubDomains: true
}
}))
.use(httpErrorHandler({
logger: console.error
}))
Diese einzige Middleware-Chain handhabt:
- JSON Parsing mit Error Handling
- Umfassende Input-Validierung (inklusive Passwort-Komplexität)
- CORS Headers mit konfigurierbaren Origins
- Security Headers zum Schutz
- Ordentliche HTTP Error Responses
- Request Logging
Deine Business Logic bleibt sauber und testbar, während alle HTTP-Concerns konsistent gehandhabt werden.
Eigene Middleware schreiben#
Manchmal brauchst du etwas spezifisches für deine App. Eigene Middleware zu erstellen ist unkompliziert, sobald du das Pattern verstehst:
import { MiddlewareObj } from '@middy/core'
interface RequestTimingOptions {
logSlowRequests?: boolean
slowRequestThreshold?: number
}
export const requestTiming = (
options: RequestTimingOptions = {}
): MiddlewareObj => {
const { logSlowRequests = true, slowRequestThreshold = 1000 } = options
return {
before: async (request) => {
// Timing initialisieren
request.internal = request.internal || {}
request.internal.startTime = Date.now()
},
after: async (request) => {
if (request.internal?.startTime) {
const duration = Date.now() - request.internal.startTime
// Timing Header zur Response hinzufügen
if (request.response && typeof request.response === 'object') {
const response = request.response as any
response.headers = {
...response.headers,
'X-Execution-Time': duration.toString()
}
}
// Langsame Requests loggen
if (logSlowRequests && duration > slowRequestThreshold) {
console.warn(`Slow request detected: ${duration}ms`, {
functionName: request.context.functionName,
requestId: request.context.awsRequestId,
duration
})
}
}
},
onError: async (request) => {
if (request.internal?.startTime) {
const duration = Date.now() - request.internal.startTime
console.error(`Request failed after ${duration}ms`, {
error: request.error?.message,
duration,
requestId: request.context.awsRequestId
})
}
}
}
}
// Verwendung
export const handler = middy(baseHandler)
.use(requestTiming({ slowRequestThreshold: 500 }))
.use(httpJsonBodyParser())
.use(httpErrorHandler())
Diese custom Middleware fügt Execution Timing zu Responses hinzu und loggt automatisch langsame Requests. Das Pattern ist sauber: before
läuft vor deinem Handler, after
nach Erfolg, und onError
handhabt Fehler.
Production Best Practices#
Hier ist was ich gelernt habe beim Betrieb von Middy in Production:
1. Reihenfolge ist wichtig#
Die Middleware-Ausführungsreihenfolge ist entscheidend. Ich hab subtile Bugs durch falsche Reihenfolge gesehen:
// Falsche Reihenfolge - validator läuft vor body parsing
export const handler = middy(baseHandler)
.use(validator({ eventSchema: schema })) // Das wird fehlschlagen!
.use(httpJsonBodyParser())
.use(httpErrorHandler())
// Korrekte Reihenfolge
export const handler = middy(baseHandler)
.use(httpJsonBodyParser()) // Erst parsen
.use(validator({ eventSchema: schema })) // Dann validieren
.use(httpErrorHandler()) // Errors zuletzt handeln
2. Type Safety ist essentiell#
Verwende immer ordentliche TypeScript Types:
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'
const typedHandler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
// TypeScript fängt Errors zur Compile-Zeit ab
const body = event.body as UserRegistration
// ... rest deiner Logic
}
3. Error Handling Strategie#
Erstelle domain-spezifische Error-Classes:
class BusinessLogicError extends Error {
statusCode: number
constructor(message: string, statusCode = 400) {
super(message)
this.statusCode = statusCode
this.name = 'BusinessLogicError'
}
}
// In Handlern verwenden
if (!isValidBusinessRule(data)) {
throw new BusinessLogicError('Invalid business data', 422)
}
4. Security Headers sollten Standard sein#
Überspringe keine Security Headers. Hier ist meine Standard-Konfiguration:
.use(httpSecurityHeaders({
contentTypeOptions: 'nosniff',
frameOptions: 'DENY',
contentSecurityPolicy: "default-src 'self'",
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}))
5. Cache Configuration Data#
Für häufig aufgerufene Funktionen, cache teure Konfiguration:
.use(ssm({
cache: true,
cacheExpiry: 5 * 60 * 1000, // 5 Minuten
names: {
dbConfig: '/myapp/database/config',
apiKeys: '/myapp/external/api-keys'
}
}))
Testen von Middy-Funktionen#
Einer von Middys größten Vorteilen ist wie es Testbarkeit verbessert. Du kannst deine Business Logic separat von der Middleware testen:
// Teste die pure Business Logic
describe('User Registration Logic', () => {
test('should create new user with valid data', async () => {
const mockEvent = {
body: {
email: 'test@example.com',
password: 'SecurePass123!',
firstName: 'John',
lastName: 'Doe'
}
} as APIGatewayProxyEvent
// Teste den Core Handler direkt
const result = await registerUser(mockEvent, {} as any)
expect(result.statusCode).toBe(201)
const responseBody = JSON.parse(result.body)
expect(responseBody.email).toBe('test@example.com')
expect(responseBody.password).toBeUndefined()
})
})
// Teste die komplette Middleware Chain
describe('User Registration API', () => {
test('should handle invalid JSON', async () => {
const event = {
body: 'invalid json',
headers: { 'content-type': 'application/json' }
} as any
const result = await handler(event, {} as any)
expect(result.statusCode).toBe(400)
})
test('should validate required fields', async () => {
const event = {
body: JSON.stringify({
email: 'test@example.com'
// Fehlende required fields
}),
headers: { 'content-type': 'application/json' }
} as any
const result = await handler(event, {} as any)
expect(result.statusCode).toBe(400)
})
})
Wann Middy NICHT verwenden#
Middy ist nicht immer die richtige Wahl. Überspringe es wenn:
- Ultra low-latency Funktionen wo jede Millisekunde zählt
- Single-purpose Utilities mit minimaler Logic
- Memory-beschränkte Umgebungen wo Bundle-Size kritisch ist
- Framework-agnostic Libraries wo explizite Composition bevorzugt wird
Häufige Fallstricke vermeiden#
Aus unserer Erfahrung, achte auf diese Issues:
- Over-engineering einfacher Funktionen - Nicht jede Lambda braucht Middleware
- Middleware-Reihenfolge ignorieren - Parse before validate, validate before business logic
- Heavy Middlewares in Cold Starts - Sei achtsam bei Initialization Overhead
- Sensitive Data loggen - Sei vorsichtig mit Input/Output Logging Middleware
- Configuration nicht cachen - Nutze built-in Caching für externe Daten
Erste Schritte#
Bereit Middy auszuprobieren? Hier ist dein Starter Kit:
# Core Package
npm install @middy/core
# Essenzielle Middlewares
npm install @middy/http-json-body-parser @middy/http-error-handler @middy/validator
# Sicherheit & CORS
npm install @middy/http-cors @middy/http-security-headers
# Performance Utilities
npm install @middy/do-not-wait-for-empty-event-loop @middy/warmup
# AWS Service Integrationen
npm install @middy/ssm @middy/secrets-manager
Fang mit einer einfachen HTTP API an, füge Middleware schrittweise hinzu und schau zu, wie deine Lambda-Funktionen wartbarer und konsistenter werden.
Was kommt als nächstes?#
Middy ist exzellent für die meisten Use Cases, aber was passiert wenn du mehr brauchst? In Teil 2 erkunden wir die Limitationen, die wir in Production getroffen haben und wie wir unser eigenes custom Middleware Framework gebaut haben um komplexe Business Requirements zu handhaben und Performance zu optimieren.
Du lernst über:
- Performance Bottlenecks die wir bei Scale entdeckt haben
- Building dynamic Middleware für Multi-Tenant Anwendungen
- Custom Framework Design Patterns
- Migration Strategien von Middy zu custom Solutions
- Echte Performance Benchmarks und Trade-offs
Middy transformierte wie wir Lambda-Funktionen schreiben, machte sie sauberer, testbarer und einfacher zu warten. Meistere diese Patterns und du schreibst besseren Serverless Code vom ersten Tag an.
AWS Lambda Middleware-Meisterschaft
Von Middy-Grundlagen bis zum Aufbau benutzerdefinierter Middleware-Frameworks für produktionstaugliche Lambda-Anwendungen
Alle Beiträge in dieser Serie
Kommentare (0)
An der Unterhaltung teilnehmen
Melde dich an, um deine Gedanken zu teilen und mit der Community zu interagieren
Noch keine Kommentare
Sei der erste, der deine Gedanken zu diesem Beitrag teilt!
Kommentare (0)
An der Unterhaltung teilnehmen
Melde dich an, um deine Gedanken zu teilen und mit der Community zu interagieren
Noch keine Kommentare
Sei der erste, der deine Gedanken zu diesem Beitrag teilt!