コーディング規約 - バックエンドAPI

April 22, 2026 · View on GitHub

Lambda関数 (TypeScript)

1. アーキテクチャ概要

概要: API Gateway 配下の API は Express Monolith(Lambda Web Adapter)に集約し、クライアントから直接 invoke される Lambda は個別関数として定義する

Lambda 構成

  • Express Monolith (api/): API Gateway 経由の全 API を1つの Lambda で処理。Lambda Web Adapter (LWA) を使用して Express アプリを Lambda 上で実行する
  • 個別 Lambda: クライアント(Cognito Identity Pool)から直接 invoke される関数(predictStream.ts, invokeFlow.ts, copyVideoJob.ts, optimizePrompt.ts 等)

2. ファイル構成

概要: Express Monolith のルーティングは api/routes/ で管理し、ビジネスロジックは既存のハンドラーファイルに実装する

packages/cdk/lambda/
├── api/                             # Express Monolith(API Gateway 経由の API)
│   ├── index.ts                     # Express アプリのエントリーポイント
│   ├── run.sh                       # LWA 起動スクリプト
│   └── routes/                      # ルート定義
│       ├── helpers.ts               # 暫定アダプター(後述)
│       ├── chats.ts                 # /chats ルート
│       ├── predict.ts               # /predict ルート
│       └── ...                      # 機能別ルートファイル
├── *.ts                             # ビジネスロジック(API単位でファイル作成)
│                                    # 例: createChat.ts, deleteChat.ts, listMessages.ts
├── predictStream.ts                 # 直接 invoke 用 Lambda
├── invokeFlow.ts                    # 直接 invoke 用 Lambda
├── repository.ts                    # データアクセス層(メイン)
├── repositoryVideoJob.ts            # 特定機能用データアクセス層
├── useCaseBuilder/                  # 機能別ディレクトリ
│   ├── *.ts                         # 機能内のAPI
│   └── useCaseBuilderRepository.ts  # 機能専用データアクセス層
└── utils/                           # 共通ユーティリティ
    ├── bedrockApi.ts               # Bedrock API呼び出し
    ├── bedrockKbApi.ts             # Bedrock Knowledge Base API
    ├── bedrockAgentApi.ts          # Bedrock Agent API
    ├── bedrockClient.ts            # Bedrockクライアント初期化
    ├── sagemakerApi.ts             # SageMaker API呼び出し
    ├── models.ts                   # モデル定義・設定
    ├── auth.ts                     # 認証処理
    └── api.ts                      # API共通処理

ファイル分割ルール

  • ビジネスロジック: API単位で lambda/ 直下にファイル作成(ファイル名でAPIの役割を表現)
  • ルート定義: api/routes/ 配下に機能別ルートファイルを作成
  • 大きな機能単位: メイン機能と独立して開発可能な場合はフォルダ分離
    • 独立性の基準: 独自に開発を進められ、独立して起動可能なレベル
    • 例: useCaseBuilder/ - Use Case Builder機能一式
    • utils等の共通機能は引き続き共有する
  • データアクセス層: repository.tsで一元管理、機能別に必要な場合のみ分離
  • 共通処理: utils/配下に外部サービス別に分割

暫定アダプター(helpers.ts)について

現在、既存のハンドラー(APIGatewayProxyEvent ベース)を Express ルートに接続するため、api/routes/helpers.tswrapHandler で暫定的にアダプターを使用している。今後、各ハンドラーを Express ネイティブ(req/res)に段階的にリファクタリングし、このアダプターを廃止する予定。

// 現状(暫定): wrapHandler で既存ハンドラーをラップ
import { handler as createChatHandler } from '../../createChat';
import { wrapHandler } from './helpers';

router.post('/', wrapHandler(createChatHandler));

// 将来(目標): Express ネイティブなルートハンドラー
router.post('/', async (req: Request, res: Response) => {
  const userId = getUserId(req);
  const result = await createChat(userId, req.body);
  res.json(result);
});

フォルダ分離の判断基準

// ✅ 良い例 - 独立性が高い機能はフォルダ分離
useCaseBuilder/
├── createUseCase.ts
├── listUseCases.ts
└── useCaseBuilderRepository.ts

// ✅ 良い例 - 関連するAPIでも独立性が低い場合は同一階層
createChat.ts
deleteChat.ts
listChats.ts

// ❌ 悪い例 - 過度な分離
chat/
├── create.ts    # 単純なCRUD操作のみ
├── delete.ts
└── list.ts

