はじめに
こんにちは、いきなり暑くなって辛いです。木暮です。 普段、業務の中でTypeScriptのコードレビューをしていて依存関係の違反が気になってしまいがちです。 AIにコード生成させても出力結果はまちまちですし、これを一つずつレビューするのも骨が折れます。 ディレクトリを切ってコーディングルール化しても自由にimportできてしまうので強制力はありません。 なので何か仕組みで改善できないのかClaudeに聞いてみました。 すると静的解析でガードできるとのことでした。ライブラリの名前はdependency-cruiserです。 今回はこのライブラリを使うことによってどのように依存関係を強制できるのか確認してみました。
npmパッケージ
https://www.npmjs.com/package/dependency-cruiser
github github.com
サンプルプロジェクトの用意
以下のディレクトリ構成で4層構成のサンプルプロジェクトを作ります。
dependency-cruiser-work/ ├── src/ │ ├── domain/ │ │ ├── entity/ │ │ │ └── Todo.ts │ │ ├── repository/ │ │ │ └── TodoRepository.ts │ │ └── error/ │ │ └── TodoNotFoundError.ts │ │ │ ├── usecase/ │ │ ├── CreateTodo.ts │ │ ├── CompleteTodo.ts │ │ └── ListTodos.ts │ │ │ ├── adapter/ │ │ ├── controller/ │ │ │ └── TodoController.ts │ │ ├── presenter/ │ │ │ └── TodoPresenter.ts │ │ └── gateway/ │ │ └── PrismaTodoRepository.ts │ │ │ ├── infrastructure/ │ │ ├── db/ │ │ │ └── prismaClient.ts │ │ ├── http/ │ │ │ └── server.ts │ │ └── config/ │ │ └── env.ts │ │ │ └── main.ts │ ├── test/ │ └── usecase/ │ └── CreateTodo.test.ts │ ├── tsconfig.json └── package.json
依存関係はこのようになります。dependency-cruiserを使ってこの図をそのままルール化していきます。

