コーディングルール
命名規則
| 対象 | 規則 | 例 |
|---|---|---|
| 変数・関数 | camelCase | getCandidateById |
| 型・インターフェース | PascalCase | ArrangeSettings |
| ファイル名 | kebab-case | survey-import.ts / use-project-arrange.ts |
| 定数 | UPPER_SNAKE_CASE | VALID_STATUS_TRANSITIONS |
| DB テーブル・カラム | snake_case(複数形テーブル) | project_candidates.external_user_id |
| API の公開フィールド | snake_case | interview_duration_minutes |
| サービス以降の内部コード | camelCase | interviewDurationMinutes |
| React コンポーネント | PascalCase(ファイルは kebab-case) | SegmentSidebar / segment-sidebar.tsx |
| Orval 生成フック | パスから自動命名(変更しない) | useGetApiV1ProjectsProjectId |
snake_case ↔ camelCase の変換位置
API の公開フィールドは snake_case、内部は camelCase。この変換はルートハンドラ内で明示的に行う(data.interview_duration_minutes → interviewDurationMinutes)。service 層に snake_case を持ち込まない。
フォーマッター・リンター
# backend
bun run format # prettier --write 'src/**/*.ts'
bun run lint # eslint src/
bun run lint:fix
bun run check-types # tsc --noEmit
# frontend
bun run format
bun run lint
bun run check-types
設定ファイル:
| ファイル | 内容 |
|---|---|
.prettierrc.mjs |
semi: true / singleQuote: true / printWidth: 100 / trailingComma: 'es5' / tabWidth: 2 |
eslint.config.js(backend) |
@typescript-eslint recommended + 独自ルール |
eslint.config.mjs(frontend) |
eslint-config-next + prettier |
主な ESLint ルール(backend):
| ルール | 設定 |
|---|---|
@typescript-eslint/no-unused-vars |
error。_ プレフィックスは除外 |
@typescript-eslint/no-explicit-any |
warn |
no-console |
warn(console.warn / console.error のみ許可。通常は logger を使う) |
@typescript-eslint/explicit-function-return-type |
off |
両リポジトリとも husky + lint-staged により、コミット時に src/** へ prettier --write と eslint --fix が走る。
backend の実装規約
import は ESM の .js 拡張子付き
TypeScript のソースを指す場合も .js と書く。
レスポンスは必ず response ヘルパーを通す
src/lib/response.ts の response.success / created / paginated / error を使う。ボディは必ず封筒形式になる。
return response.success(c, project);
return response.created(c, project);
return response.paginated(c, items, pagination);
c.json() を直接呼ばない。
エラーは throw するだけ
middlewares/error-handler.ts が status / code / メッセージに変換する。ハンドラ内で try-catch してレスポンスを組み立てない。
if (!project) throw new NotFoundError('Project'); // 404
throw new InvalidStatusTransitionError(oldStatus, newStatus); // 400
| 定義場所 | 内容 |
|---|---|
src/lib/errors.ts |
汎用(HTTP / DB / 外部 API / Databricks) |
src/error.ts |
業務固有(Google API、スケジューリングトークン、枠競合、ステータス遷移など) |
isOperational が false のものはクライアントに詳細を出さない(開発時のみ stack を含める)。
層をまたいだ呼び出しをしない
routes → services → repositories → Drizzle の一方向依存を守る。
- ルートから Drizzle を直接呼ばない
- service に Hono の
Contextを渡さない - repository に業務判断を書かない
環境変数は getEnvConfig() 経由
process.env を直接読まない。特に PROTOTYPE_MODE の判定は専用ヘルパーを使う。
if (isEmailEnabled()) { ... } // ✅
if (isCalendarEnabled()) { ... } // ✅
if (process.env.PROTOTYPE_MODE) { ... } // ❌
例外はバッチ(src/batch/*.ts)。アプリ用の必須環境変数が揃わない環境で動かすため、DATABASE_URL だけを直接読む。
ルート定義はハンドラと対で書く
const getProjectRoute = createRoute({
method: 'get',
path: '/{projectId}',
tags: ['Projects'],
summary: 'プロジェクト詳細取得',
security: [{ bearerAuth: [] }],
request: { params: ProjectIdParamSchema },
responses: { 200: { ... }, 404: commonResponses[404] },
});
projectsRoutes.openapi(getProjectRoute, async (c) => { ... });
エラーレスポンスは commonResponses を再利用する。
frontend の実装規約
src/api/** は生成物。手で編集しない
API を変えたら backend を起動して bun run generate-api を実行する。
認証トークンの出入口は src/lib/auth.ts のみ
localStorage を直接触らない。setAuthToken / getAuthToken / removeAuthToken を使う。
API レスポンスは apiData() で取り出す
response.data.data を手で辿らない。
状態の置き場所
| 種類 | 置き場所 |
|---|---|
| サーバ状態 | React Query(Orval 生成フック) |
| ローカル UI 状態 | 各コンポーネントの useState |
| 共有プロバイダ | src/components/providers.tsx |
| アレンジフローの状態 | use-project-arrange.ts に集約。画面は薄く保つ |
クラス名の合成は cn()
型の二重定義について
ArrangeSettings(projects.arrange_settings JSON カラムの中身)は、OpenAPI では内部構造を表現できないため以下の 2 か所に二重定義されている。
| ファイル | 用途 |
|---|---|
backend src/db/schema.ts |
DB・service 側 |
frontend src/types/arrange-settings.ts |
画面側 |
片方を変えたら必ずもう片方も直す。
PR とレビューの基準
- 1 PR は 1 つの目的に絞る
- 依頼されていない機能追加・リファクタ・抽象化を混ぜない
- API を変更した PR では、
bun run generate-apiの結果(src/api/**の差分)を同じ PR に含める - DB スキーマを変更した PR では、データベース のドキュメントも更新する
- セルフレビューをしてからレビュー依頼する
git push --force系は行わない
アンチパターン
any型の多用(no-explicit-anyは warn だが、原則使わない)console.logの直書き(loggerを使う。console.warn/console.errorのみ許可)- ルートハンドラでの Drizzle 直呼び
c.json()の直接呼び出し(responseヘルパーを使う)- ハンドラ内での try-catch によるエラーレスポンス組み立て(
throwする) process.envの直読み- 生成物(
src/api/**)の手編集 - 環境変数を
src/config/env.tsにだけ追加して.env.exampleを忘れる - マジックナンバーの直書き(定数に切り出す)
- 深いネスト(早期リターンで対応する)