lift

module
v1.0.78 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 15, 2025 License: Apache-2.0

README

Lift: Type-Safe Serverless Framework for AWS Lambda in Go

Lift is a production-ready framework for building AWS Lambda functions in Go. It provides automatic error handling, logging, observability, and multi-tenant support while reducing boilerplate code.

Why Lift?

Use Lift when you need:

  • ✅ Production-ready Lambda functions with minimal cold start overhead
  • ✅ Type-safe handlers with compile-time validation
  • ✅ Guardrails for request size, response size, and execution timeout baked into the runtime
  • ✅ Built-in error handling, logging, metrics, and distributed tracing
  • ✅ Multi-tenant support with automatic tenant isolation
  • ✅ Zero-configuration middleware for auth, CORS, rate limiting, and adaptive load shedding
  • ❌ Don't use for: Non-Lambda deployments, custom runtimes, or non-Go languages

Quick Start

// This is a recommended pattern for Lambda functions in Go
// It provides automatic error handling, validation, and observability
package main

import (
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/pay-theory/lift/pkg/lift"
    "github.com/pay-theory/lift/pkg/middleware"
)

// Type-safe request/response with automatic validation
type CreateUserRequest struct {
    Name string `json:"name" validate:"required"`
    Age  int    `json:"age" validate:"min=0,max=120"`
}

type UserResponse struct {
    UserID   string `json:"user_id"`
    TenantID string `json:"tenant_id,omitempty"`
}

func main() {
    app := lift.New()
    defer app.Stop()

    // Configure the app
    config := &lift.Config{
        MaxRequestSize: 5 * 1024 * 1024, // 5MB
        Timeout:        30,               // 30 seconds (default)
        LogLevel:       "INFO",
    }
    app.WithConfig(config)
    
    // Add essential middleware for production
    app.Use(middleware.RequestID())    // Distributed tracing
    app.Use(middleware.Logger())       // Structured logging
    app.Use(middleware.Recover())      // Panic recovery

    // Attach load shedding with automatic lifecycle management
    loadConfig := middleware.ConfigureLoadSheddingForApp(app, middleware.NewBasicLoadShedding("api"))
    app.Use(middleware.LoadSheddingMiddleware(loadConfig))

    // Unified observability with automatic tenant/user tagging
    app.Use(middleware.EnhancedObservabilityMiddleware(middleware.EnhancedObservabilityConfig{
        EnableLogging: true,
        EnableMetrics: true,
        EnableTracing: true,
        SampleRate:    0.25, // observe 25% of traffic end-to-end
    }))
    
    // Type-safe handler - recommended over raw handlers
    app.POST("/users", lift.SimpleHandler(func(ctx *lift.Context, req CreateUserRequest) (UserResponse, error) {
        // Automatic: parsing, validation, error handling
        return UserResponse{
            UserID:   "user_123",
            TenantID: ctx.TenantID(), // Multi-tenant support
        }, nil
    }))
    
    // For Lambda deployment
    lambda.Start(app.HandleRequest)
}

// Alternative: Basic handler pattern
func CreateUser(ctx *lift.Context) error {
    var req CreateUserRequest
    if err := ctx.ParseRequest(&req); err != nil {
        return lift.ValidationError(err.Error())
    }
    
    ctx.Status(201)
    return ctx.JSON(UserResponse{
        UserID:   "user_123",
        TenantID: ctx.TenantID(),
    })
}

Core Concepts

Context

The Context is Lift's unified interface for Lambda functions. This is important because it provides all request data, response methods, and service clients in one place.

Example:

// Use lift.Context as your handler parameter
func HandlePayment(ctx *lift.Context) error {
    // Parse request with validation
    var payment PaymentRequest
    if err := ctx.ParseRequest(&payment); err != nil {
        return lift.ValidationError("Invalid request")
    }
    
    // Access multi-tenant context
    userID := ctx.UserID()
    tenantID := ctx.TenantID()
    
    // Structured logging
    ctx.Logger.Info("Processing payment", 
        "user_id", userID,
        "amount", payment.Amount)
    
    // Return JSON response
    ctx.Status(200)
    return ctx.JSON(PaymentResponse{
        ID: "payment_123",
        Status: "completed",
    })
}

// The Context abstracts all event sources (API Gateway, SQS, S3, etc.)
Type-Safe Handlers

Lift uses Go generics for compile-time type safety. This prevents runtime errors and provides IDE autocomplete.

Example:

// Type-safe handler with automatic validation
app.POST("/orders", lift.SimpleHandler(func(ctx *lift.Context, req OrderRequest) (OrderResponse, error) {
    // Request is already parsed and validated
    // Return type is enforced at compile time
    return processOrder(req)
}))

