この記事は約 5 分で読めます。
筆者プロフィール: ソフトウェアエンジニア。「知った気にならない。いつまでも学び続ける」を信条に、業務と個人開発の両輪で技術を磨いています。AI 駆動開発で複数の個人開発アプリを構築・運用中。
👉 ポートフォリオ: 筆者ホームページ
「同じ定数を 3 箇所にハードコードして、変更時に 1 箇所漏れて事故」 — これを構造的に防ぐ src/config/ 集約パターン を整理します。運用中の SaaS 「たすきば Knowledge Relay」 で 6 ヶ月運用して、変更時の grep が常に 1 箇所で済む状態を保っています。
サービスの機能紹介・画面イメージ・コンセプトは公式プロダクトページをご覧ください。
👉 たすきば Knowledge Relay — 公式プロダクトページ
何を src/config/ に入れるか
src/config/
├── master-data.ts # プロジェクト状態 / リスク種別 / 重要度 enum
├── security.ts # bcrypt cost / ログイン失敗ロック回数 / セッション有効期限
├── validation.ts # 文字数上限 / 入力長制約
├── app-routes.ts # 画面遷移パス
├── theme-definitions.ts # テーマ色定義
├── billing.ts # プラン定数 / 課金単価 / 上限値
└── ...
ポイントは 「業務的意味を持つ」 という基準。
| 種別 | 配置先 |
|---|---|
| 技術的な定数 (タイムアウト、リトライ回数) |
src/lib/ 配下のヘルパ内 |
| 業務的意味のある定数 (プロジェクト状態、Beginner プラン月 50 回上限) | src/config/ |
1. なぜハードコードを許さないのか
実装中、よくこういう状況が起きます。
// src/app/(dashboard)/projects/page.tsx
<Input maxLength={200} placeholder="プロジェクト名" />
// src/services/project.service.ts
const projectSchema = z.object({
name: z.string().max(200), // 文字数を別ファイルで定義
});
// src/components/forms/ProjectForm.tsx
<span>{name.length} / 200 文字</span> // また別の場所で 200
「プロジェクト名の文字数上限を 200 → 300 に変えたい」という要望が来ると、上記 3 箇所をすべて修正する必要があります。1 つでも漏らすと、UI と API でバリデーション結果が食い違う。
src/config/validation.ts に集約すれば:
// src/config/validation.ts
/**
* プロジェクト名の最大文字数。
* UI と Zod schema の両方からこの定数を参照する。
*
* @business 業務観点での意味: プロジェクト一覧で 1 行に収まる長さの上限。
*/
export const PROJECT_NAME_MAX_LENGTH = 200;
1 箇所変更で全部反映 されます。
2. JSDoc で「なぜこの値か」を残す
src/config/ の定数には、必ず JSDoc を付けます。
// src/config/security.ts
/**
* bcrypt のコストパラメータ。
*
* 12 を採用している理由:
* - 10 はやや弱い (現在の GPU 攻撃を考えると 12 推奨)
* - 14 は MFA 検証時に体感遅延が出る (200ms 超)
* - 12 は OWASP Password Storage Cheat Sheet (2026 年版) で推奨
*
* @see https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
*/
export const BCRYPT_COST = 12;
JSDoc がないと「なんで 12 なんだっけ?」が再起します。KDD として書いておくと、半年後の自分が助かる。
3. Client Component → service の value import 問題
ここでひとつ落とし穴があります。
Next.js App Router で Client Component から service の定数を import する と、Prisma が client bundle に混入して build が失敗します。
// ✗ NG: client component から service を import
'use client';
import { PROJECT_NAME_MAX_LENGTH } from '@/services/project.service';
project.service.ts は内部で prisma を import しているため、bundle 解析がそれをたどって client にも Prisma を入れようとする。結果、Webpack で巨大なエラー。
これを防ぐため、src/config/ は prisma を一切 import しない純粋な定数モジュール として保ちます。
// ✓ OK: config からだけ import する
'use client';
import { PROJECT_NAME_MAX_LENGTH } from '@/config/validation';
ルールはシンプル:
src/config/配下の.tsはprisma/auth/ DB 関連を一切 import しない。
4. 課金関連定数は特に厳重に
課金関連の定数 (プラン上限・単価) は、変更時に grep を 4 軸 (生値 / 表示文字列 / 自然文 / アサーション) で打つ必要 があります。
| 軸 | 例 |
|---|---|
| 生値 | BEGINNER_SEAT_LIMIT = 5 |
| 表示文字列 |
'5,000円' (toLocaleString) |
| 自然文 | 「5 席まで無料」(ドキュメント) |
| アサーション |
toContainText('5 席') (E2E) |
対策として、たすきばは:
1. 価格表示の生値を 1 つの定数にまとめる
2. その定数を JSDoc で「変更時はこれらを必ず一緒に更新」と注記
3. テストファイルも from '@/config/billing' で参照させる
5. 「業務的意味を持つ」の境界線
判断に迷うことがある:
- これは技術的な定数か、業務的な定数か?
- これは
src/lib/に入れるべきか、src/config/に入れるべきか?
ルール:
ユーザに見える概念に紐づく定数は
src/config/
具体例:
| 定数 | 場所 | 理由 |
|---|---|---|
| プロジェクト状態の enum | src/config/master-data.ts |
ユーザに見える業務概念 |
| プロジェクト名の最大文字数 | src/config/validation.ts |
UI に表示される制約 |
| bcrypt cost | src/config/security.ts |
業務観点でのセキュリティ判断 |
| HTTP リトライ回数 | src/lib/http.ts |
ユーザに見えない技術詳細 |
| データベース接続プールサイズ | src/lib/db.ts |
ユーザに見えない技術詳細 |
| Voyage embedding の次元数 | src/config/master-data.ts |
業務観点で「精度設計」に直結 |
おわりに — 3 つのルール
ハードコードを物理的に消すための 3 つのルール:
1. 業務的意味を持つ定数は src/config/ に集約する
2. JSDoc で「なぜこの値か」を残す
3. src/config/ は Prisma / auth を import しない
たすきばはこのルールを 6 ヶ月厳守してきた結果、変更時の grep が常に 1 箇所で済む 状態になっています。
個人開発でも、商用 SaaS と同じ品質を出すための、地味だが効く設計判断 です。
本記事の設計は、運用中の SaaS 「たすきば Knowledge Relay」 で実装しています。
👉 たすきば Knowledge Relay — 公式プロダクトページ