第79章:モジュール境界の実戦:禁止importルール🚧
1) 今日のゴール🎯✨
- 「domain が infra を import しちゃった😵💫」みたいな事故を 自動で検知できる
- PRで境界違反が混ざっても CIで落ちるから安心💪
- チーム開発でも「崩れないDDDの骨格」になる🏰
2) なんで禁止importが必要なの?🥺
境界ルールって、最初はみんな守れるんだよね。 でもだんだんこうなる👇
- 「とりあえず急ぎだから domain から infra 呼ぶか〜」😇
- 「DTO便利だし domain でも使っちゃお」😇
- 「気づいたら循環参照でビルドが謎死」😇
つまり… “守ろうね” じゃ守れないから、仕組みで守るが正解💡
3) まずは境界ルールを1分で確定📝
このロードマップの流れ(第78章)を、そのまま “禁止importルール” にするよ✨
依存(import)の向き🌊
イメージはこれ👇(左ほど内側=強い💎)

infra → app → domain
- domain:誰にも依存しない(最強の核)💎
- app:domain を使う(手順・ユースケース)🎬
- infra:外の世界(DB/HTTP/UIなど)🌍
例外(なんでも見えていい場所)🧩
依存を組み立てる場所(Composition Root)は、だいたい別枠にするのが楽👍
例:src/bootstrap や src/main みたいな場所。
4) 実装①:最短で効く!禁止import(no-restricted-imports)⚡🧹
まず知っておくポイント👀
- ESLintの
no-restricted-importsは「このimport禁止!」を作れるルールだよ🚫 - ただし 静的importに適用で、dynamic import には基本効かないよ(仕様)📌 (ESLint)
- TypeScriptでは、ESLint本体ルールより typescript-eslint版を使うのが安全(型importなど考慮)✅ (TypeScript ESLint)
“簡単版” の設定例(境界を壊すimportを止める)🚧
ESLintは最近の主流が Flat Config(eslint.config.mjs) だよ🧁 (ESLintの最新メジャーでもこの流れが続いてる) (GitHub)
eslint.config.mjs(例)
import js from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
js.configs.recommended,
...tseslint.configs.recommended,
// ✅ TypeScriptではこっちを使う(型import等に対応)
{
files: ["src/**/*.ts"],
rules: {
"no-restricted-imports": "off",
"@typescript-eslint/no-restricted-imports": [
"error",
{
// 🧱 app から infra を触らせない
patterns: [
{
group: ["../infra/*", "../../infra/*", "../../../infra/*"],
message: "🚧 app → infra は禁止だよ!依存の向きを守ってね",
},
],
},
],
},
},
// 🧊 domain は app/infra を触らせない
{
files: ["src/domain/**/*.ts"],
rules: {
"@typescript-eslint/no-restricted-imports": [
"error",
{
patterns: [
{ group: ["../app/*", "../../app/*"], message: "🚧 domain → app は禁止だよ!" },
{ group: ["../infra/*", "../../infra/*"], message: "🚧 domain → infra は禁止だよ!" },
],
},
],
},
},
];
✅ これだけでも「うっかり境界違反」をかなり止められるよ! ただし…相対パスの深さが増えるとパターンがつらい🥲(次で解決✨)
5) 実装②:境界を“ルールとして定義”する(eslint-plugin-boundaries)🏰✨
ここからが本命〜!🥳 相対パスの地獄をやめて、“domain/app/infra” を種類として宣言して守るやつ💎
eslint-plugin-boundariesは「ファイルがどの領域か」を設定して、どことどこがimportしていいかをルール化できるよ📦 (JS Boundaries)- 直近でも更新されてて、最新版リリースが出てるよ🆕 (GitHub)
eslint.config.mjs(境界定義つき・おすすめ)
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import boundaries from "eslint-plugin-boundaries";
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["src/**/*.ts"],
plugins: { boundaries },
settings: {
// 👇 どのフォルダがどの“領域タイプ”か決める
"boundaries/elements": [
{ type: "domain", pattern: "src/domain/**" },
{ type: "app", pattern: "src/app/**" },
{ type: "infra", pattern: "src/infra/**" },
{ type: "shared", pattern: "src/shared/**" },
{ type: "bootstrap", pattern: "src/bootstrap/**" },
],
},
rules: {
// ✅ どの領域がどこを import していいかを宣言する
"boundaries/element-types": [
"error",
{
default: "disallow",
message: "🚧 境界違反だよ! import を見直してね",
rules: [
{ from: "domain", allow: ["domain", "shared"] },
{ from: "app", allow: ["app", "domain", "shared"] },
{ from: "infra", allow: ["infra", "app", "domain", "shared"] },
{ from: "bootstrap", allow: ["bootstrap", "infra", "app", "domain", "shared"] },
{ from: "shared", allow: ["shared"] },
],
},
],
// ついでに:未分類ファイルを許さない(守り固め)🛡️
"boundaries/no-unknown": "error",
},
},
];
これの良さはね… ✅ 相対パスの深さに左右されない ✅ 境界ルールが 文章みたいに読める ✅ “例外(bootstrap)” をちゃんと作れる
最高〜〜〜!🥹✨
6) 実装③:依存グラフで“循環参照”も潰す(dependency-cruiser)🗺️🔁
ESLintは「その場のimport違反」には強いけど、 **循環参照(A→B→C→A)**みたいな “全体の形” は別ツールが便利👍
そこで dependency-cruiser 💣
ざっくり導入イメージ(ルール例)
domainからinfraへ行く依存は禁止🚫- 循環参照は禁止🚫
infraだけが外部(DB/HTTP)に触れてOK🌍
(ここはプロジェクトに合わせて少しずつ育てるのがコツだよ🌱)
7) (おまけ)モノレポならNxの境界ルールも強い🧩💪
もし将来、複数アプリ/複数ライブラリのモノレポになったら、Nxの Enforce Module Boundaries がめちゃ強いよ🔥
タグ(例:type:domain)で依存ルールを宣言して、違反を止められる〜! (npmdiff.dev)
8) よくあるハマりポイント集😂🧯
Q1. domain から便利関数使いたい…でも infra にある🥲
👉 shared を作ってそこへ移動が定番✨ 「どの層でも使える純粋ロジック」だけ置くよ🧊
Q2. “barrel export(index.ts)” で境界をすり抜ける…😵
👉 barrel は便利だけど、境界チェックが弱くなることあるよ💦 最初は やりすぎないのがおすすめ!
Q3. 型だけ import したい!(実体importは禁止したい)🥺
👉 TypeScriptなら @typescript-eslint/no-restricted-imports が、型import構文に対応してるよ✅ (TypeScript ESLint)
さらに、ESLint側でも allowTypeImports が入ってきてる流れがあるよ🧠 (ESLint)
(プロジェクトの方針次第で使い分けると◎)
9) ミニ演習🎮✨(15〜25分)
ミッションA:わざと境界違反してみる😈
src/appのどこかでsrc/infraを import してみる- lint を実行
- ちゃんと怒られたら勝ち🏆🎉
ミッションB:正しい形に直す🛠️
- 「infra で実装したもの」は interface を domain に置く
- app は interface を使う
- 依存の組み立ては bootstrap でやる
DDDの気持ちよさ、ここで出るよ🥹✨
10) AIに頼むと爆速になるプロンプト集🤖💬
コピペで使えるやつ置いとくね〜🫶
- 「このフォルダ構成(domain/app/infra/bootstrap/shared)で、境界の許可import表を作って。例外(bootstrap)も含めて」
- 「このimport違反を直したい。依存の向きを守ったまま、最小変更でリファクタ案を3つ出して」
- 「循環参照が出た。依存グラフを想像して、切るべき依存と移動先(shared/domain/app)を提案して」
11) 仕上げチェックリスト✅💖
- domain が app/infra を import してない
- app が infra を import してない
- 依存の組み立ては bootstrap に寄せた
- lint が CI でも動く(PRで落ちる)
- shared が “便利箱化” してない(純粋ロジックだけ)
次の第80章は、ここで作った抽象(Repository)を「差し替えできて嬉しい〜〜!」って体験する回だよ🔁🎮✨