dependency-cruiserインストールとセットアップ
ドキュメントを確認してインストールを行います。
各パッケージ管理ツールを使用してインストールができます
npm install --save-dev dependency-cruiser # or yarn add -D dependency-cruiser pnpm add -D dependency-cruiser
今回はnpmでインストール
$ npm install --save-dev dependency-cruiser npm WARN EBADENGINE Unsupported engine { npm WARN EBADENGINE package: 'dependency-cruiser@18.1.0', npm WARN EBADENGINE required: { node: '^22||^24||>=26' }, npm WARN EBADENGINE current: { node: 'v20.12.2', npm: '10.5.0' } npm WARN EBADENGINE } added 43 packages, and audited 52 packages in 1s 12 packages are looking for funding run `npm fund` for details found 0 vulnerabilities
次にコンフィグファイルを生成します。
npx depcruise --init
nodeのバージョンが古くて動きませんでした。 インストール時にワーニングで出ていた内容ですね。
$ npx depcruise --init ERROR: Your node version (20.12.2) is not supported. dependency-cruiser follows the node.js release cycle and runs on these node versions: ^22||^24||>=26 See https://nodejs.org/en/about/releases/ for details.
バージョンを上げて改めて生成
$ npx depcruise --init ✔ Where do your source files live? … src ✔ Do your test files live in a separate folder? … yes ✔ Where do your test files live? … test ✔ Looks like you're using a 'tsconfig.json'. Use that? … yes ✔ Full path to your 'tsconfig.json › tsconfig.json ✔ Do you want to detect process.getBuiltinModule imports (slightly slower)? … no ✔ Also regard TypeScript dependencies that exist only before compilation? … yes ‼ TypeScript compiler not found TypeScript features are used but the TypeScript compiler isn't installed. Install it to ensure dependency-cruiser cruises TypeScript sources e.g. by running npm i -D typescript ✔ Successfully created '.dependency-cruiser.js'
各質問に答えてコンフィグファイルの生成に成功しました。
質問を翻訳するとこんな感じです。
✔ ソースファイルはどこにありますか? … src ✔ テストファイルは別のフォルダにありますか? … はい ✔ テストファイルはどこにありますか? … test ✔ 「tsconfig.json」を使用しているようですね。これを使用しますか? … はい ✔ 「tsconfig.json」へのフルパスは? … tsconfig.json ✔ process.getBuiltinModule によるインポートを検出しますか(処理が若干遅くなります)? … いいえ ✔ コンパイル前にのみ存在する TypeScript の依存関係も考慮しますか? … はい
process.getBuiltinModule は fs や path といったNode.js組み込みモジュールを関数呼び出しで取得するAPIで、今回は使用しないためno にしました。
その次の質問はdependency-cruiserは既定ではコンパイル後に残る依存だけを見るため、import type のような型だけのimportを無視します。層の分離は型レベルでも守りたいので、ここは yes にしておきます。
実行結果を確認すると
‼ TypeScript compiler not found
TypeScript features are used but the TypeScript compiler isn't installed.
Install it to ensure dependency-cruiser cruises TypeScript sources e.g. by running
npm i -D typescript
と書かれていました。
前提としてTypeScriptはインストール済みなのですがなぜ認識されていないのかnpx depcruise --infoコマンドで確認しました。
出力内容を確認するとTypeScriptのバージョンが2から6の間である必要がありました。
$ npx depcruise --info dependency-cruiser@18.1.0 node version supported : ^22||^24||>=26 node version found : v24.15.0 os version found : arm64 darwin@25.5.0 If you need a supported, but not enabled transpiler ('x' below), just install it in the same folder dependency-cruiser is installed. E.g. 'npm i livescript' will enable livescript support if it's installed in your project folder. ✔ transpiler versions supported version found - ---------------------- ------------------- ------------------------ ✔ javascript * acorn@8.17.0 x babel >=7.0.0 <8.0.0 - x coffee-script >=1.0.0 <2.0.0 - x coffeescript >=1.0.0 <3.0.0 - x livescript >=1.0.0 <2.0.0 - x svelte >=3.0.0 <6.0.0 - x swc >=1.0.0 <2.0.0 - x typescript >=2.0.0 <7.0.0 - x vue-template-compiler >=2.0.0 <3.0.0 - x @vue/compiler-sfc >=3.0.0 <4.0.0 - ✔ extension - --------- ✔ .js ✔ .cjs ✔ .mjs ✔ .jsx x .ts x .tsx x .d.ts x .cts x .d.cts x .mts x .d.mts x .vue x .svelte x .ls x .coffee x .litcoffee x .coffee.md x .csx x .cjsx
今インストールされているTypeScriptは7.0.2でした。なのでバージョン7未満の最新版をインストールしました。
$ npm ls typescript dependency-cruiser-work@1.0.0 /dependency-cruiser-work └── typescript@7.0.2 $ npm i -D "typescript@<7" removed 1 package, changed 1 package, and audited 51 packages in 478ms 12 packages are looking for funding run `npm fund` for details found 0 vulnerabilities
改めて確認すると無事認識されました。
$ npx depcruise --info
dependency-cruiser@18.1.0
node version supported : ^22||^24||>=26
node version found : v24.15.0
os version found : arm64 darwin@25.5.0
If you need a supported, but not enabled transpiler ('x' below), just install
it in the same folder dependency-cruiser is installed. E.g. 'npm i livescript'
will enable livescript support if it's installed in your project folder.
✔ transpiler versions supported version found
- ---------------------- ------------------- ------------------------
✔ javascript * acorn@8.17.0
x babel >=7.0.0 <8.0.0 -
x coffee-script >=1.0.0 <2.0.0 -
x coffeescript >=1.0.0 <3.0.0 -
x livescript >=1.0.0 <2.0.0 -
x svelte >=3.0.0 <6.0.0 -
x swc >=1.0.0 <2.0.0 -
✔ typescript >=2.0.0 <7.0.0 typescript@6.0.3
x vue-template-compiler >=2.0.0 <3.0.0 -
x @vue/compiler-sfc >=3.0.0 <4.0.0 -
✔ extension
- ---------
✔ .js
✔ .cjs
✔ .mjs
✔ .jsx
✔ .ts
✔ .tsx
✔ .d.ts
✔ .cts
✔ .d.cts
✔ .mts
✔ .d.mts
x .vue
x .svelte
x .ls
x .coffee
x .litcoffee
x .coffee.md
x .csx
x .cjsx
依存関係のルール設定
セットアップは終わったのでルールを設定していきます。 ルールは.dependency-cruiser.jsに記載します。 ルールの書き方は以下のドキュメントに書かれています。
許容しない依存関係の追加を行ってみます。 forbiddenの配列の中に追加します。
forbidden: [ { name: 'ドメイン層から外側の層への依存', severity: 'error', comment: 'ドメイン層から他の層に属するクラスを参照することはできません', from: { path: '^src/domain' }, to: { path: '^src/(usecase|adapter|infrastructure)' } } ]
設定を保存し、静的解析を実行してみます
$ npx depcruise src ✔ no dependency violations found (13 modules, 23 dependencies cruised)
依存関係の違反は見つからずに実行完了しました。 試しにdomain層のクラスにインフラ層のクラスをインポートして実行してみました。
src/domain/entity/Todo.ts
import { prismaClient } from '../../infrastructure/db/prismaClient'; export class Todo { constructor( readonly id: string, readonly title: string, readonly done: boolean = false, ) { } complete(): Todo { console.log(`Todo.complete: ${this.id}`); return new Todo(this.id, this.title, true); } }
$ npx depcruise src error ドメイン層から外側の層への依存: src/domain/entity/Todo.ts → src/infrastructure/db/prismaClient.ts x 1 dependency violations (1 errors, 0 warnings). 13 modules, 24 dependencies cruised.
静的解析を実行すると違反を検知できました。 また出力内容を確認すると23 dependencies → 24 dependenciesに増えていて増えた依存が違反していることがわかります。
ちなみに実行コマンドにオプションを追加するとルールに設定したコメントも表示することができます。
$ npx depcruise src --output-type err-long error ドメイン層から外側の層への依存: src/domain/entity/Todo.ts → src/infrastructure/db/prismaClient.ts ドメイン層から他の層に属するクラスを参照することはできません x 1 dependency violations (1 errors, 0 warnings). 13 modules, 24 dependencies cruised.
しっかりとルールが効いていることが確認できたので先ほどの違反していた依存関係は元の戻し、冒頭の依存関係を表した図にそって、他のルールも追加します。
forbidden: [ { name: 'ドメイン層から外側の層への依存', severity: 'error', comment: 'ドメイン層から他の層に属するクラスを参照することはできません', from: { path: '^src/domain' }, to: { path: '^src/(usecase|adapter|infrastructure)' } }, { name: 'ユースケース層から外側の層への依存', severity: 'error', comment: 'ユースケース層はドメイン層にのみ依存できます。' + 'DBなどの具体的な実装が必要な場合は、ドメイン層のインターフェース経由で扱ってください', from: { path: '^src/usecase' }, to: { path: '^src/(adapter|infrastructure)' } }, { name: 'アダプター層からインフラ層への依存', severity: 'error', comment: 'アダプター層はインフラ層の実装を直接参照できません。' + '必要なものはコンストラクタなどで外部から渡してください', from: { path: '^src/adapter' }, to: { path: '^src/infrastructure' } } ]
設定を保存し、静的解析を実行してみます
$ npx depcruise src ✔ no dependency violations found (13 modules, 23 dependencies cruised)
依存関係の違反は見つからずに実行完了しました。
forbiddenと同階層に設定できるキーは他にもあります。簡単に説明します。
| キー | 説明 |
|---|---|
forbidden |
ブラックリストでの依存関係の制限 |
allowed |
ホワイトリストでの依存関係の制限 |
allowedSeverity |
allowed で違反があった場合のエラーレベルの設定 |
required |
必ず依存しなければいけないファイルの定義 |
extends |
他の設定ファイルを継承するための設定 |
options |
解析全体の挙動を設定 |
プロジェクトに途中から導入する場合
次にプロジェクトに対して途中からこのライブラリを導入したい場合の方法を確認します。 ルールを追加しても依存関係違反が大量に出てしまって使えない!となってしまうことを避けたいです。 依存関係の違反を検知できる状態にして以下のように手順で簡単に導入することが可能でした。
# 現時点での依存関係違反を記録 $ npx depcruise-baseline src # 記録されている依存関係違反を無視して解析 $ npx depcruise src --ignore-known ✔ no dependency violations found (13 modules, 24 dependencies cruised) ‼ 1 known violations ignored. Run with --no-ignore-known to see them. # 依存関係違反を解消して再度実行 $ npx depcruise src --ignore-known ✔ no dependency violations found (13 modules, 23 dependencies cruised)
依存関係違反は.dependency-cruiser-known-violations.jsonに出力されています。
$ cat .dependency-cruiser-known-violations.json [ { "type": "dependency", "from": "src/domain/entity/Todo.ts", "to": "src/infrastructure/db/prismaClient.ts", "unresolvedTo": "../../infrastructure/db/prismaClient", "dependencyTypes": [ "local", "import" ], "rule": { "severity": "error", "name": "ドメイン層から外側の層への依存" } } ]
視覚的に依存関係を確認
依存関係をdot言語で出力できるようです。これを使用すれば視覚的に確認ができます。
# アーキテクチャの確認 $ npx depcruise src --output-type archi strict digraph "dependency-cruiser output"{ rankdir="LR" splines="true" overlap="false" nodesep="0.16" ranksep="0.18" fontname="Helvetica-bold" fontsize="9" style="rounded,bold,filled" fillcolor="#ffffff" compound="true" node [shape="box" style="rounded, filled" height="0.2" color="black" fillcolor="#ffffcc" fontcolor="black" fontname="Helvetica" fontsize="9"] edge [arrowhead="normal" arrowsize="0.6" penwidth="2.0" color="#00000033" fontname="Helvetica" fontsize="9"] subgraph "cluster_src" {label="src" "src/adapter" [label=<adapter> tooltip="adapter" URL="src/adapter" shape="box3d"] } "src/adapter" -> "src/usecase" [arrowhead="onormal" penwidth="1.0"] "src/adapter" -> "src/domain" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" "src/domain" [label=<domain> tooltip="domain" URL="src/domain" shape="box3d"] } subgraph "cluster_src" {label="src" "src/infrastructure" [label=<infrastructure> tooltip="infrastructure" URL="src/infrastructure" shape="box3d"] } "src/infrastructure" -> "src/adapter" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" "src/main.ts" [label=<main.ts> tooltip="main.ts" URL="src/main.ts" fillcolor="#ddfeff"] } "src/main.ts" -> "src/adapter" "src/main.ts" -> "src/infrastructure" "src/main.ts" -> "src/usecase" subgraph "cluster_src" {label="src" "src/usecase" [label=<usecase> tooltip="usecase" URL="src/usecase" shape="box3d"] } "src/usecase" -> "src/domain" }
dot言語はライブラリをインストールするかオンラインツールで画像に変換して確認できます。 今回はオンラインツールで出力された内容を貼り付けて確認してみました。

