lambda-handler-pattern
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLambda Handler Pattern
Lambda Handler 架构模式
This is a reference pattern. Learn from the approach, adapt to your context — don't copy verbatim.
Problem: Lambda functions need environment variables and AWS clients, but improper initialization causes cold start issues or hard-to-test code.
Solution: Initialize and validate everything at module level (runs once on cold start), inject dependencies into pure helper functions.
这是一个参考模式。请借鉴其思路,适配你自己的场景——不要直接逐字复制。
问题:Lambda函数需要环境变量和AWS客户端,但初始化不当会导致冷启动问题或难以测试的代码。
解决方案:在模块层面初始化并验证所有内容(冷启动时仅运行一次),将依赖注入到纯工具函数中。
Core Pattern
核心模式
Key Principle: Lambda environment variables are immutable at runtime. Validate once on cold start, use safely throughout the module.
核心原则:Lambda环境变量在运行时是不可变的。冷启动时仅验证一次,即可在整个模块中安全使用。
Module Level: Environment Variables + AWS Clients
模块层面:环境变量 + AWS客户端
typescript
import { 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 })
};
}Why This Pattern:
- ✅ Fail fast: Missing config caught on cold start, before any invocation
- ✅ Performance: Clients cached across warm invocations
- ✅ Testability: Helper functions are pure, dependencies injected
- ✅ Industry standard: Aligns with AWS documentation and common practice
- ✅ Consistency: Both env vars and clients at module level
typescript
import { 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 })
};
}为什么选择这个模式:
- ✅ 快速失败:缺失配置在冷启动阶段就被捕获,不会等到实际调用时才报错
- ✅ 性能优异:客户端在热调用之间被缓存
- ✅ 可测试性强:工具函数是纯函数,依赖被注入
- ✅ 符合行业标准:与AWS官方文档和通用实践保持一致
- ✅ 一致性高:环境变量和客户端都在模块层面定义
Helper Function for Validation
验证用的辅助函数
For cleaner validation with helpful error messages:
typescript
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');用于实现更简洁的验证,附带友好的错误提示:
typescript
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');Complete Example
完整示例
typescript
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 })
};
}typescript
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 })
};
}What Goes Where
代码放置规则
Module Level (Outside Handler)
模块层面(Handler外部)
Initialize once on cold start:
- Environment variable validation (fail fast)
- AWS SDK clients (DynamoDB, S3, SSM, Secrets Manager, etc.)
- Database connection pools
- HTTP clients with connection pooling
- Compiled templates or schemas
- Heavy computations that don't change
typescript
// ✅ Module level
const TABLE_NAME = getRequiredEnv('TABLE_NAME');
const s3Client = new S3Client({});
const ssmClient = new SSMClient({});
const httpClient = new HttpClient({ keepAlive: true });冷启动时仅初始化一次:
- 环境变量验证(快速失败)
- AWS SDK客户端(DynamoDB、S3、SSM、Secrets Manager等)
- 数据库连接池
- 带连接池的HTTP客户端
- 编译后的模板或 schema
- 不发生变化的重计算逻辑
typescript
// ✅ Module level
const TABLE_NAME = getRequiredEnv('TABLE_NAME');
const s3Client = new S3Client({});
const ssmClient = new SSMClient({});
const httpClient = new HttpClient({ keepAlive: true });Handler Level (Inside Handler)
Handler层面(Handler内部)
Thin orchestration only:
- Parse event data
- Call pure helper functions with injected dependencies
- Return response
typescript
// ✅ Handler: Orchestration only
export const handler = async (event: APIGatewayProxyEvent) => {
// Delegate to pure functions
return processRequest(event, TABLE_NAME, REGION, s3Client);
};仅做轻量编排:
- 解析事件数据
- 传入依赖调用纯工具函数
- 返回响应
typescript
// ✅ Handler: Orchestration only
export const handler = async (event: APIGatewayProxyEvent) => {
// Delegate to pure functions
return processRequest(event, TABLE_NAME, REGION, s3Client);
};Testing Benefits
测试优势
Pure functions are easy to test without environment setup:
typescript
// 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();
});
});纯函数无需配置环境即可轻松测试:
typescript
// 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();
});
});Core Principles Still Apply
核心原则仍然适用
All Core Principles remain valid:
- Ordering: Imports → Env validation → Constants → Clients → Types → Pure functions → Impure functions → Handler
- No Fallbacks: Fail fast if environment variables are missing
- Explicit Errors: Clear error messages with SSM parameter paths
- Type Safety: Use TypeScript strict mode
- Dependency Injection: Pass clients and config to helper functions
所有核心原则依然有效:
- 代码顺序:导入 → 环境验证 → 常量 → 客户端 → 类型 → 纯函数 → 非纯函数 → Handler
- 无降级逻辑:如果环境变量缺失则快速失败
- 错误明确:附带SSM参数路径的清晰错误提示
- 类型安全:使用TypeScript严格模式
- 依赖注入:将客户端和配置传入工具函数
Anti-Patterns
反模式
❌ Don't: Validate env vars in handler
❌ 禁止:在Handler中验证环境变量
typescript
// ❌ 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);
};Why bad: Validation runs on every invocation instead of once on cold start.
typescript
// ❌ 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);
};为什么不好:验证逻辑会在每次调用时都运行,而不是冷启动时仅运行一次。
❌ Don't: Initialize clients in handler
❌ 禁止:在Handler中初始化客户端
typescript
// ❌ 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({ /* ... */ }));
};Why bad: Loses Lambda's warm container caching benefits, slower performance.
typescript
// ❌ 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({ /* ... */ }));
};为什么不好:会丢失Lambda热容器缓存的优势,降低性能。
❌ Don't: Use module-level vars in helper functions
❌ 禁止:在工具函数中使用模块级变量
typescript
// ❌ 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, /* ... */ }));
}Why bad: Function is not pure, harder to test, hidden dependencies.
typescript
// ❌ 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, /* ... */ }));
}为什么不好:函数不是纯函数,更难测试,依赖是隐式的。
✅ Do: Inject dependencies
✅ 推荐:注入依赖
typescript
// ✅ 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, /* ... */ }));
}Related:
- Core Principles - Ordering, purity, error handling
- Environment Validation - Validating required config
- Resource Naming - Lambda layer organization
typescript
// ✅ 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, /* ... */ }));
}相关内容:
- 核心原则 - 顺序、纯度、错误处理
- 环境变量验证 - 验证必要配置
- 资源命名 - Lambda层代码组织
Progressive Improvement
持续改进
If the developer corrects a behavior that this skill should have prevented, suggest a specific amendment to this skill to prevent the same correction in the future.
如果开发者修正了本规范本应避免的问题,请提出对本规范的具体修改建议,避免后续再出现同类问题。