Loading...
Loading...
Architecture pattern for Lambda handlers. Env vars validated at module level, AWS clients at module scope, pure business logic with injected dependencies. Apply when creating or modifying any Lambda function.
npx skill4agent add loxosceles/ai-dev lambda-handler-patternimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
// 1. Validate environment variables (fail fast on cold start)
const TABLE_NAME = process.env.TABLE_NAME;
const API_KEY = process.env.API_KEY;
const REGION = process.env.AWS_REGION;
if (!TABLE_NAME) {
throw new Error('TABLE_NAME not set. Configure in SSM: /myapp/${env}/table-name');
}
if (!API_KEY) {
throw new Error('API_KEY not set. Configure in SSM: /myapp/${env}/api-key');
}
if (!REGION) {
throw new Error('AWS_REGION not set');
}
// 2. Initialize AWS clients (cached across warm invocations)
const dynamoClient = new DynamoDBClient({ region: REGION });
const docClient = DynamoDBDocumentClient.from(dynamoClient);
// 3. Handler: Thin orchestration layer
export const handler = async (event: APIGatewayProxyEvent) => {
return processEvent(event, TABLE_NAME, API_KEY, docClient);
};
// 4. Pure function: All dependencies injected
async function processEvent(
event: APIGatewayProxyEvent,
tableName: string,
apiKey: string,
client: DynamoDBDocumentClient
) {
// Business logic here - fully testable without env vars
const body = JSON.parse(event.body || '{}');
await client.send(new PutCommand({
TableName: tableName,
Item: { id: body.id, data: body.data }
}));
return {
statusCode: 200,
body: JSON.stringify({ success: true })
};
}function getRequiredEnv(key: string, ssmPath?: string): string {
const value = process.env[key];
if (!value) {
const hint = ssmPath ? ` Configure in SSM: ${ssmPath}` : '';
throw new Error(`${key} environment variable not set.${hint}`);
}
return value;
}
// Usage
const TABLE_NAME = getRequiredEnv('TABLE_NAME', '/myapp/${env}/table-name');
const API_KEY = getRequiredEnv('API_KEY', '/myapp/${env}/api-key');
const REGION = getRequiredEnv('AWS_REGION');import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
// Helper: Validate required env vars
function getRequiredEnv(key: string, ssmPath?: string): string {
const value = process.env[key];
if (!value) {
const hint = ssmPath ? ` Configure in SSM: ${ssmPath}` : '';
throw new Error(`${key} environment variable not set.${hint}`);
}
return value;
}
// Module level: Validate env vars
const TABLE_NAME = getRequiredEnv('TABLE_NAME', '/myapp/${env}/table-name');
const API_KEY = getRequiredEnv('API_KEY', '/myapp/${env}/api-key');
const REGION = getRequiredEnv('AWS_REGION');
// Module level: Initialize AWS clients
const dynamoClient = new DynamoDBClient({ region: REGION });
const docClient = DynamoDBDocumentClient.from(dynamoClient);
// Handler: Thin orchestration
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
return processEvent(event, TABLE_NAME, API_KEY, docClient);
};
// Pure function: All dependencies injected
async function processEvent(
event: APIGatewayProxyEvent,
tableName: string,
apiKey: string,
client: DynamoDBDocumentClient
): Promise<APIGatewayProxyResult> {
const body = JSON.parse(event.body || '{}');
// Validate API key from request
if (event.headers['x-api-key'] !== apiKey) {
return {
statusCode: 401,
body: JSON.stringify({ error: 'Unauthorized' })
};
}
// Store in DynamoDB
await client.send(new PutCommand({
TableName: tableName,
Item: { id: body.id, data: body.data, timestamp: Date.now() }
}));
return {
statusCode: 200,
body: JSON.stringify({ success: true })
};
}// ✅ Module level
const TABLE_NAME = getRequiredEnv('TABLE_NAME');
const s3Client = new S3Client({});
const ssmClient = new SSMClient({});
const httpClient = new HttpClient({ keepAlive: true });// ✅ Handler: Orchestration only
export const handler = async (event: APIGatewayProxyEvent) => {
// Delegate to pure functions
return processRequest(event, TABLE_NAME, REGION, s3Client);
};// Test without environment variables
describe('processEvent', () => {
it('stores item in DynamoDB', async () => {
const mockClient = createMockDocClient();
const event = createMockEvent({ id: '123', data: 'test' });
const result = await processEvent(
event,
'test-table',
'test-api-key',
mockClient
);
expect(result.statusCode).toBe(200);
expect(mockClient.send).toHaveBeenCalledWith(
expect.objectContaining({
input: {
TableName: 'test-table',
Item: { id: '123', data: 'test', timestamp: expect.any(Number) }
}
})
);
});
it('returns 401 for invalid API key', async () => {
const mockClient = createMockDocClient();
const event = createMockEvent({ id: '123' }, { 'x-api-key': 'wrong-key' });
const result = await processEvent(event, 'test-table', 'correct-key', mockClient);
expect(result.statusCode).toBe(401);
expect(mockClient.send).not.toHaveBeenCalled();
});
});// ❌ Validates on every invocation (wasteful)
export const handler = async (event: APIGatewayProxyEvent) => {
const tableName = process.env.TABLE_NAME;
if (!tableName) throw new Error('TABLE_NAME not set');
return processEvent(event, tableName);
};// ❌ Recreates client on every invocation
export const handler = async (event: APIGatewayProxyEvent) => {
const dynamoClient = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(dynamoClient);
await docClient.send(new PutCommand({ /* ... */ }));
};// ❌ Helper function depends on global state
const TABLE_NAME = process.env.TABLE_NAME!;
function processEvent(event: APIGatewayProxyEvent) {
// Uses global TABLE_NAME - not pure, hard to test
await docClient.send(new PutCommand({ TableName: TABLE_NAME, /* ... */ }));
}// ✅ Pure function with explicit dependencies
const TABLE_NAME = getRequiredEnv('TABLE_NAME');
function processEvent(
event: APIGatewayProxyEvent,
tableName: string,
client: DynamoDBDocumentClient
) {
// All dependencies explicit - easy to test
await client.send(new PutCommand({ TableName: tableName, /* ... */ }));
}