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:

TypeScript
// 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' })
    }
  }
}
TypeScript
// 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#

TypeScript
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#

TypeScript
import httpSecurityHeaders from '@middy/http-security-headers'    // Fügt Security Headers hinzu
import httpCors from '@middy/http-cors'                          // Handhabt CORS

Validierung#

TypeScript
import validator from '@middy/validator'                         // JSON Schema Validierung

AWS Service Integration#

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
// 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:

TypeScript
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:

TypeScript
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:

TypeScript
.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:

TypeScript
.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:

TypeScript
// 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:

  1. Over-engineering einfacher Funktionen - Nicht jede Lambda braucht Middleware
  2. Middleware-Reihenfolge ignorieren - Parse before validate, validate before business logic
  3. Heavy Middlewares in Cold Starts - Sei achtsam bei Initialization Overhead
  4. Sensitive Data loggen - Sei vorsichtig mit Input/Output Logging Middleware
  5. Configuration nicht cachen - Nutze built-in Caching für externe Daten

Erste Schritte#

Bereit Middy auszuprobieren? Hier ist dein Starter Kit:

Bash
# 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

Fortschritt1/2 Beiträge abgeschlossen

Alle Beiträge in dieser Serie

Teil 1: Einführung in Middy - Die Lambda Middleware Engine
Loading...

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!

Related Posts