概要
モジュールとは「機能をまとめる箱」ではない。
それは責務・依存・拡張性・再利用性を保証する、構造的な単位である。
JavaScriptでのモジュール設計は、その柔軟さから無秩序な依存・結合・循環を生みやすい。
それを制御し、壊れない構造・テスト可能な分離・変更に強い境界を設計することが不可欠になる。
本稿では、依存構造を制御するためのレイヤー分離、境界戦略、循環参照の防止技術を解説する。
1. モジュール構造は「責務」で分ける
src/
├─ features/
│ ├─ user/
│ │ ├─ index.ts
│ │ ├─ service.ts
│ │ └─ validator.ts
│ └─ auth/
│ ├─ login.ts
│ └─ session.ts
├─ libs/
│ ├─ api.ts
│ └─ storage.ts
- ✅ 機能単位(Feature-Based) で分離
- ✅ 機能間を直接依存させず、共通ライブラリ越しに橋渡す
2. レイヤー設計と依存方向の一方向化
UI → Application → Domain → Infrastructure
- ✅ 上層から下層へは依存可能
- ❌ 下層から上層への逆依存は禁止
- ✅ 依存の方向性は「読みやすく」「テストしやすく」「循環を防ぐ」
3. 循環参照の検出と防止
✅ 循環参照の症状
-
undefined
が返ってくる -
Cannot read property
エラー - 静的解析エラー(ツールによっては)
✅ 対策
- 明確な依存境界を引く(依存のハブを作らない)
- index.ts の双方向import禁止
-
depcheck
,madge
,eslint-plugin-import
などで検出
4. 共通ロジックは “低層に抽象化して” 橋渡す
features/
├─ user/
│ └─ useUser.ts → domain/user.ts + libs/api.ts を使う
- ✅ 共通処理(ログ、バリデーション、変換)は libs 層で抽象化
- ✅ features から直接他の features に依存しない
5. DI(依存注入)と Adapter層の活用
// domain/use-case.ts
export function createUserService({ userRepo }) {
return {
getUser: () => userRepo.find(),
};
}
- ✅ 依存を明示的に渡す構造で、テスト・差し替えが容易に
- ✅ Adapter層で外部依存(API, DB)とのインターフェースを統一
6. index.ts は「公開窓口」であり、構造を隠す
// features/user/index.ts
export { createUserService } from './service';
- ✅ 公開する機能だけを
index.ts
に定義 - ✅ 内部構造は外部から見えないようにし、意図的に使わせる構造へ
設計判断フロー
① このファイルが他モジュールに依存していないか? → 依存構造を可視化
② 双方向のimportが発生していないか? → 共通化 or 分離構造へ
③ UIとロジックが同一モジュールにないか? → Presentation / Logic の分離
④ 共通ロジックがfeature配下に埋もれていないか? → libs層に抽出
⑤ テスト用モックが依存に組み込めているか? → DI構造へ移行
よくあるミスと対策
❌ features同士でimportし合って依存が複雑化
→ ✅ 共通処理はlibs層に抽象化して橋渡す
❌ index.ts が再帰的にindex.tsを読み込み循環参照
→ ✅ index.tsは1方向の“公開インターフェース”だけに使う
❌ APIやDBを直接サービスで呼び、テスト不能に
→ ✅ use-case + DI構造 + Adapter層で依存を外部化
結語
モジュールとは「コードを分けること」ではない。
それは**“意味・依存・責任を構造化し、破綻を防ぐための戦略単位”**である。
- 機能でモジュールを切り
- 依存は一方向に制御し
- 外部はAdapterに抽象化し
- 公開構造はindexで制御する
JavaScriptにおけるモジュール依存戦略とは、
“複雑さを壊さず拡張可能に保つための、構造的ガードレール”である。