コーディング規約 - バックエンド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.ts の wrapHandler で暫定的にアダプターを使用している。今後、各ハンドラーを 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.ts、message.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);