3. Lambda関数の基本構造

概要: Express Monolith 内のルートハンドラーと、直接 invoke 用の個別 Lambda ハンドラーの2パターンがある

Express ルートハンドラー(API Gateway 経由)

現在は暫定アダプター経由で既存ハンドラーを使用しているが、新規実装やリファクタリング時は Express ネイティブで実装する。

// ✅ 良い例 - Express ネイティブなルートハンドラー(目標形式)
import { Router, Request, Response } from 'express';

export const router = Router();

router.post('/', async (req: Request, res: Response) => {
  try {
    const userId =
      req.apiGateway.event.requestContext.authorizer.claims['cognito:username'];
    const result = await businessLogic(userId, req.body);
    res.json(result);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'Internal Server Error' });
  }
});

直接 invoke 用 Lambda ハンドラー

// ✅ 良い例 - 標準的なハンドラー構造
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  try {
    const userId: string =
      event.requestContext.authorizer!.claims['cognito:username'];
    const result = await businessLogic(userId, event.body);

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
      },
      body: JSON.stringify(result),
    };
  } catch (error) {
    console.log(error);
    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
      },
      body: JSON.stringify({ message: 'Internal Server Error' }),
    };
  }
};
};

// ❌ 悪い例 - 型定義なし、エラーハンドリングなし
export const handler = async (event: any) => {
  const result = await someFunction();
  return result;
};

4. 環境変数の使用

概要: 環境変数は型安全に取得し、必須項目は起動時にチェックする

// ✅ 良い例 - 型安全な環境変数取得
const TABLE_NAME: string = process.env.TABLE_NAME!;
const MODEL_REGION: string = process.env.MODEL_REGION!;

// ✅ 良い例 - オプショナルな環境変数
const KNOWLEDGE_BASE_ID: string | undefined = process.env.KNOWLEDGE_BASE_ID;

// ❌ 悪い例 - 型指定なし、nullチェックなし
const tableName = process.env.TABLE_NAME;

5. データアクセス層

概要: repository.tsでデータアクセスを一元管理し、ビジネスロジックと分離する。DynamoDBの効率的なクエリパターンを使用する

DynamoDB操作のベストプラクティス

// ✅ 良い例 - QueryCommandを使用(効率的)
export const getChatsByUserId = async (userId: string): Promise<Chat[]> => {
  const result = await dynamoDbDocument.send(
    new QueryCommand({
      TableName: TABLE_NAME,
      KeyConditionExpression: 'id = :userId',
      ExpressionAttributeValues: {
        ':userId': `user#${userId}`,
      },
    })
  );
  return result.Items as Chat[];
};

// ✅ 良い例 - GSI(Global Secondary Index)を使用
export const getChatsByUseCase = async (usecase: string): Promise<Chat[]> => {
  const result = await dynamoDbDocument.send(
    new QueryCommand({
      TableName: TABLE_NAME,
      IndexName: 'UseCaseIndex', // GSIを使用
      KeyConditionExpression: 'usecase = :usecase',
      ExpressionAttributeValues: {
        ':usecase': usecase,
      },
    })
  );
  return result.Items as Chat[];
};

// ✅ 良い例 - BatchGetCommandで複数アイテムを効率的に取得
export const getMultipleChats = async (chatIds: string[]): Promise<Chat[]> => {
  const result = await dynamoDbDocument.send(
    new BatchGetCommand({
      RequestItems: {
        [TABLE_NAME]: {
          Keys: chatIds.map((id) => ({ id })),
        },
      },
    })
  );
  return (result.Responses?.[TABLE_NAME] as Chat[]) || [];
};

// ❌ 悪い例 - ScanCommandを使用(全件走査)
export const getAllChats = async (): Promise<Chat[]> => {
  const result = await dynamoDbDocument.send(
    new ScanCommand({
      TableName: TABLE_NAME, // 全件走査は非効率
    })
  );
  return result.Items as Chat[];
};

// ❌ 悪い例 - N+1クエリ問題
export const getChatsWithMessages = async (userId: string) => {
  const chats = await getChatsByUserId(userId);

  // 各チャットに対して個別にクエリ実行(N+1問題)
  for (const chat of chats) {
    chat.messages = await getMessagesByChatId(chat.chatId);
  }

  return chats;
};