// Standard handler with manual parsing
app.POST("/orders", func(ctx *lift.Context) error {
    var req OrderRequest
    if err := ctx.ParseRequest(&req); err != nil {
        return lift.ValidationError(err.Error())
    }
    // Process and return response
    return ctx.JSON(response)
})

Installation

# This method is preferred for new serverless Go applications
go mod init myservice
go get github.com/pay-theory/lift/pkg/lift
go get github.com/pay-theory/lift/pkg/middleware
For Existing Lambda Projects
# Use this when migrating from raw Lambda handlers
go get github.com/pay-theory/lift/pkg/lift

# See Migration Guide below for step-by-step conversion

Common Patterns

Pattern: Request Validation

When to use: Every API endpoint accepting user input Why: Prevents invalid data from reaching business logic

// Validation tags with automatic enforcement
type PaymentRequest struct {
    Amount   int64  `json:"amount" validate:"required,min=100"`
    Currency string `json:"currency" validate:"required,oneof=USD EUR"`
    Email    string `json:"email" validate:"required,email"`
}

app.POST("/payments", lift.SimpleHandler(func(ctx *lift.Context, req PaymentRequest) (PaymentResponse, error) {
    // Request is guaranteed valid here
    return processPayment(req)
}))

// Manual validation in standard handler
app.POST("/payments", func(ctx *lift.Context) error {
    var req PaymentRequest
    if err := ctx.ParseRequest(&req); err != nil {
        return lift.ValidationError(err.Error())
    }
    
    // Additional business logic validation if needed
    if req.Amount < 100 {
        return lift.ValidationError("amount too small")
    }
    
    return ctx.JSON(response)
})
Pattern: Multi-Tenant Isolation

When to use: SaaS applications with tenant data isolation Why: Ensures data security and compliance

// Use Context tenant helpers
func GetUserOrders(ctx *lift.Context) error {
    tenantID := ctx.TenantID() // Automatic from JWT/headers
    userID := ctx.UserID()
    
    orders := db.Query("SELECT * FROM orders WHERE tenant_id = ? AND user_id = ?", 
        tenantID, userID)
    
    return ctx.JSON(orders)
}

// Configure app for multi-tenant support
config := &lift.Config{
    RequireTenantID: true,
    TenantIsolation: true,  // Enable tenant-based data isolation
}
app.WithConfig(config)
Pattern: Middleware Composition

When to use: Cross-cutting concerns (auth, logging, rate limiting) Why: Separation of concerns and reusability

// Middleware chains for different route groups
api := app.Group("/api")

// JWT authentication
jwtMiddleware := middleware.JWTAuth(middleware.JWTConfig{
    Secret: os.Getenv("JWT_SECRET"),
})
api.Use(jwtMiddleware)

// Rate limiting
rateLimiter, _ := middleware.UserRateLimitWithLimited(100, time.Hour)
api.Use(rateLimiter)

admin := api.Group("/admin")
admin.Use(middleware.RequireRole("admin")) // Additional admin check

// Routes automatically inherit middleware
api.GET("/orders", GetOrders)       // Has auth + rate limit
admin.GET("/users", ListUsers)      // Has auth + rate limit + admin

### Runtime Guardrails
**When to use:** Always — these limits are enforced before your handler executes
**Why:** Protects Lambda concurrency, keeps responses within platform constraints, and enforces tenant contracts

