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
For New Lambda Projects (Recommended)
# 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
- Use type-safe handlers when possible - Prevents runtime errors and improves code clarity
- Use Context methods instead of raw Lambda events - Better portability and testing
- Use struct tag validation - Cleaner than manual validation
- Add standard middleware - RequestID, Logger, Recover
- Never log sensitive data - No passwords, tokens, or PII in logs
- 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 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
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
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.