コーディングルール
Guinness バックエンド(api-admin / api-user)共通のコーディング標準・規約・ベストプラクティス。
基本原則
- 型安全性 — 厳格な TypeScript を使用し、
any型を避ける - 明示性 — 可読性が向上する場合は暗黙的な推論より明示的な型を優先する
- 一貫性 — コードベースの既存パターンに従う
- シンプルさ — 巧妙なコードより明確でメンテナブルなコードを書く
ディレクトリ構造
apps/<app>/src/
├── config/ # 設定モジュール
├── middlewares/ # ミドルウェア関数
├── routes/ # API ルートハンドラー
│ └── v1/ # バージョン 1 のルート
├── services/ # ビジネスロジック
├── repositories/ # データアクセスレイヤー
├── lib/ # 共有ライブラリコード
├── utils/ # ユーティリティ関数
├── types/ # 型定義
│ ├── endpoint/ # エンドポイント固有の型
│ └── zod-openapi/ # Zod スキーマと OpenAPI
│ ├── components/ # 再利用可能なスキーマ
│ └── routes/ # ルート固有のスキーマ
├── app.ts # アプリケーション初期化
├── server.ts # サーバー設定
└── error.ts # エラー定義
レイヤードアーキテクチャ
| レイヤー | 責務 | 例 |
|---|---|---|
| Routes | HTTP ハンドリング・バリデーション・フォーマット | src/routes/v1/user.ts |
| Services | ビジネスロジック・オーケストレーション | src/services/user.ts |
| Repositories | データベース操作 | src/repositories/user.ts |
| Types | 型定義・スキーマ | src/types/zod-openapi/routes/user.ts |
| Middlewares | リクエスト / レスポンス処理 | src/middlewares/protected-route.ts |
| Config | 設定管理 | src/config/database.ts |
| Utils | 純粋なユーティリティ関数 | src/utils/pagination.ts |
命名規則
ファイル名
すべてのファイル名に kebab-case を使用する。テストファイルは同名に .test.ts を付ける。
変数
| 種類 | ケース | 例 |
|---|---|---|
| 真の定数 | UPPER_SNAKE_CASE |
MAX_FILE_SIZE, API_BASE_URL |
| 設定オブジェクト | camelCase |
databaseConfig |
| ローカル変数 | camelCase |
userId, isAdmin |
関数
camelCase で動詞始まりにする。非同期関数に async / Async サフィックスは付けない。
| 動詞 | 目的 | 例 |
|---|---|---|
find |
データの取得(複数) | findAll, findById |
findOne |
単一取得 | findOneById |
create |
新規作成 | createUser |
update |
更新 | updateProject |
softDelete |
論理削除 | softDelete |
is / has / can |
Boolean | isAdmin, hasPermission |
クラス・インターフェース・型
PascalCase を使用する。インターフェースに I プレフィックスは付けない。
interface User { }
interface CreateUserData { }
class NotFoundError extends Error { }
type Status = 'pending' | 'active';
API
| 対象 | ケース | 例 |
|---|---|---|
| エンドポイントパス | kebab-case resource names | /v1/des2code |
| パスパラメータ | snake_case(単数形) | :organization_id |
| クエリパラメータ | snake_case | ?created_after= |
| JSON キー | snake_case | "organization_id" |
データベース
| 対象 | ケース | 例 |
|---|---|---|
| テーブル名 | snake_case(複数形) | users, generated_codes |
| カラム名 | snake_case | cognito_sub, created_at |
TypeScript
tsconfig.json(必須設定)
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true
}
}
型定義
interface はオブジェクトの形状・公開 API・データモデルに使う。type はユニオン型・ユーティリティ型・関数シグネチャに使う。
// interface
interface User { id: string; name: string; }
interface AdminUser extends User { roleId: string; }
// type
type Status = 'pending' | 'processing' | 'completed' | 'failed';
type CreateUserData = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
any を使わない
// ✗ Bad
function processData(data: any) { return data.value; }
// ✓ Good
function processData(data: Record<string, unknown>) {
if (typeof data === 'object' && data !== null && 'value' in data) {
return data.value;
}
throw new Error('Invalid data format');
}
真に不明な型には unknown を使い、型ガードで絞り込む。
function handleError(error: unknown): void {
if (error instanceof Error) {
logger.error('Error occurred', { message: error.message });
} else {
logger.error('Unknown error', { error: String(error) });
}
}
Null 処理
// ✗ Bad
function getUserName(user: User) { return user.name.toUpperCase(); }
// ✓ Good
function getUserName(user: User): string {
return user.name?.toUpperCase() ?? 'Unknown';
}
関数の戻り値型
すべての関数・非同期関数に明示的な戻り値型を付ける。
// ✓ Good
export async function findUser(id: string): Promise<User | null> { }
// ✗ Avoid
export async function findUser(id: string) { }
ユニオン型を Enum より優先
イミュータビリティ
インポートルール
インポート順序
// 1. 外部ライブラリ
import { OpenAPIHono } from '@hono/zod-openapi';
// 2. 内部の絶対インポート(@ エイリアス)
import { logger } from '@GenAI-Guinness-backend/utils';
import * as userService from '@/services/user';
// 3. 相対インポート
import { formatOutput } from './utils';
// 4. 型インポート
import type { User } from '@/repositories/user';
パスエイリアス
src/ からの絶対インポートには @/ を使う。相対パスの ../.. による深いパスは避ける。
// ✓ Good
import * as userService from '@/services/user';
// ✗ Avoid
import * as userService from '../../services/user';
エラーハンドリング
型付きエラークラス
export class NotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'NotFoundError';
}
}
export class ForbiddenError extends Error {
constructor(message: string) {
super(message);
this.name = 'ForbiddenError';
}
}
レイヤー別の責務
// Routes — エラーをログして伝播
try {
const user = await userService.findOneById(userId, orgId, requester);
return c.json(formatOutput(user));
} catch (error) {
logger.error('Failed to fetch user', { error, userId });
throw error;
}
// Services — 型付きエラーをスロー
if (!user) throw new NotFoundError(`User ${userId} not found`);
if (!hasPermission(requester, user)) throw new ForbiddenError('Insufficient permissions');
共通パターン
CRUD 関数名
| レイヤー | 操作 | 関数名 |
|---|---|---|
| Service | リスト取得 | find() |
| Service | ID で取得 | findOneById() |
| Service | 作成 | create() |
| Service | 更新 | update() |
| Service | 論理削除 | softDelete() |
| Repository | ID で SELECT | findById() |
| Repository | 全件 SELECT | findAll() |
| Repository | INSERT | insert() |
| Repository | UPDATE | update() |
| Repository | 論理削除 | softDelete() |
ページネーション
export async function find(
organizationId: string,
requesterUsername: string,
options: PaginationOptions
): Promise<PaginatedResponse<User>> {
const { page, limit, sort } = options;
const offset = pagination.calculateOffset(page, limit);
const users = await userRepository.findAll({ organizationId, limit, offset, sort });
return { currentPage: page, totalCount: users.total, list: users.items };
}
テスト
ファイルの配置
ソースファイルの隣に置く(または __tests__/ ディレクトリ内):
テスト構造
import { describe, it, expect } from 'bun:test';
describe('User Service', () => {
describe('findById()', () => {
it('should return user when found', async () => {
// Arrange
const userId = 'user-123';
// Act
const user = await userService.findById(userId);
// Assert
expect(user).toBeDefined();
expect(user?.id).toBe(userId);
});
});
});
コマンド
bun test # 全テスト実行
bun test src/services/user.test.ts # 特定ファイルを実行
bun test --coverage # カバレッジ付き
bun run type-check # 型チェック
bun run lint # リンター実行
bun run lint:fix # 自動修正
コードレビューチェックリスト
型安全性
- [ ]
any型を使用していない - [ ] 関数に明示的な戻り値型がある
- [ ] null / undefined を適切に処理している
命名
- [ ] ファイルは kebab-case
- [ ] 変数は camelCase、定数は UPPER_SNAKE_CASE
- [ ] 関数は動詞始まり
- [ ] 型 / インターフェースは PascalCase
設計
- [ ] 正しいレイヤー(routes / services / repositories)に配置されている
- [ ] インポートが適切に整理されている(順序・エイリアス)
- [ ] 名前付きエクスポートのみ(デフォルトエクスポートなし)
エラーハンドリング
- [ ] エラーに型が付いている
- [ ] エラーが適切にログ出力されている
- [ ] 適切なレベルで try-catch を使用している
テスト
- [ ] サービスのユニットテストがある
- [ ] ルートの統合テストがある
- [ ] エッジケース・エラーケースがカバーされている