mcpField
L
lambda-handler-pattern

lambda-handler-pattern

github.com/loxosceles-dev开发128 次安装
代码仓库

关于 lambda-handler-pattern

Lambda Handler Pattern 是面向 AWS Lambda 开发者的 TypeScript 参考模式,说明如何安排配置校验、客户端初始化和处理器逻辑。它将依赖放在模块级复用,并通过依赖注入使辅助函数更易测试。

主要功能

  1. 01冷启动时校验环境变量
  2. 02模块级初始化并复用客户端
  3. 03将处理器限制为轻量编排
  4. 04通过依赖注入编写纯辅助函数
  5. 05提供反模式与测试示例

适用场景

  1. 01减少每次调用时重复初始化
  2. 02在调用前发现缺失配置
  3. 03编写不依赖运行环境的单元测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/loxosceles-dev/dev-skills --skill lambda-handler-pattern

安装后在 Claude Code 中输入/lambda-handler-pattern确认已加载。

详细介绍

Lambda 处理程序模式

**这是一种参考模式。**了解其方法,并根据你的情况进行调整——不要原样照搬。

问题:Lambda 函数需要环境变量和 AWS 客户端,但初始化不当会导致冷启动问题,或产生难以测试的代码。

解决方案:在模块级别初始化并验证所有内容(冷启动时运行一次),并将依赖注入纯辅助函数。


核心模式

关键原则:Lambda 环境变量在运行时不可变。冷启动时验证一次,然后在整个模块中安全使用。

模块级别:环境变量 + AWS 客户端
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 文档和通行做法
  • ✅ 一致性:环境变量和客户端都在模块级别

用于验证的辅助函数

如需更清晰的验证和更有帮助的错误消息:

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 })
  };
}

各部分的放置位置

模块级别(处理程序外部)

在冷启动时初始化一次:

  • 环境变量验证(快速失败)
  • AWS SDK 客户端(DynamoDB、S3、SSM、Secrets Manager 等)
  • 数据库连接池
  • 使用连接池的 HTTP 客户端
  • 已编译的模板或架构
  • 不会变化的高开销计算
// ✅ 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();
  });
});

核心原则仍然适用

所有核心原则仍然有效:

  • 顺序:导入 → 环境验证 → 常量 → 客户端 → 类型 → 纯函数 → 非纯函数 → 处理程序
  • 不使用回退方案:缺少环境变量时快速失败
  • 显式错误:提供包含 SSM 参数路径的清晰错误消息
  • 类型安全:使用 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);
};

不好的原因:验证会在每次调用时运行,而不是在冷启动时运行一次。

❌ 不要:在处理程序中初始化客户端
// ❌ 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 热容器缓存的优势,性能更慢。

❌ 不要:在辅助函数中使用模块级变量
// ❌ 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, /* ... */ }));
}

相关内容:


渐进式改进

如果开发者纠正了本技能本应避免的行为,请提出对此技能的具体修订建议,以防止今后再次出现同样的纠正。

评论

支持 Markdown