データアクセスのルール

  • 禁止: ScanCommand(全件走査)の使用
  • 必須: QueryCommandまたはGetItemCommandを使用
  • 推奨: GSI(Global Secondary Index)の活用
  • N+1クエリ: 原則禁止(ただし、データ量が少ない場合は許可)
  • バッチ処理: BatchGetCommand、BatchWriteCommandを活用
  • 分離: repository.tsでデータアクセスロジックを集約

6. 外部API呼び出し

概要: utils配下にAPI別のファイルを作成し、エラーハンドリングを適切に行う

Bedrock API呼び出し

// ✅ 良い例 - utils/bedrockApi.tsで外部API呼び出しを集約
import {
  InvokeModelCommand,
  ConverseCommand,
} from '@aws-sdk/client-bedrock-runtime';
import { initBedrockRuntimeClient } from './bedrockClient';

export const invokeModel = async (params: InvokeParams): Promise<Response> => {
  try {
    const client = initBedrockRuntimeClient();
    const command = new InvokeModelCommand(params);
    const response = await client.send(command);
    return response;
  } catch (error) {
    if (error instanceof ThrottlingException) {
      // 適切なエラーハンドリング
      throw new Error('Rate limit exceeded');
    }
    throw error;
  }
};

// ❌ 悪い例 - Lambda関数内で直接API呼び出し
export const handler = async (event: APIGatewayProxyEvent) => {
  const client = new BedrockRuntimeClient({});
  // 直接API呼び出し
};

7. エラーハンドリング

概要: try-catchで例外を捕捉し、適切なHTTPステータスコードとエラーメッセージを返す

// ✅ 良い例 - 基本的なエラーハンドリング
export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  try {
    const userId: string =
      event.requestContext.authorizer!.claims['cognito:username'];
    const result = await businessLogic(userId);

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
      },
      body: JSON.stringify(result),
    };
  } catch (error) {
    console.log(error);
    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
      },
      body: JSON.stringify({ message: 'Internal Server Error' }),
    };
  }
};

// ✅ 良い例 - エラー種別による分岐
try {
  const result = await businessLogic();
  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
    },
    body: JSON.stringify(result),
  };
} catch (error) {
  console.log(error);

  // エラー種別による処理分岐
  if (error instanceof ValidationError) {
    return {
      statusCode: 400,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
      },
      body: JSON.stringify({ message: 'Invalid input' }),
    };
  }

  return {
    statusCode: 500,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
    },
    body: JSON.stringify({ message: 'Internal Server Error' }),
  };
}

// ❌ 悪い例 - エラーハンドリングなし
export const handler = async (event: APIGatewayProxyEvent) => {
  const result = await businessLogic(); // エラー時に例外が伝播
  return { statusCode: 200, body: JSON.stringify(result) };
};

// ❌ 悪い例 - 不適切なステータスコード
catch (error) {
  return { statusCode: 200, body: JSON.stringify({ error: 'Failed' }) };
}

レスポンス構造の統一

// 成功レスポンス
{
  statusCode: 200,
  headers: {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*',
  },
  body: JSON.stringify(data),
}

// エラーレスポンス
{
  statusCode: 500,
  headers: {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*',
  },
  body: JSON.stringify({ message: 'Error message' }),
}

HTTPステータスコード

概要: 一般的なHTTPステータスコードを使用する

// ✅ 良い例 - 一般的なステータスコード
200: // 成功
400: // バリデーションエラー
401: // 認証エラー
403: // 認可エラー
404: // リソースが見つからない
500: // サーバー内部エラー

// ❌ 悪い例 - 独自ステータスコード
299: // 独自の成功コード
450: // 独自のエラーコード

8. 型定義

概要: バックエンドとフロントエンドで共有する型定義はpackages/types/src内に定義し、APIスキーマはprotocol.d.tsで管理する

型定義の配置

  • 共有型定義: packages/types/src/ - バックエンドとフロントエンド間で共有する全ての型
  • APIスキーマ: packages/types/src/protocol.d.ts - API Request/Response型の専用ファイル
  • エンティティ型: packages/types/src/chat.d.tsmessage.d.ts等 - ドメインオブジェクトの型定義
  • ファイル拡張子: .d.tsを使用(型定義専用ファイル)

APIスキーマ定義(protocol.d.ts)

  • Request型: APIエンドポイントへの入力パラメータを定義
  • Response型: APIエンドポイントからの戻り値を定義
  • 命名規則: {機能名}Request{機能名}Responseの形式
  • 用途: フロントエンドとバックエンドで型安全性を保証

