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 の strict モードを使用します
  • 依存性注入:クライアントと設定をヘルパー関数に渡します

アンチパターン

❌ しないでください:ハンドラー内で環境変数を検証する
// ❌ 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, /* ... */ }));
}

関連項目:


段階的な改善

このスキルで防ぐべき動作を開発者が修正した場合は、同じ修正が今後必要にならないよう、このスキルへの具体的な追記を提案してください。

コメント