ESLint でアーキテクチャ境界を守る:層間 import を規制する設計パターン
プロジェクトが大きくなるにつれて、コードの構造を保つことが難しくなっていきますよね。特にレイヤードアーキテクチャやクリーンアーキテクチャを採用している場合、各層の責任を明確に分離することが重要です。しかし、人間がレビューだけで境界を守り続けるのは大変な作業でしょう。
そこで活躍するのが ESLint です。ESLint を使えば、コードを書いた瞬間にアーキテクチャ違反を検出できます。この記事では、ESLint を使って層間の import を規制し、アーキテクチャの境界を自動的に守る方法を解説します。
実際のプロジェクトで使える具体的な設定例も紹介しますので、ぜひ最後までお読みください。
背景
レイヤードアーキテクチャとは
レイヤードアーキテクチャは、システムを複数の層(レイヤー)に分割する設計手法です。各層には明確な責任があり、特定の方向にのみ依存関係を持つことが原則となっています。
典型的な 3 層アーキテクチャでは、以下のような構成になります。
typescript// プロジェクト構造の例
src/
├── presentation/ // プレゼンテーション層(UI、コントローラー)
├── application/ // アプリケーション層(ビジネスロジック)
└── infrastructure/ // インフラストラクチャ層(DB、外部API)
各層の責任と役割を表にまとめました。
| # | 層 | 役割 | 具体例 |
|---|---|---|---|
| 1 | プレゼンテーション層 | ユーザーとのやり取りを担当 | React コンポーネント、API コントローラー |
| 2 | アプリケーション層 | ビジネスロジックの実装 | ユースケース、サービスクラス |
| 3 | インフラストラクチャ層 | 外部システムとの接続 | データベースアクセス、外部 API 呼び出し |
アーキテクチャの依存関係ルール
レイヤードアーキテクチャでは、依存関係の方向が重要です。基本的に、上位層は下位層に依存できますが、下位層が上位層に依存してはいけません。
以下の図で、正しい依存関係の方向を確認しましょう。
mermaidflowchart TD
presentation["プレゼンテーション層<br/>(UI・コントローラー)"]
application["アプリケーション層<br/>(ビジネスロジック)"]
infrastructure["インフラストラクチャ層<br/>(DB・外部API)"]
presentation -->|依存OK| application
application -->|依存OK| infrastructure
infrastructure -.->|依存NG| application
infrastructure -.->|依存NG| presentation
application -.->|依存NG| presentation
この図から分かるように、依存の方向は一方向です。下位層から上位層への依存は禁止されており、これにより各層の独立性が保たれます。
なぜ層間の境界が重要なのか
層間の境界を守ることで、以下のメリットが得られます。
保守性の向上が最も大きな利点でしょう。各層が独立しているため、一つの層を変更しても他の層への影響が最小限に抑えられます。
テストのしやすさも見逃せません。依存関係が明確なので、モックやスタブを使ったテストが書きやすくなりますね。
チーム開発の効率化にも貢献します。各層の責任が明確なので、複数人で並行して開発を進めやすくなるでしょう。
課題
よくあるアーキテクチャ違反のパターン
実際の開発現場では、以下のような違反がよく発生します。これらは、レビューだけでは見逃されやすい問題です。
下位層から上位層への不正な import
インフラストラクチャ層がプレゼンテーション層のコンポーネントを直接 import してしまうケースです。
typescript// infrastructure/database/userRepository.ts
// ❌ NG:インフラ層がプレゼンテーション層に依存
import { UserProfile } from '../../presentation/components/UserProfile';
export class UserRepository {
// ...
}
このコードは、本来依存してはいけない上位層を参照してしまっています。
層をスキップした import
プレゼンテーション層がアプリケーション層を経由せず、直接インフラストラクチャ層にアクセスするパターンです。
typescript// presentation/pages/UserPage.tsx
// ❌ NG:プレゼンテーション層がインフラ層を直接参照
import { UserRepository } from '../../infrastructure/database/userRepository';
export const UserPage = () => {
const repo = new UserRepository();
// ...
};
アプリケーション層を経由すべきところを、直接インフラ層にアクセスしています。
循環依存の発生
二つの層が互いに依存し合ってしまうケースです。
typescript// application/services/userService.ts
import { UserController } from '../../presentation/controllers/userController';
// presentation/controllers/userController.ts
import { UserService } from '../../application/services/userService';
このような循環依存は、ビルドエラーやランタイムエラーの原因になります。
人的レビューの限界
これらの問題を人的レビューだけで防ぐのは困難です。以下のような課題があるでしょう。
| # | 課題 | 詳細 |
|---|---|---|
| 1 | レビュー負荷の増加 | すべての import 文を目視確認するのは時間がかかる |
| 2 | 見落としのリスク | 複雑な import パスは見落としやすい |
| 3 | プロジェクト参加時の学習コスト | 新メンバーがルールを理解するまで時間がかかる |
| 4 | リファクタリング時の事故 | 大規模なリファクタリング時に違反を混入させやすい |
以下の図で、違反が発生するフローを確認しましょう。
mermaidflowchart LR
dev["開発者"] -->|コード記述| code["コード"]
code -->|commit| review["レビュー"]
review -->|見落とし| merge["マージ"]
merge -->|違反混入| main["main ブランチ"]
review -.->|発見| fix["修正依頼"]
fix -.-> dev
人的レビューでは、どうしても見落としが発生してしまいます。自動化された仕組みが必要ですね。
解決策
ESLint による自動検出の仕組み
ESLint を使えば、コードを書いた瞬間にアーキテクチャ違反を検出できます。以下のような仕組みで動作します。
mermaidflowchart LR
dev["開発者"] -->|コード記述| editor["エディタ"]
editor -->|保存| eslint["ESLint 実行"]
eslint -->|ルール検証| result{違反あり?}
result -->|Yes| error["エラー表示"]
result -->|No| ok["OK"]
error --> dev
ok --> commit["コミット可能"]
このフローにより、問題を早期に発見できます。
ESLint プラグインの選択肢
層間 import を規制するための ESLint プラグインには、主に以下の 3 つの選択肢があります。
| # | プラグイン | 特徴 | 推奨用途 |
|---|---|---|---|
| 1 | no-restricted-imports | ESLint 標準ルール | シンプルな規制 |
| 2 | eslint-plugin-boundaries | 境界定義に特化 | 複雑なアーキテクチャ |
| 3 | eslint-plugin-import | import 文全般を管理 | 総合的な import 管理 |
それぞれの特徴を詳しく見ていきましょう。
方法 1:no-restricted-imports を使う
no-restricted-imports は ESLint の標準ルールで、特定のパスからの import を禁止できます。
基本的な設定
.eslintrc.js に以下のように記述します。
javascript// .eslintrc.js
module.exports = {
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
// 設定をここに記述
],
},
],
},
};
この基本設定に、具体的な禁止パターンを追加していきます。
層ごとの規制ルール設定
各層に対して、import してはいけないパスをパターンで指定しましょう。
javascript// .eslintrc.js
module.exports = {
overrides: [
{
// インフラ層の規制
files: ['src/infrastructure/**/*'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/presentation/**'],
message:
'インフラ層からプレゼンテーション層への import は禁止です',
},
{
group: ['**/application/**'],
message:
'インフラ層からアプリケーション層への import は禁止です',
},
],
},
],
},
},
],
};
overrides を使うことで、特定のディレクトリにのみルールを適用できます。
アプリケーション層の規制
アプリケーション層がプレゼンテーション層に依存しないようにします。
javascript// .eslintrc.js(続き)
{
// アプリケーション層の規制
files: ['src/application/**/*'],
rules: {
'no-restricted-imports': ['error', {
patterns: [
{
group: ['**/presentation/**'],
message: 'アプリケーション層からプレゼンテーション層への import は禁止です'
}
]
}]
}
}
これにより、ビジネスロジックが UI に依存することを防げます。
方法 2:eslint-plugin-boundaries を使う
より柔軟で強力な境界管理には、eslint-plugin-boundaries が適しています。
インストール
まず、パッケージをインストールしましょう。
bashyarn add -D eslint-plugin-boundaries
依存関係が追加されます。
基本設定
.eslintrc.js でプラグインを有効にします。
javascript// .eslintrc.js
module.exports = {
plugins: ['boundaries'],
extends: ['plugin:boundaries/recommended'],
settings: {
'boundaries/elements': [
// エレメントの定義をここに記述
],
},
};
この設定が基盤となります。
エレメント(層)の定義
各層をエレメントとして定義します。
javascript// .eslintrc.js
settings: {
'boundaries/elements': [
{
type: 'presentation',
pattern: 'src/presentation/**/*',
mode: 'folder',
capture: ['component']
},
{
type: 'application',
pattern: 'src/application/**/*',
mode: 'folder',
capture: ['service']
},
{
type: 'infrastructure',
pattern: 'src/infrastructure/**/*',
mode: 'folder',
capture: ['repository']
}
]
}
pattern でファイルパスを指定し、type で層の種類を定義しています。
依存関係ルールの設定
定義したエレメント間の依存関係を制御します。
javascript// .eslintrc.js
settings: {
'boundaries/elements': [
// 前述のエレメント定義
],
'boundaries/ignore': ['**/*.test.ts', '**/*.spec.ts']
},
rules: {
'boundaries/element-types': ['error', {
default: 'disallow',
rules: [
// 許可する依存関係のみを列挙
]
}]
}
default: 'disallow' により、明示的に許可したもの以外は禁止されます。
許可する依存関係の定義
具体的に許可する依存関係を指定しましょう。
javascript// .eslintrc.js(続き)
rules: [
{
from: 'presentation',
allow: ['application'],
},
{
from: 'application',
allow: ['infrastructure'],
},
{
from: 'infrastructure',
allow: [], // どこにも依存しない
},
];
これにより、一方向の依存関係のみが許可されます。
方法 3:eslint-plugin-import との組み合わせ
eslint-plugin-import を併用すると、さらに細かい制御が可能です。
インストールと基本設定
パッケージをインストールします。
bashyarn add -D eslint-plugin-import
設定ファイルに追加しましょう。
javascript// .eslintrc.js
module.exports = {
plugins: ['import'],
extends: ['plugin:import/recommended'],
rules: {
// ルールをここに追加
},
};
この基盤の上に、追加のルールを設定していきます。
循環依存の検出
import/no-cycle ルールで循環依存を防ぎます。
javascript// .eslintrc.js
rules: {
'import/no-cycle': ['error', {
maxDepth: 1,
ignoreExternal: true
}]
}
循環依存が発生した瞬間にエラーとなります。
未使用 import の検出
未使用の import も自動的に検出できます。
javascript// .eslintrc.js
rules: {
'import/no-unused-modules': ['error', {
unusedExports: true
}]
}
コードの整理にも役立ちますね。
具体例
ケーススタディ 1:3 層アーキテクチャでの実装
実際の Next.js プロジェクトを例に、3 層アーキテクチャでの設定を見ていきます。
プロジェクト構造
以下のようなディレクトリ構成を想定します。
typescriptsrc/
├── presentation/
│ ├── components/ // React コンポーネント
│ ├── pages/ // Next.js ページ
│ └── hooks/ // カスタムフック
├── application/
│ ├── useCases/ // ユースケース
│ └── services/ // ビジネスロジック
└── infrastructure/
├── api/ // 外部 API クライアント
└── repositories/ // データアクセス
この構造に対して、適切な ESLint 設定を行います。
完全な ESLint 設定ファイル
以下が、3 層アーキテクチャに対応した完全な設定例です。
javascript// .eslintrc.js
module.exports = {
plugins: ['boundaries', 'import'],
extends: [
'plugin:boundaries/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
],
settings: {
'boundaries/elements': [
{
type: 'presentation',
pattern: 'src/presentation/**/*',
},
{
type: 'application',
pattern: 'src/application/**/*',
},
{
type: 'infrastructure',
pattern: 'src/infrastructure/**/*',
},
],
},
};
基本的なプラグインと層の定義を行いました。
依存関係ルールの詳細設定
各層の依存関係を詳細に設定します。
javascript// .eslintrc.js(続き)
rules: {
'boundaries/element-types': ['error', {
default: 'disallow',
message: '${file.type} は ${dependency.type} に依存できません',
rules: [
{
from: 'presentation',
allow: ['application'],
message: 'プレゼンテーション層はアプリケーション層のみに依存できます'
},
{
from: 'application',
allow: ['infrastructure'],
message: 'アプリケーション層はインフラ層のみに依存できます'
},
{
from: 'infrastructure',
allow: [],
message: 'インフラ層は他の層に依存できません'
}
]
}],
'import/no-cycle': 'error',
'import/no-unused-modules': 'error'
}
カスタムメッセージにより、違反時に分かりやすいエラーが表示されます。
実際のコード例
正しい依存関係のコード例を見てみましょう。
typescript// presentation/pages/UserListPage.tsx
import { useUserList } from '../hooks/useUserList';
import { UserCard } from '../components/UserCard';
export const UserListPage = () => {
const { users, loading } = useUserList();
if (loading) return <div>読み込み中...</div>;
return (
<div>
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
);
};
プレゼンテーション層内での import のみを使用しています。
カスタムフックでのアプリケーション層利用
プレゼンテーション層からアプリケーション層を呼び出す例です。
typescript// presentation/hooks/useUserList.ts
// ✅ OK:プレゼンテーション層 → アプリケーション層
import { getUserListUseCase } from '../../application/useCases/getUserListUseCase';
import { useState, useEffect } from 'react';
export const useUserList = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
getUserListUseCase()
.then(setUsers)
.finally(() => setLoading(false));
}, []);
return { users, loading };
};
許可された依存関係に沿って実装されています。
アプリケーション層の実装
ビジネスロジックを含むユースケースの実装例です。
typescript// application/useCases/getUserListUseCase.ts
// ✅ OK:アプリケーション層 → インフラ層
import { userRepository } from '../../infrastructure/repositories/userRepository';
import type { User } from '../types/user';
export const getUserListUseCase = async (): Promise<
User[]
> => {
// ビジネスロジック
const users = await userRepository.findAll();
// フィルタリングや整形などの処理
return users.filter((user) => user.isActive);
};
アプリケーション層がインフラ層に依存しています。
インフラ層の実装
データアクセスを担当するリポジトリの実装です。
typescript// infrastructure/repositories/userRepository.ts
import type { User } from '../../application/types/user';
// ✅ OK:インフラ層は他の層に依存しない
export const userRepository = {
findAll: async (): Promise<User[]> => {
const response = await fetch('/api/users');
return response.json();
},
findById: async (id: string): Promise<User> => {
const response = await fetch(`/api/users/${id}`);
return response.json();
},
};
インフラ層は外部システムとのやり取りに専念しています。
ケーススタディ 2:クリーンアーキテクチャでの実装
より複雑なクリーンアーキテクチャにも対応できます。
クリーンアーキテクチャの層構造
クリーンアーキテクチャでは、以下のような層構成になります。
mermaidflowchart TD
frameworks["Frameworks & Drivers<br/>(外部I/O)"]
interface["Interface Adapters<br/>(アダプター)"]
usecase["Application Business Rules<br/>(ユースケース)"]
entity["Enterprise Business Rules<br/>(エンティティ)"]
frameworks -->|依存| interface
interface -->|依存| usecase
usecase -->|依存| entity
依存の方向は、外側から内側への一方向です。
ディレクトリ構造
クリーンアーキテクチャに対応したディレクトリ構成を定義します。
typescriptsrc/
├── frameworks/ // フレームワーク・ドライバー層
│ ├── web/ // Next.js 関連
│ └── database/ // DB 接続
├── adapters/ // インターフェースアダプター層
│ ├── controllers/
│ └── presenters/
├── usecases/ // ユースケース層
│ └── *.usecase.ts
└── entities/ // エンティティ層
└── *.entity.ts
各層の責任が明確に分かれています。
クリーンアーキテクチャ用 ESLint 設定
4 層構造に対応した設定を行います。
javascript// .eslintrc.js
module.exports = {
plugins: ['boundaries'],
extends: ['plugin:boundaries/recommended'],
settings: {
'boundaries/elements': [
{
type: 'frameworks',
pattern: 'src/frameworks/**/*',
},
{
type: 'adapters',
pattern: 'src/adapters/**/*',
},
{
type: 'usecases',
pattern: 'src/usecases/**/*',
},
{
type: 'entities',
pattern: 'src/entities/**/*',
},
],
},
};
4 つの層をそれぞれエレメントとして定義しました。
依存関係の詳細ルール
内側の層ほど依存先が少なくなるように設定します。
javascript// .eslintrc.js(続き)
rules: {
'boundaries/element-types': ['error', {
default: 'disallow',
rules: [
{
from: 'frameworks',
allow: ['adapters', 'usecases', 'entities']
},
{
from: 'adapters',
allow: ['usecases', 'entities']
},
{
from: 'usecases',
allow: ['entities']
},
{
from: 'entities',
allow: [] // 最内層は依存なし
}
]
}]
}
この設定により、依存性逆転の原則が守られます。
エラー例と修正方法
実際にどのようなエラーが表示されるか見てみましょう。
違反コードの例
以下のコードは ESLint エラーとなります。
typescript// infrastructure/repositories/userRepository.ts
// ❌ エラー:infrastructure は presentation に依存できません
import { UserProfile } from '../../presentation/components/UserProfile';
export class UserRepository {
async save(profile: UserProfile) {
// ...
}
}
このコードを保存すると、以下のエラーが表示されます。
textError: infrastructure は presentation に依存できません
at src/infrastructure/repositories/userRepository.ts:3:1
infrastructure/repositories/userRepository.ts
3:1 error インフラ層からプレゼンテーション層への import は禁止です boundaries/element-types
エラーメッセージが明確ですね。
修正方法
型を共通の場所に移動することで解決できます。
typescript// application/types/userProfile.ts
// 共通の型定義
export interface UserProfile {
id: string;
name: string;
email: string;
}
型定義を適切な層に配置します。
修正後のコード
型を適切な場所から import するように修正しましょう。
typescript// infrastructure/repositories/userRepository.ts
// ✅ OK:infrastructure は application に依存可能
import type { UserProfile } from '../../application/types/userProfile';
export class UserRepository {
async save(profile: UserProfile) {
// データベースへの保存処理
await db.users.create(profile);
}
}
これで ESLint エラーは解消されます。
CI/CD での活用
ESLint チェックを CI/CD パイプラインに組み込むことで、違反コードのマージを防げます。
GitHub Actions での設定例
.github/workflows/lint.yml に以下を記述します。
yamlname: ESLint Check
on:
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
基本的なワークフローの設定です。
ESLint 実行ステップ
依存関係のインストールと ESLint 実行を追加します。
yaml- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run ESLint
run: yarn eslint . --ext .ts,.tsx
- name: Check for architecture violations
run: |
echo "アーキテクチャ境界チェック完了"
echo "違反が見つかった場合は、上記のエラーを確認してください"
これにより、PR 作成時に自動的にチェックが実行されます。
VS Code での開発体験向上
エディタでリアルタイムにエラーを確認できるようにしましょう。
VS Code 拡張機能のインストール
ESLint 拡張機能をインストールすることをお勧めします。
json// .vscode/extensions.json
{
"recommendations": ["dbaeumer.vscode-eslint"]
}
チームメンバーにも拡張機能のインストールを促せます。
プロジェクト設定
.vscode/settings.json で ESLint の動作を設定します。
json// .vscode/settings.json
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.workingDirectories": [{ "mode": "auto" }]
}
保存時に自動で修正可能なエラーが修正されます。
エディタ上でのエラー表示
コードを書いている最中に、リアルタイムでエラーが表示されるようになります。
typescript// 例:エディタ上での表示
import { UserProfile } from '../../presentation/components/UserProfile';
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
エラー: infrastructure は presentation に依存できません
boundaries/element-types
即座にフィードバックが得られるため、開発効率が向上しますね。
まとめ
ESLint を使ったアーキテクチャ境界の管理について解説してきました。重要なポイントを振り返りましょう。
自動化の重要性が第一です。人的レビューだけでは限界があるため、ESLint による自動検出が不可欠でしょう。コードを書いた瞬間に違反が検出されるため、問題の早期発見につながります。
適切なプラグイン選択も重要ですね。プロジェクトの規模や複雑さに応じて、no-restricted-imports、eslint-plugin-boundaries、eslint-plugin-import を使い分けることが推奨されます。
段階的な導入をお勧めします。既存プロジェクトに導入する場合は、まず一部の層から始めて徐々に拡大していくと良いでしょう。全体に一度に適用すると、大量のエラーに圧倒されてしまう可能性があります。
チーム全体での共有を忘れずに。ESLint の設定をリポジトリに含め、CI/CD パイプラインに組み込むことで、チーム全体で一貫したアーキテクチャを維持できます。
この記事で紹介した設定を参考に、ぜひあなたのプロジェクトでもアーキテクチャ境界の自動管理を始めてみてください。コードの品質と保守性が大きく向上するはずです。
関連リンク
articleESLint でアーキテクチャ境界を守る:層間 import を規制する設計パターン
articleESLint no-restricted-* 活用レシピ集:API 禁止・依存制限・危険パターン封じ込め
articleESLint × Vitest/Playwright:テスト環境のグローバルと型を正しく設定
articleESLint パーサ比較:espree と @typescript-eslint/parser の互換性と速度
articleESLint が遅い時の処方箋:--cache/並列化/ルール絞り込みの実践
articleESLint の内部構造を覗く:Parser・Scope・Rule・Fixer の連携を図解
articleGit rev-spec チートシート:^/~/A..B/A...B を完全図解
article【早見表】JavaScript MutationObserver & ResizeObserver 徹底活用:DOM 変化を正しく監視する
articlehtmx × Laravel/PHP 導入手順:Blade パーシャルとルート設計の落とし穴回避
articleHomebrew の Bottle vs ソースビルド比較検証:時間・サイズ・再現性の差をデータで解説
articleGemini CLI プロンプト型カタログ:仕様化・バグ調査・コード変換の雛形 40 パターン
articleHaystack で RAG アーキテクチャ設計:ハイブリッド検索と再ランキングの最適解
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来