共通型の使用

  • 基本エンティティ: Chat、Message、SystemContext等の既存型を使用
  • AWS SDK型: DynamoDB、Bedrock等のAWS SDK提供型を活用
  • 独自型: 既存型で表現できない場合のみ最小限で定義

型定義のルール

  • 共有型: packages/types/src内で定義し、generative-ai-use-casesパッケージ経由でimport
  • APIスキーマ: protocol.d.tsでRequest/Response型を定義
  • 独自型: 必要最小限に留め、既存型との重複を避ける
  • 禁止: any型の使用、型なしでの実装

9. ログ出力

概要: console.logを使用し、適切なログレベルで出力する

// ✅ 良い例 - 適切なログ出力
console.log('Processing request for user:', userId);
console.error('Error occurred:', error);

// ✅ 良い例 - 構造化ログ
console.log(
  JSON.stringify({
    level: 'info',
    message: 'Chat created',
    userId,
    chatId,
    timestamp: new Date().toISOString(),
  })
);

// ❌ 悪い例 - 機密情報のログ出力
console.log('User credentials:', event.headers.authorization);

10. 非同期処理

概要: async/awaitを使用し、Promise.allで並列処理を活用する

// ✅ 良い例 - 並列処理の活用
const [userData, chatHistory] = await Promise.all([
  getUserData(userId),
  getChatHistory(chatId),
]);

// ✅ 良い例 - 適切なエラーハンドリング付き非同期処理
try {
  const result = await asyncOperation();
  return result;
} catch (error) {
  console.error('Async operation failed:', error);
  throw error;
}

// ❌ 悪い例 - 逐次処理
const userData = await getUserData(userId);
const chatHistory = await getChatHistory(chatId);

11. 認可処理

概要: userIdを取得してデータアクセスを制御し、ユーザーが自分のデータのみにアクセスできるようにする

// ✅ 良い例 - 適切な認可処理
export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  try {
    // Cognitoからユーザー情報を取得
    const userId: string =
      event.requestContext.authorizer!.claims['cognito:username'];

    // userIdを使ってデータアクセスを制御
    const userChats = await getChatsByUserId(userId);
    const userMessages = await getMessagesByUserIdAndChatId(userId, chatId);

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
      },
      body: JSON.stringify({ chats: userChats }),
    };
  } catch (error) {
    console.log(error);
    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
      },
      body: JSON.stringify({ message: 'Internal Server Error' }),
    };
  }
};

// ✅ 良い例 - repository層での認可制御
export const getChatsByUserId = async (userId: string): Promise<Chat[]> => {
  const result = await dynamoDbDocument.send(
    new QueryCommand({
      TableName: TABLE_NAME,
      KeyConditionExpression: 'id = :userId',
      ExpressionAttributeValues: {
        ':userId': `user#${userId}`, // userIdでフィルタリング
      },
    })
  );
  return result.Items as Chat[];
};

// ❌ 悪い例 - 認可制御なし
export const handler = async (event: APIGatewayProxyEvent) => {
  // userIdを取得せずに全データを返す
  const allChats = await getAllChats(); // 他ユーザーのデータも含む
  return {
    statusCode: 200,
    body: JSON.stringify({ chats: allChats }),
  };
};

// ❌ 悪い例 - クライアントから送信されたuserIdを信頼
export const handler = async (event: APIGatewayProxyEvent) => {
  const requestBody = JSON.parse(event.body || '{}');
  const userId = requestBody.userId; // クライアントから送信された値を使用(危険)

  const userChats = await getChatsByUserId(userId);
  // 他ユーザーのデータにアクセス可能
};

認可処理のベストプラクティス

  • 必須: Cognitoの認証情報からuserIdを取得
  • データアクセス: 全てのクエリでuserIdによるフィルタリングを実装
  • 禁止: クライアントから送信されたuserIdの使用
  • 原則: ユーザーは自分のデータのみにアクセス可能

概要: 不要な処理を避け、効率的な実装を心がける

// ✅ 良い例 - 早期リターン
if (!userId) {
  return errorResponse(400, 'User ID is required');
}

// ✅ 良い例 - 必要な場合のみ重い処理を実行
if (shouldProcessLargeData) {
  const result = await heavyProcessing();
}

// ❌ 悪い例 - 不要な処理
const allData = await getAllData(); // 大量データ取得
const filteredData = allData.filter((item) => item.userId === userId);