Lambda Utilities
HTTP handling, error management, and middleware for AWS Lambda functions with built-in observability and security.
Overview
The Lambda utilities module provides comprehensive HTTP handling for Lambda functions with integrated observability, error handling, and security:
- HTTP Handler: Complete wrapper with observability and middleware
- Error Handler: Standardized HTTP error responses with proper logging
- Headers: Stage-specific security and CORS headers
HTTP Handler
A comprehensive wrapper that applies observability middleware and standardizes Lambda HTTP responses.
Basic Usage
import { withHttpHandler } from '@leighton-digital/lambda-toolkit';
import { MetricUnits } from '@aws-lambda-powertools/metrics';
export const handler = withHttpHandler(async ({ event, metrics, stage }) => {
// Add custom metrics
metrics.addMetric('RequestCount', MetricUnits.Count, 1);
// Your business logic
const userId = event.pathParameters?.userId;
return {
statusCode: 200,
body: {
message: `Hello from ${stage}!`,
userId
},
};
});
Handler Function Arguments
The wrapped handler receives an object with:
- event: Standard API Gateway proxy event
- metrics: AWS Lambda Powertools Metrics instance
- stage: Current deployment stage
Return Format
Your handler should return:
{
statusCode?: number, // Defaults to 200
body: unknown // Response body (auto-serialized to JSON)
}
Complete Example
import { withHttpHandler } from '@leighton-digital/lambda-toolkit';
import { validateSchema } from '@leighton-digital/lambda-toolkit';
import { MetricUnits } from '@aws-lambda-powertools/metrics';
import { z } from 'zod';
const requestSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
export const createUserHandler = withHttpHandler(async ({ event, metrics, stage }) => {
// Parse and validate request
const body = JSON.parse(event.body || '{}');
const validatedData = validateSchema(requestSchema, body);
// Add metrics
metrics.addMetric('UserCreated', MetricUnits.Count, 1, {
stage,
});
// Business logic
const user = await createUser(validatedData);
return {
statusCode: 201,
body: {
message: 'User created successfully',
user,
},
};
});
Middleware Stack
The HTTP handler automatically applies these middleware layers:
1. Context Injection
- Injects AWS Lambda Powertools Logger context
- Provides structured logging with request correlation
2. Distributed Tracing
- AWS Lambda Powertools Tracer integration
- Automatic trace capture and context propagation
3. Metrics Collection
- AWS Lambda Powertools Metrics integration
- Automatic metric flushing and dimensional metadata
4. Error Handling
- HTTP error handling with proper status codes
- Standardized error responses
5. Security Headers
- Stage-specific security headers
- CORS configuration per environment
Error Handler
Standardized error handling that maps custom errors to appropriate HTTP status codes.
Automatic Error Mapping
import { ValidationError, ResourceNotFound } from '@leighton-digital/lambda-toolkit/errors';
// These errors are automatically mapped:
throw new ValidationError('Invalid email format'); // → 400 Bad Request
throw new ResourceNotFound('User not found'); // → 404 Not Found
throw new UnauthorisedError('Invalid token'); // → 401 Unauthorized
throw new ForbiddenError('Access denied'); // → 403 Forbidden
throw new ConflictError('Email already exists'); // → 409 Conflict
throw new TooManyRequestsError('Rate limit exceeded'); // → 429 Too Many Requests
throw new Error('Database connection failed'); // → 500 Internal Server Error
Error Response Format
All errors return a consistent format:
{
"error": "Error message",
"statusCode": 400
}
Custom Error Example
import { withHttpHandler } from '@leighton-digital/lambda-toolkit';
import { ValidationError } from '@leighton-digital/lambda-toolkit/errors';
export const handler = withHttpHandler(async ({ event }) => {
const { userId } = event.pathParameters;
if (!userId) {
throw new ValidationError('User ID is required');
}
const user = await getUserById(userId);
if (!user) {
throw new ResourceNotFound(`User ${userId} not found`);
}
return {
statusCode: 200,
body: { user },
};
});
Headers
Stage-specific security and CORS headers with environment-aware configurations.
Get Headers by Stage
import { getHeaders } from '@leighton-digital/lambda-toolkit';
// Development headers (permissive CORS)
const devHeaders = getHeaders('develop');
// Production headers (restrictive CORS)
const prodHeaders = getHeaders('prod');
// Manual header application (usually not needed with withHttpHandler)
return {
statusCode: 200,
headers: getHeaders(stage),
body: { message: 'Success' },
};
Development Stage Headers
{
"Content-Type": "application/json",
"Content-Security-Policy": "default-src 'self'",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Referrer-Policy": "no-referrer",
"Permissions-Policy": "geolocation=(), microphone=()",
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": true
}
Production Stage Headers
Same as development, but without Access-Control-Allow-Origin: "*" for enhanced security.
Supported Stages
- develop: Permissive CORS for local development
- staging: Production-like security (no wildcard CORS)
- prod: Production security (no wildcard CORS)
- others: Defaults to development headers
Advanced Examples
Database Integration
import { withHttpHandler } from '@leighton-digital/lambda-toolkit';
import { stripInternalKeys } from '@leighton-digital/lambda-toolkit';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';
const dynamodb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const getUserHandler = withHttpHandler(async ({ event, metrics }) => {
const { userId } = event.pathParameters;
// Track database operations
metrics.addMetric('DatabaseQuery', MetricUnits.Count, 1);
const result = await dynamodb.send(new GetCommand({
TableName: process.env.USER_TABLE,
Key: { pk: `USER#${userId}`, sk: 'PROFILE' },
}));
if (!result.Item) {
throw new ResourceNotFound('User not found');
}
// Clean internal DynamoDB keys
const publicUser = stripInternalKeys(result.Item);
return {
statusCode: 200,
body: { user: publicUser },
};
});
File Upload Handler
import { withHttpHandler } from '@leighton-digital/lambda-toolkit';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3Client = new S3Client({});
export const getUploadUrlHandler = withHttpHandler(async ({ event, metrics }) => {
const { fileName, contentType } = JSON.parse(event.body || '{}');
if (!fileName || !contentType) {
throw new ValidationError('fileName and contentType are required');
}
const key = `uploads/${Date.now()}-${fileName}`;
const command = new PutObjectCommand({
Bucket: process.env.UPLOAD_BUCKET,
Key: key,
ContentType: contentType,
});
const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
metrics.addMetric('UploadUrlGenerated', MetricUnits.Count, 1, {
contentType,
});
return {
statusCode: 200,
body: {
uploadUrl,
key,
expiresIn: 3600,
},
};
});
Authenticated Handler
import { withHttpHandler } from '@leighton-digital/lambda-toolkit';
import { UnauthorisedError, ForbiddenError } from '@leighton-digital/lambda-toolkit/errors';
export const protectedHandler = withHttpHandler(async ({ event, metrics }) => {
// Extract user from JWT token (from API Gateway authorizer)
const user = event.requestContext.authorizer?.user;
if (!user) {
throw new UnauthorisedError('Authentication required');
}
if (user.role !== 'admin') {
throw new ForbiddenError('Admin access required');
}
metrics.addMetric('AdminAction', MetricUnits.Count, 1, {
userId: user.id,
});
return {
statusCode: 200,
body: {
message: 'Admin access granted',
user: user.id,
},
};
});
Best Practices
- Error Handling: Use specific error types for better HTTP status code mapping
- Metrics: Add meaningful metrics with dimensional metadata for observability
- Validation: Validate input data early in your handler using schema validation
- Security: Let the framework handle headers automatically for consistent security
- Logging: Use the injected logger context for structured logging with correlation IDs
- Performance: Keep business logic separate from HTTP concerns for better testability
Dependencies
Required
@middy/core- Lambda middleware framework@middy/http-error-handler- HTTP error handling middlewarehttp-errors- Standardized HTTP error creation
Peer Dependencies
@aws-lambda-powertools/logger- Structured logging@aws-lambda-powertools/metrics- Custom metrics@aws-lambda-powertools/tracer- Distributed tracing
IAM Permissions
Ensure your Lambda execution role has:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"cloudwatch:PutMetricData"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords"
],
"Resource": "*"
}
]
}