Skip to main content

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 middleware
  • http-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": "*"
}
]
}