想定した依存関係が図で確認できました。 正しく生成できているかmain.tsを確認してみます。
// src/main.ts import { TodoController } from './adapter/controller/TodoController'; import { PrismaTodoRepository } from './adapter/gateway/PrismaTodoRepository'; import { TodoPresenter } from './adapter/presenter/TodoPresenter'; import { prismaClient } from './infrastructure/db/prismaClient'; import { startServer } from './infrastructure/http/server'; import { CompleteTodo } from './usecase/CompleteTodo'; import { CreateTodo } from './usecase/CreateTodo'; import { ListTodos } from './usecase/ListTodos'; const repository = new PrismaTodoRepository(prismaClient); const createTodo = new CreateTodo(repository); const completeTodo = new CompleteTodo(repository); const listTodos = new ListTodos(repository); console.log('wired:', completeTodo.constructor.name); startServer(new TodoController(createTodo, listTodos, new TodoPresenter()));
各ファイルごとの依存関係も確認できます。
# 各ファイルの依存関係を確認 $ npx depcruise src --output-type dot strict digraph "dependency-cruiser output"{ rankdir="LR" splines="true" overlap="false" nodesep="0.16" ranksep="0.18" fontname="Helvetica-bold" fontsize="9" style="rounded,bold,filled" fillcolor="#ffffff" compound="true" node [shape="box" style="rounded, filled" height="0.2" color="black" fillcolor="#ffffcc" fontcolor="black" fontname="Helvetica" fontsize="9"] edge [arrowhead="normal" arrowsize="0.6" penwidth="2.0" color="#00000033" fontname="Helvetica" fontsize="9"] subgraph "cluster_src" {label="src" subgraph "cluster_src/adapter" {label="adapter" subgraph "cluster_src/adapter/controller" {label="controller" "src/adapter/controller/TodoController.ts" [label=<TodoController.ts> tooltip="TodoController.ts" URL="src/adapter/controller/TodoController.ts" fillcolor="#ddfeff"] } } } "src/adapter/controller/TodoController.ts" -> "src/usecase/CreateTodo.ts" [arrowhead="onormal" penwidth="1.0"] "src/adapter/controller/TodoController.ts" -> "src/usecase/ListTodos.ts" [arrowhead="onormal" penwidth="1.0"] "src/adapter/controller/TodoController.ts" -> "src/adapter/presenter/TodoPresenter.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/adapter" {label="adapter" subgraph "cluster_src/adapter/gateway" {label="gateway" "src/adapter/gateway/PrismaTodoRepository.ts" [label=<PrismaTodoRepository.ts> tooltip="PrismaTodoRepository.ts" URL="src/adapter/gateway/PrismaTodoRepository.ts" fillcolor="#ddfeff"] } } } "src/adapter/gateway/PrismaTodoRepository.ts" -> "src/domain/entity/Todo.ts" "src/adapter/gateway/PrismaTodoRepository.ts" -> "src/domain/repository/TodoRepository.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/adapter" {label="adapter" subgraph "cluster_src/adapter/presenter" {label="presenter" "src/adapter/presenter/TodoPresenter.ts" [label=<TodoPresenter.ts> tooltip="TodoPresenter.ts" URL="src/adapter/presenter/TodoPresenter.ts" fillcolor="#ddfeff"] } } } "src/adapter/presenter/TodoPresenter.ts" -> "src/domain/entity/Todo.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/domain" {label="domain" subgraph "cluster_src/domain/entity" {label="entity" "src/domain/entity/Todo.ts" [label=<Todo.ts> tooltip="Todo.ts" URL="src/domain/entity/Todo.ts" fillcolor="#ddfeff"] } } } subgraph "cluster_src" {label="src" subgraph "cluster_src/domain" {label="domain" subgraph "cluster_src/domain/error" {label="error" "src/domain/error/TodoNotFoundError.ts" [label=<TodoNotFoundError.ts> tooltip="TodoNotFoundError.ts" URL="src/domain/error/TodoNotFoundError.ts" fillcolor="#ddfeff"] } } } subgraph "cluster_src" {label="src" subgraph "cluster_src/domain" {label="domain" subgraph "cluster_src/domain/repository" {label="repository" "src/domain/repository/TodoRepository.ts" [label=<TodoRepository.ts> tooltip="TodoRepository.ts" URL="src/domain/repository/TodoRepository.ts" fillcolor="#ddfeff"] } } } "src/domain/repository/TodoRepository.ts" -> "src/domain/entity/Todo.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/infrastructure" {label="infrastructure" subgraph "cluster_src/infrastructure/config" {label="config" "src/infrastructure/config/env.ts" [label=<env.ts> tooltip="env.ts" URL="src/infrastructure/config/env.ts" fillcolor="#ddfeff"] } } } subgraph "cluster_src" {label="src" subgraph "cluster_src/infrastructure" {label="infrastructure" subgraph "cluster_src/infrastructure/db" {label="db" "src/infrastructure/db/prismaClient.ts" [label=<prismaClient.ts> tooltip="prismaClient.ts" URL="src/infrastructure/db/prismaClient.ts" fillcolor="#ddfeff"] } } } subgraph "cluster_src" {label="src" subgraph "cluster_src/infrastructure" {label="infrastructure" subgraph "cluster_src/infrastructure/http" {label="http" "src/infrastructure/http/server.ts" [label=<server.ts> tooltip="server.ts" URL="src/infrastructure/http/server.ts" fillcolor="#ddfeff"] } } } "src/infrastructure/http/server.ts" -> "src/adapter/controller/TodoController.ts" [arrowhead="onormal" penwidth="1.0"] "src/infrastructure/http/server.ts" -> "src/infrastructure/config/env.ts" subgraph "cluster_src" {label="src" "src/main.ts" [label=<main.ts> tooltip="main.ts" URL="src/main.ts" fillcolor="#ddfeff"] } "src/main.ts" -> "src/adapter/controller/TodoController.ts" "src/main.ts" -> "src/adapter/gateway/PrismaTodoRepository.ts" "src/main.ts" -> "src/adapter/presenter/TodoPresenter.ts" "src/main.ts" -> "src/infrastructure/db/prismaClient.ts" "src/main.ts" -> "src/infrastructure/http/server.ts" "src/main.ts" -> "src/usecase/CompleteTodo.ts" "src/main.ts" -> "src/usecase/CreateTodo.ts" "src/main.ts" -> "src/usecase/ListTodos.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/usecase" {label="usecase" "src/usecase/CompleteTodo.ts" [label=<CompleteTodo.ts> tooltip="CompleteTodo.ts" URL="src/usecase/CompleteTodo.ts" fillcolor="#ddfeff"] } } "src/usecase/CompleteTodo.ts" -> "src/domain/error/TodoNotFoundError.ts" "src/usecase/CompleteTodo.ts" -> "src/domain/repository/TodoRepository.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/usecase" {label="usecase" "src/usecase/CreateTodo.ts" [label=<CreateTodo.ts> tooltip="CreateTodo.ts" URL="src/usecase/CreateTodo.ts" fillcolor="#ddfeff"] } } "src/usecase/CreateTodo.ts" -> "src/domain/entity/Todo.ts" "src/usecase/CreateTodo.ts" -> "src/domain/repository/TodoRepository.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/usecase" {label="usecase" "src/usecase/ListTodos.ts" [label=<ListTodos.ts> tooltip="ListTodos.ts" URL="src/usecase/ListTodos.ts" fillcolor="#ddfeff"] } } "src/usecase/ListTodos.ts" -> "src/domain/entity/Todo.ts" [arrowhead="onormal" penwidth="1.0"] "src/usecase/ListTodos.ts" -> "src/domain/repository/TodoRepository.ts" [arrowhead="onormal" penwidth="1.0"] }

白抜きの矢印は 以下のコードのようにimport type による型だけの依存です。設定生成時に「コンパイル前にのみ存在する依存も考慮しますか」を yes にしたため、これらもグラフに現れています。
// src/infrastructure/http/server.ts import type { TodoController } from '../../adapter/controller/TodoController'; import { env } from '../config/env'; export function startServer(controller: TodoController): void { console.log(`startServer: listening on ${env.port}`); void controller.get(); }
他にもオプションがありますが今回は、手元で確認できていないので割愛させていただきます。
さいごに
一通り触った印象としてはルール設定を行う部分は時間をかける必要がありそうですが途中から導入する分にも問題なく使えそうで タイミングを見て導入してみたいと思いました。あとはこれをCIやpre-commitに組み込めれば仕組み化できそうですね
ここまで読んでくださりありがとうございます!
弊社ではエンジニアを募集しております!少しでもご興味がありましたら、カジュアル面談でお話ししましょう!