lift

module
v1.0.56 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 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
  • ✅ Built-in error handling, logging, and distributed tracing
  • ✅ Multi-tenant support with automatic tenant isolation
  • ✅ Zero-configuration middleware for auth, CORS, rate limiting
  • ❌ 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()
    
    // Configure the app
    config := &lift.Config{
        MaxRequestSize: 5 * 1024 * 1024, // 5MB
        Timeout:        29,               // 29 seconds
        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
    
    // 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,
}
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

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:         29,                // 29 seconds
    LogLevel:        "INFO",
    MetricsEnabled:  true,
}
app.WithConfig(config)
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
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

// Define your model with BOTH DynamORM and DynamoDB tags
type User struct {
    // Keys must have both tags
    PK       string `dynamorm:"pk" `                    // user#{user_id}
    SK       string `dynamorm:"sk" `                    // user#{user_id}
    
    // GSI fields need both tags too
    Email    string `dynamorm:"index:email-index,pk" `      // GSI for email lookup
    TenantID string `dynamorm:"index:tenant-index,pk" ` // GSI for tenant queries
    
    // DynamORM handles marshaling internally
    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
    
    // Query using composite key for tenant isolation
    user, err := dynamorm.Get[User](ctx.Context, db).
        WithTable(os.Getenv("DYNAMODB_TABLE")).
        WithPK(fmt.Sprintf("tenant#%s", tenantID)).
        WithSK(fmt.Sprintf("user#%s", userID)).
        Execute()
        
    if err == dynamorm.ErrNotFound {
        return lift.NotFound("user not found")
    }
    if err != nil {
        return lift.NewLiftError("DATABASE_ERROR", "Failed to get user", 500)
    }
    
    return ctx.JSON(user)
}
With SQS Events
// Lift handles multiple event sources
app.SQS("process-orders", func(ctx *lift.Context) error {
    // SQS message is in ctx.Request.Body
    var order Order
    if err := ctx.ParseRequest(&order); err != nil {
        return err // Message returns to queue
    }
    
    ctx.Logger.Info("Processing order", "order_id", order.ID)
    return processOrder(order)
})
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)
})

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
}

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
pkg
cdk
Package cdk provides AWS CDK constructs for deploying Lift applications.
Package cdk provides AWS CDK constructs for deploying Lift applications.
cli
dev
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