```go
app.WithConfig(&lift.Config{
    MaxRequestSize:  6 * 1024 * 1024, // reject oversize payloads early
    MaxResponseSize: 6 * 1024 * 1024, // prevent Lambda from returning huge bodies
    Timeout:         25,              // per-request deadline (seconds)
    RequireTenantID: true,            // drop calls without tenant context
})

Handlers receive structured guardrail errors automatically; the enhanced observability middleware emits counters for every rejection.

Observability & Sampling

When to use: Production workloads that need correlated logs, metrics, and traces Why: Runs logging/metrics/tracing for every trigger with explicit control over sampling

app.Use(middleware.EnhancedObservabilityMiddleware(middleware.EnhancedObservabilityConfig{
    EnableLogging:   true,
    EnableMetrics:   true,
    EnableTracing:   true,
    SampleRate:      0.1,                          // 10% instrumentation
    DefaultTags:     map[string]string{"service": "checkout"},
    TenantIDFunc:    func(ctx *lift.Context) string { return ctx.TenantID() },
    UserIDFunc:      func(ctx *lift.Context) string { return ctx.UserID() },
    DisableSampling: false,                        // set true to short-circuit instrumentation
}))

Logging and metrics automatically include tenant/user dimensions; AWS X-Ray segments receive the same annotations.

Disaster Recovery Scheduling

When to use: Multi-region deployments with automated failover rehearsals Why: Validates testing cadence while preventing monitoring from deadlocking during notifications

drm := disaster.NewDisasterRecoveryManager(disaster.DRConfig{
    PrimaryRegion: "us-east-1",
    BackupRegions: []string{"us-west-2"},
    TestingSchedule: disaster.TestingScheduleConfig{
        Enabled:      true,
        Frequency:    time.Hour,        // defaults to 24h when zero
        NotifyBefore: 5 * time.Minute,  // must be < Frequency
    },
})

if err := drm.StartMonitoring(ctx); err != nil {
    log.Fatalf("DR configuration invalid: %v", err)
}

The manager validates durations during StartMonitoring, and it now releases internal locks before waiting for notification intervals so health events keep flowing.

Managed Connection Pools

When to use: High-throughput DynamoDB workloads that reuse SDK clients Why: Enforces MaxConnections, surfaces safe metrics, and closes without deadlocks

pool, err := performance.NewConnectionPool(ctx, &performance.ConnectionPoolConfig{
    Region:         "us-east-1",
    MaxConnections: 64,
    MinConnections: 8,
})
if err != nil {
    log.Fatalf("pool init failed: %v", err)
}
defer pool.Close()

client, err := pool.GetClient(ctx)
// ... use client ...
pool.ReturnClient(client)

stats := pool.PoolStats() // safe even if no requests were served yet

The pool tracks active + idle clients internally; closing it stops health checks and drains the channel without re-locking.

Auto-Optimisation Results

When to use: Performance optimisation workflows that should surface concrete actions Why: Runs registered optimizers when EnableAutoOptimize is true and records both successes and failures

optimizer := performance.NewPerformanceOptimizer(performance.PerformanceConfig{
    BenchmarkTimeout:   time.Minute,
    EnableAutoOptimize: true,
})

optimizer.AddOptimizer(NewCPUOptimizer())
optimizer.AddOptimizer(NewCostOptimizer())

result, err := optimizer.OptimizePerformance(ctx, "billing-service")
if err != nil {
    log.Fatal(err)
}

for name, opt := range result.Optimizations {
    fmt.Printf("%s optimizer suggestion: %+v\n", name, opt)
}
if len(result.Errors) > 0 {
    log.Printf("optimizers reported warnings: %v", result.Errors)
}

Failed optimizers no longer block the run—errors are surfaced alongside successful recommendations.

API Reference

lift.New() *App

Purpose: Creates a new Lift application instance When to use: Once at the start of your Lambda function When NOT to use: Don't create multiple apps per Lambda

// Create and configure app
app := lift.New()

config := &lift.Config{
    MaxRequestSize:  10 * 1024 * 1024, // 10MB
    MaxResponseSize: 6 * 1024 * 1024,  // 6MB (Lambda limit)
    Timeout:         30,                // 30 seconds (default)
    LogLevel:        "INFO",
    MetricsEnabled:  true,
    TracingEnabled:  false,             // Optional: enable distributed tracing
    Debug:           false,             // Optional: enable debug mode
    CORSEnabled:     true,              // Optional: enable CORS
    RequireTenantID: false,             // Optional: require tenant ID
}
app.WithConfig(config)

Configuration Options:

  • MaxRequestSize: Maximum request body size (default: 10MB)
  • MaxResponseSize: Maximum response body size (default: 6MB, Lambda limit)
  • Timeout: Request timeout in seconds (default: 30)
  • LogLevel: Logging level - "DEBUG", "INFO", "WARN", "ERROR" (default: "INFO")
  • MetricsEnabled: Enable metrics collection (default: true)
  • TracingEnabled: Enable distributed tracing (default: false)
  • Debug: Enable debug mode with verbose logging (default: false)
  • CORSEnabled: Enable CORS headers (default: true)
  • RequireTenantID: Require tenant ID for all requests (default: false)
  • AllowedOrigins: CORS allowed origins (default: ["*"])
app.Use(middleware ...Middleware)

Purpose: Adds middleware to all routes When to use: For cross-cutting concerns like logging, auth When NOT to use: For route-specific logic

// Standard middleware stack
app.Use(
    middleware.RequestID(),    // First: generates request ID
    middleware.Logger(),       // Second: logs with request ID
    middleware.Recover(),      // Third: catches panics
)
// Order matters: RequestID must come before Logger
// All middleware functions are available in github.com/pay-theory/lift/pkg/middleware
ctx.ParseRequest(dest interface{}) error

Purpose: Parses and validates request body into struct When to use: For all POST/PUT/PATCH endpoints When NOT to use: GET requests (use ctx.Query instead)

// Safe request parsing
var req UpdateUserRequest
if err := ctx.ParseRequest(&req); err != nil {
    return lift.ValidationError(err.Error())
}
// req is now validated and type-safe
Error Handling

Built-in error constructors:

// 401 Unauthorized
return lift.Unauthorized("authentication required")

// 403 Forbidden
return lift.AuthorizationError("insufficient permissions")

// 404 Not Found
return lift.NotFound("user not found")

// 422 Validation Error
return lift.ValidationError("invalid email format")

// Custom errors
return lift.NewLiftError("PAYMENT_FAILED", "Payment processing failed", 500)

Best Practices

  1. Use type-safe handlers when possible - Prevents runtime errors and improves code clarity
  2. Use Context methods instead of raw Lambda events - Better portability and testing
  3. Use struct tag validation - Cleaner than manual validation
  4. Add standard middleware - RequestID, Logger, Recover
  5. Never log sensitive data - No passwords, tokens, or PII in logs
  6. Use middleware for cross-cutting concerns - Don't repeat auth/logging in handlers

Integration Examples

With DynamoDB (via DynamORM)

Lift provides standardized DynamoDB table structures that work seamlessly with DynamORM. All tables use a consistent pk/sk pattern with GSIs defined through struct tags.

For detailed DynamORM integration, see: DynamORM Integration Guide

import (
    "fmt"
    "github.com/pay-theory/lift/pkg/lift"
    "github.com/pay-theory/lift/pkg/dynamorm"
)

// Define your model with DynamORM tags
type User struct {
    // Primary key structure
    PK       string `dynamorm:"pk"`                    // user#{user_id}
    SK       string `dynamorm:"sk"`                    // user#{user_id}
    
    // GSI fields for efficient queries
    Email    string `dynamorm:"index:email-index,pk"`      // GSI for email lookup
    TenantID string `dynamorm:"index:tenant-index,pk"`      // GSI for tenant queries
    
    // User data fields
    UserID   string    `json:"user_id"`
    Name     string    `json:"name"`
    TTL      int64     `json:"ttl,omitempty" dynamorm:"ttl"`
}

func GetUser(ctx *lift.Context) error {
    userID := ctx.Param("id")
    tenantID := ctx.TenantID() // Multi-tenant isolation
    
    // Get DynamORM instance from context
    db, err := dynamorm.TenantDB(ctx)
    if err != nil {
        return lift.SystemError("Database not available").WithCause(err)
    }
    
    // Query using DynamORM wrapper
    var user User
    err = db.Get(ctx.Context, fmt.Sprintf("user#%s", userID), &user)
        
    if err != nil {
        return lift.SystemError("Failed to get user").WithCause(err)
    }
    
    return ctx.JSON(user)
}
With SQS Events

Lift automatically runs logging and metrics middleware for all event sources. Authentication and other HTTP-specific middleware do not execute for SQS/S3/EventBridge handlers unless you explicitly wrap them yourself.

// Batch processing with error handling
app.SQS("process-orders", func(ctx *lift.Context) error {
    // Parse batch of SQS messages
    var messages []SQSMessage
    if err := ctx.ParseRequest(&messages); err != nil {
        ctx.Logger.Error("Failed to parse SQS messages", "error", err)
        return err // All messages return to queue
    }
    
    var failedMessages []string
    
    for _, msg := range messages {
        var order Order
        if err := json.Unmarshal([]byte(msg.Body), &order); err != nil {
            ctx.Logger.Error("Invalid order format", "messageId", msg.MessageId)
            failedMessages = append(failedMessages, msg.MessageId)
            continue
        }
        
        if err := processOrder(order); err != nil {
            ctx.Logger.Error("Order processing failed", 
                "orderId", order.ID, 
                "messageId", msg.MessageId,
                "error", err)
            failedMessages = append(failedMessages, msg.MessageId)
        } else {
            ctx.Logger.Info("Order processed successfully", "orderId", order.ID)
        }
    }
    
    // Return error if any messages failed (they'll be retried)
    if len(failedMessages) > 0 {
        return fmt.Errorf("failed to process %d messages", len(failedMessages))
    }
    
    return nil // All messages processed successfully
})

// Dead letter queue handling
app.SQS("process-failed-orders", func(ctx *lift.Context) error {
    var messages []SQSMessage
    if err := ctx.ParseRequest(&messages); err != nil {
        return err
    }
    
    for _, msg := range messages {
        // Log failed message for manual review
        ctx.Logger.Error("Message reached DLQ", 
            "messageId", msg.MessageId,
            "body", msg.Body,
            "attributes", msg.MessageAttributes)
        
        // Store in database for analysis
        if err := storeFailedMessage(msg); err != nil {
            ctx.Logger.Error("Failed to store DLQ message", "error", err)
        }
    }
    
    return nil
})
With S3 Events
// File processing with S3 triggers
app.S3("process-uploads", func(ctx *lift.Context) error {
    var records []S3Record
    if err := ctx.ParseRequest(&records); err != nil {
        ctx.Logger.Error("Failed to parse S3 event", "error", err)
        return err
    }
    
    for _, record := range records {
        bucket := record.S3.Bucket.Name
        key := record.S3.Object.Key
        
        ctx.Logger.Info("Processing file", 
            "bucket", bucket,
            "key", key,
            "size", record.S3.Object.Size)
        
        // Download and process file
        if err := processFile(bucket, key); err != nil {
            ctx.Logger.Error("File processing failed", 
                "bucket", bucket,
                "key", key,
                "error", err)
            return err
        }
        
        // Move to processed folder
        if err := moveToProcessed(bucket, key); err != nil {
            ctx.Logger.Error("Failed to move file", "error", err)
            return err
        }
    }
    
    return nil
})

// Image processing pipeline
app.S3("resize-images", func(ctx *lift.Context) error {
    var records []S3Record
    if err := ctx.ParseRequest(&records); err != nil {
        return err
    }
    
    for _, record := range records {
        bucket := record.S3.Bucket.Name
        key := record.S3.Object.Key
        
        // Only process images
        if !isImageFile(key) {
            ctx.Logger.Info("Skipping non-image file", "key", key)
            continue
        }
        
        // Generate multiple sizes
        sizes := []string{"thumbnail", "medium", "large"}
        for _, size := range sizes {
            if err := resizeImage(bucket, key, size); err != nil {
                ctx.Logger.Error("Image resize failed", 
                    "key", key,
                    "size", size,
                    "error", err)
                return err
            }
        }
        
        ctx.Logger.Info("Image processed successfully", "key", key)
    }
    
    return nil
})
With AWS AppSync GraphQL Resolvers
// AppSync Lambda resolvers work seamlessly with Lift
// The adapter automatically maps GraphQL operations to HTTP-like semantics
func main() {
    app := lift.New()

    // Register handlers using GraphQL field names as paths
    // Mutations → POST, Queries → GET
    app.POST("/createPazeOnboarding", CreatePazeOnboarding)
    app.GET("/pazeMerchantIdentity", GetPazeMerchantIdentity)

    lambda.Start(app.HandleRequest)
}

// Mutation handler - receives GraphQL arguments in request body
func CreatePazeOnboarding(ctx *lift.Context) error {
    // Parse GraphQL arguments (e.g., { "input": { "merchantUid": "...", "scope": "..." } })
    var input PazeOnboardingInput
    if err := ctx.ParseRequest(&input); err != nil {
        return lift.ParameterError("input", "Invalid input format")
    }

    // Access AppSync identity from metadata
    if identity, ok := ctx.Request.Metadata["identity"].(map[string]any); ok {
        userSub := ctx.Request.Metadata["userSub"].(string) // Cognito user ID
        ctx.Logger.Info("Processing request", "userSub", userSub)
    }

    // Business logic
    result := onboardMerchant(input)

    return ctx.OK(result)
}

// Query handler - receives GraphQL arguments in request body
func GetPazeMerchantIdentity(ctx *lift.Context) error {
    // GraphQL arguments are passed as JSON body
    var args struct {
        MerchantUID string `json:"merchantUid"`
    }
    if err := ctx.ParseRequest(&args); err != nil {
        return lift.ParameterError("merchantUid", "Missing merchant UID")
    }

    identity, err := fetchMerchantIdentity(args.MerchantUID)
    if err != nil {
        return lift.NotFound("Merchant identity not found")
    }

    return ctx.OK(identity)
}

// Dual API Gateway + AppSync support
// The same handler can serve both REST and GraphQL requests
func GetUser(ctx *lift.Context) error {
    // Try path parameter first (API Gateway: /user/:id)
    userID := ctx.Param("id")

    // Fall back to request body (AppSync GraphQL arguments)
    if userID == "" {
        var args struct {
            UserID string `json:"userId"`
        }
        if err := ctx.ParseRequest(&args); err == nil {
            userID = args.UserID
        }
    }

    if userID == "" {
        return lift.ParameterError("userId", "Missing user ID")
    }

    user, err := getUser(userID)
    if err != nil {
        return lift.NotFound("User not found")
    }

    return ctx.OK(user)
}

// Register routes for both API Gateway and AppSync
app.GET("/user/:id", GetUser)           // API Gateway: GET /user/123
app.GET("/getUser", GetUser)            // AppSync: query getUser(userId: "123")

AppSync Event Structure: The adapter automatically handles AppSync Lambda resolver events:

  • Extracts fieldName and maps it to the request path
  • Maps GraphQL operation types: Mutation → POST, Query → GET, Subscription → GET
  • Converts arguments to JSON request body
  • Preserves identity (Cognito/IAM caller info) in metadata
  • Includes source object for nested resolvers

GraphQL Schema Example:

type Query {
  getUser(userId: ID!): User
  pazeMerchantIdentity(merchantUid: ID!): PazeMerchantIdentity
}

type Mutation {
  createPazeOnboarding(input: PazeOnboardingInput!): PazeOnboardingResult!
}
With EventBridge Scheduled Events
// Same Context interface for all event types
app.EventBridge("daily-report", func(ctx *lift.Context) error {
    ctx.Logger.Info("Running scheduled job")

    // Use same patterns as HTTP handlers
    return runScheduledJob(ctx)
})

// Cron-like scheduling
app.EventBridge("hourly-cleanup", func(ctx *lift.Context) error {
    ctx.Logger.Info("Starting hourly cleanup")

    // Clean up old temporary files
    if err := cleanupTempFiles(); err != nil {
        ctx.Logger.Error("Cleanup failed", "error", err)
        return err
    }

    // Archive old logs
    if err := archiveOldLogs(); err != nil {
        ctx.Logger.Error("Log archival failed", "error", err)
        return err
    }

    ctx.Logger.Info("Hourly cleanup completed")
    return nil
})
With WebSocket Connections
// WebSocket authentication middleware
func WebSocketJWTMiddleware(jwtSecret string) lift.Middleware {
    return func(next lift.Handler) lift.Handler {
        return lift.HandlerFunc(func(ctx *lift.Context) error {
            // Only validate on $connect events
            wsCtx, err := ctx.AsWebSocket()
            if err == nil && wsCtx.IsConnectEvent() {
                token := ctx.Query("Authorization")
                if token == "" {
                    return ctx.Status(401).JSON(map[string]string{
                        "error": "Missing authorization token",
                    })
                }
                
                // Validate JWT and store claims
                claims, err := validateJWTToken(token, jwtSecret)
                if err != nil {
                    return ctx.Status(401).JSON(map[string]string{
                        "error": "Invalid token",
                    })
                }
                
                ctx.SetUserID(claims.UserID)
            }
            return next.Handle(ctx)
        })
    }
}

// WebSocket connection handler
func handleConnect(ctx *lift.Context) error {
    wsCtx, err := ctx.AsWebSocket()
    if err != nil {
        return err
    }
    
    // Store connection info
    err = storeConnection(ctx.Context, wsCtx.ConnectionID(), ctx.UserID())
    if err != nil {
        ctx.Logger.Error("Failed to store connection", "error", err)
    }
    
    return ctx.Status(200).JSON(map[string]string{
        "message": "Connected successfully",
    })
}

// WebSocket message handler
func handleMessage(ctx *lift.Context) error {
    wsCtx, err := ctx.AsWebSocket()
    if err != nil {
        return err
    }
    
    // Parse incoming message
    var message map[string]any
    if err := ctx.ParseRequest(&message); err != nil {
        return wsCtx.SendJSONMessage(map[string]string{
            "error": "Invalid message format",
        })
    }
    
    // Echo back with connection info
    response := map[string]any{
        "type":         "echo",
        "originalMsg":  message,
        "connectionId": wsCtx.ConnectionID(),
        "timestamp":    time.Now(),
    }
    
    return wsCtx.SendJSONMessage(response)
}

// Register WebSocket handlers
app.Use(WebSocketJWTMiddleware(os.Getenv("JWT_SECRET")))
app.Handle("CONNECT", "/connect", handleConnect)
app.Handle("MESSAGE", "/message", handleMessage)
app.Handle("DISCONNECT", "/disconnect", handleDisconnect)

Troubleshooting

Error: "json: cannot unmarshal string into Go struct field"

Cause: Request body doesn't match struct types Solution: Check struct tags and request payload

// Correct: Matching types
type Request struct {
    Count int    `json:"count"`    // Expects number
    Name  string `json:"name"`     // Expects string
}

// If client sends: {"count": "5"} - this will fail
// Fix: Ensure client sends: {"count": 5}
Error: "context deadline exceeded"

Cause: Handler took longer than configured timeout Solution: Increase timeout or optimize handler

// Solution 1: Increase app timeout (must be less than Lambda timeout)
config := &lift.Config{
    Timeout: 300, // 5 minutes in seconds
}
app.WithConfig(config)

// Solution 2: Add timeout awareness
func LongRunningHandler(ctx *lift.Context) error {
    deadline, _ := ctx.Deadline()
    
    for {
        select {
        case <-ctx.Done():
            return lift.NewLiftError("TIMEOUT", "operation timed out", 504)
        default:
            // Do work in chunks
        }
    }
}
Error: "no handler found for path"

Cause: Route not registered or wrong HTTP method Solution: Check route registration and method

// Common mistake: Wrong method
app.GET("/users", GetUsers)    // Registered GET
// But client calls POST /users - will get 404

// Fix: Register correct method
app.POST("/users", CreateUser)

Migration Guide

From Raw Lambda Handlers
// Old pattern (raw Lambda handler):
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    // Manual JSON parsing
    var req Request
    json.Unmarshal([]byte(request.Body), &req)
    
    // Manual validation
    if req.Name == "" {
        return events.APIGatewayProxyResponse{
            StatusCode: 400,
            Body: `{"error": "name required"}`,
        }, nil
    }
    
    // Manual response building
    resp, _ := json.Marshal(Response{ID: "123"})
    return events.APIGatewayProxyResponse{
        StatusCode: 200,
        Body: string(resp),
        Headers: map[string]string{
            "Content-Type": "application/json",
        },
    }, nil
}

// New pattern with Lift:
func main() {
    app := lift.New()
    app.Use(middleware.Logger())
    
    app.POST("/", lift.SimpleHandler(func(ctx *lift.Context, req Request) (Response, error) {
        // Automatic parsing, validation, and response formatting
        return Response{ID: "123"}, nil
    }))
    
    lambda.Start(app.HandleRequest)
}
// Benefits: Type safety, automatic validation, consistent errors, logging, tracing
From Gin/Echo/Fiber
// Old pattern (Gin on Lambda):
func setupRouter() *gin.Engine {
    r := gin.Default()
    r.POST("/users", func(c *gin.Context) {
        var req Request
        c.ShouldBindJSON(&req)
        c.JSON(200, Response{})
    })
    return r
}

// New pattern with Lift (Lambda-optimized):
func main() {
    app := lift.New()
    app.POST("/users", lift.SimpleHandler(func(ctx *lift.Context, req Request) (Response, error) {
        return Response{}, nil
    }))
    lambda.Start(app.HandleRequest)
}
// Benefits: Faster cold starts, native Lambda integration, smaller binary

Performance Characteristics

Lift is designed for Lambda environments:

  • Minimal cold start overhead (typically under 15ms)
  • Low memory footprint
  • Efficient request routing
  • Built-in connection pooling for AWS services
  • Automatic resource cleanup

Compared to traditional web frameworks:

  • Optimized for Lambda's execution model
  • No unnecessary HTTP server overhead
  • Native Lambda event support without adapters

Security Features

Built-in Security
  • Input Validation: Automatic via struct tags
  • Error Sanitization: Never leak internal errors
  • Panic Recovery: Graceful error responses
  • Request ID: Trace requests across services
  • CORS: Configurable CORS middleware
  • Rate Limiting: Multiple strategies available
JWT Authentication
// Built-in JWT validation
jwtMiddleware := middleware.JWTAuth(middleware.JWTConfig{
    Secret: os.Getenv("JWT_SECRET"),
})
app.Use(jwtMiddleware)

// Access claims in handlers
func SecureHandler(ctx *lift.Context) error {
    userID := ctx.UserID() // From JWT claims
    // Claims are validated and available
}
Data Protection Keys
  • The security.DataProtectionConfig now validates that EncryptionKey is non-empty.
  • Store the key securely (for example, in AWS Secrets Manager) and inject it at startup:
    dataProtectionConfig := security.DataProtectionConfig{
        EncryptionKey: os.Getenv("DATA_PROTECTION_KEY"), // must be non-empty
        DefaultClassification: security.DataInternal,
    }
    
  • Leaving the key blank results in an initialization error to prevent accidentally shipping unencrypted payloads.

Testing Support

// Lift includes testing utilities
import lifttesting "github.com/pay-theory/lift/pkg/testing"

func TestHandler(t *testing.T) {
    // Create test context
    ctx := lifttesting.NewTestContext(
        lifttesting.WithMethod("POST"),
        lifttesting.WithPath("/users"),
        lifttesting.WithBody(`{"name": "test"}`),
        lifttesting.WithHeaders(map[string]string{
            "Authorization": "Bearer token",
        }),
    )
    
    // Execute handler
    err := CreateUser(ctx)
    assert.NoError(t, err)
    
    // Check response
    assert.Equal(t, 200, ctx.Response.StatusCode)
}

Production Checklist

Before deploying to production:

  • Add standard middleware (RequestID, Logger, Recover)
  • Configure appropriate timeouts (less than Lambda timeout)
  • Set up structured logging with log levels
  • Enable distributed tracing
  • Add health check endpoint
  • Configure CORS if needed
  • Set up monitoring alerts
  • Test error scenarios
  • Load test with expected traffic

Contributing

This is a Pay Theory internal project. See our development documentation:

  • Architecture: docs/architecture/
  • Development Guide: docs/development/
  • API Patterns: docs/patterns/

License

Apache License - See LICENSE file for details


About This Codebase

This entire codebase was written 100% by AI code generation, guided by the development team at Pay Theory. The framework represents a collaboration between human architectural vision and AI implementation capabilities, demonstrating the potential of AI-assisted software development for creating production-ready systems.

Directories

Path Synopsis
examples
error-handling command
hello-world command
jwt-auth-demo command
mocking-demo command
production-api command
rate-limiting command
zap-sns-logging command
pkg
cdk
Package cdk provides AWS CDK constructs for deploying Lift applications with infrastructure as code.
Package cdk provides AWS CDK constructs for deploying Lift applications with infrastructure as code.
cdk/constructs
Package constructs provides AWS CDK constructs for Lift applications.
Package constructs provides AWS CDK constructs for Lift applications.
cli
dev
lift
Package lift provides a Lambda-native framework for building serverless applications in Go.
Package lift provides a Lambda-native framework for building serverless applications in Go.
lift/adapters
Package adapters provides event adapters that normalize AWS Lambda events to a single internal request shape.
Package adapters provides event adapters that normalize AWS Lambda events to a single internal request shape.
lift/health
Package health contains HTTP endpoints and middleware for service health.
Package health contains HTTP endpoints and middleware for service health.
lift/resources
Package resources provides resource management utilities for Lift applications, including resource pooling, lifecycle management, and cleanup coordination.
Package resources provides resource management utilities for Lift applications, including resource pooling, lifecycle management, and cleanup coordination.
middleware
Package middleware contains production‑ready middleware for Lift applications, including request correlation, structured logging, panic recovery, error formatting, input validation, JWT authentication, rate limiting, idempotency, retries, circuit breaking, load shedding, security headers, and service mesh helpers.
Package middleware contains production‑ready middleware for Lift applications, including request correlation, structured logging, panic recovery, error formatting, input validation, JWT authentication, rate limiting, idempotency, retries, circuit breaking, load shedding, security headers, and service mesh helpers.
observability
Package observability defines interfaces and common types for structured logging and metrics used across Lift.
Package observability defines interfaces and common types for structured logging and metrics used across Lift.
observability/aws
Package aws provides helper utilities and adapters for AWS SDK error inspection and metadata used by Lift observability components.
Package aws provides helper utilities and adapters for AWS SDK error inspection and metadata used by Lift observability components.
observability/cloudwatch
Package cloudwatch contains structured logger and metrics implementations backed by Amazon CloudWatch.
Package cloudwatch contains structured logger and metrics implementations backed by Amazon CloudWatch.
observability/xray
Package xray provides integration helpers for AWS X-Ray, including middleware and utility functions to add subsegments, annotations, and metadata to traces in serverless applications built with Lift.
Package xray provides integration helpers for AWS X-Ray, including middleware and utility functions to add subsegments, annotations, and metadata to traces in serverless applications built with Lift.
services/kernel
Package kernel provides authenticated cross-account calls to kernel services.
Package kernel provides authenticated cross-account calls to kernel services.
streamer
Package streamer provides WebSocket connection management for AWS API Gateway.
Package streamer provides WebSocket connection management for AWS API Gateway.
utils/sanitization
Package sanitization provides centralized data sanitization utilities to prevent sensitive data exposure across the Lift framework.
Package sanitization provides centralized data sanitization utilities to prevent sensitive data exposure across the Lift framework.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL