コンテンツにスキップ

コーディングルール

Guinness バックエンド(api-admin / api-user)共通のコーディング標準・規約・ベストプラクティス。


基本原則

  1. 型安全性 — 厳格な TypeScript を使用し、any 型を避ける
  2. 明示性 — 可読性が向上する場合は暗黙的な推論より明示的な型を優先する
  3. 一貫性 — コードベースの既存パターンに従う
  4. シンプルさ — 巧妙なコードより明確でメンテナブルなコードを書く

ディレクトリ構造

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(API レイヤー)
    ↓
Services(ビジネスロジック)
    ↓
Repositories(データアクセス)
    ↓
Database
レイヤー 責務 例
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 を付ける。

✓ user-service.ts
✓ auth-middleware.ts
✓ user-service.test.ts
✗ userService.ts
✗ user_service.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 より優先

// ✓ Preferred
type Status = 'pending' | 'processing' | 'completed' | 'failed';

イミュータビリティ

// ✓ Good
const updatedUser = { ...user, name: 'New Name' };

// ✗ Avoid
user.name = 'New Name';

インポートルール

インポート順序

// 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__/ ディレクトリ内):

src/services/user.ts  →  user.test.ts(隣)
                     または
__tests__/unit/services/user.test.ts

テスト構造

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 を使用している

テスト

  • [ ] サービスのユニットテストがある
  • [ ] ルートの統合テストがある
  • [ ] エッジケース・エラーケースがカバーされている