TypeScriptで学ぶ依存性逆転の原則 〜テスト可能なコードへのリファクタリング実践〜
目次
- TypeScriptで学ぶ依存性逆転の原則 〜テスト可能なコードへのリファクタリング実践〜
1. はじめに
この記事で学べること
この記事では、シンプルな図書館アプリケーションを題材に、以下の内容を学びます。
- クラス間の強い依存関係がもたらす問題点
- interfaceを使った依存関係の解消方法
- **依存性注入(Dependency Injection)**パターンの実践
- Jestを使ったユニットテストの書き方
- モックを活用したテスト手法
実際のコードを段階的にリファクタリングしながら、「なぜこの設計が必要なのか」を体感していただきます。
対象読者
- TypeScriptの基本文法を理解している方
- クラスやinterfaceの構文は知っているが、「なぜinterfaceを使うのか」がピンと来ていない方
- テストを書きたいが、どう設計すればテストしやすくなるか分からない方
- レイヤードアーキテクチャやクリーンアーキテクチャに興味がある方
2. プロジェクトのセットアップ
まず、今回使用するプロジェクトをセットアップします。
ディレクトリ構成
最終的なディレクトリ構成は以下の通りです。
library-app/
├── prisma/
│ ├── generated/prisma/ # Prismaが生成するクライアント
│ └── schema.prisma # データベーススキーマ定義
├── src/
│ ├── buisinessLogic/ # ビジネスロジック層
│ │ ├── bookService.ts
│ │ └── bookServiceInterface.ts
│ ├── dataAccess/ # データアクセス層
│ │ ├── prismaBookRepository.ts
│ │ └── bookRepositoryInterface.ts
│ ├── presentation/ # プレゼンテーション層
│ │ └── bookController.ts
│ └── app.ts # エントリーポイント
├── package.json
├── tsconfig.json
├── jest.config.js
└── prisma.config.ts
package.json
以下のpackage.jsonをプロジェクトルートに作成してください。
{
"name": "library-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "tsx watch src/app.ts",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"@prisma/adapter-better-sqlite3": "^7.3.0",
"@prisma/client": "^7.3.0",
"express": "^5.2.1"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/jest": "^30.0.0",
"@types/node": "^25.1.0",
"jest": "^30.2.0",
"prisma": "^7.3.0",
"ts-jest": "^29.4.6",
"ts-node-dev": "^2.0.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
}
}
注意:
"type": "module"を指定しているため、プロジェクト全体がESM(ECMAScript Modules)として動作します。
tsconfig.json
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"types": ["jest", "node"],
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true
}
}
Prismaの設定
prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "./generated/prisma"
}
datasource db {
provider = "sqlite"
}
model Book {
id String @id @default(uuid())
title String
isAvailable Boolean
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
prisma.config.ts
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
seed: "npx tsx prisma/seed.ts",
},
datasource: {
url: process.env["DATABASE_URL"] ?? "file:./prisma/dev.db",
},
});
依存関係のインストール
以下のコマンドを実行して、依存関係をインストールします。
# プロジェクトディレクトリを作成
mkdir library-app
cd library-app
# package.jsonを配置後、依存関係をインストール
npm install
# Prismaクライアントを生成
npx prisma generate
# データベースのマイグレーション
npx prisma migrate dev --name init
3. 問題のあるコード(Before)
アプリケーション全体像
今回のアプリケーションは、シンプルな図書館の書籍管理システムです。以下の3層構造になっています。
┌─────────────────────────────────────────────────────────────┐
│ Presentation層 │
│ (BookController) │
│ HTTPリクエスト/レスポンスの処理 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Business Logic層 │
│ (BookService) │
│ ビジネスロジックの実行 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Data Access層 │
│ (PrismaBookRepository) │
│ データベースとのやり取り │
└─────────────────────────────────────────────────────────────┘
各ファイルの役割と実装
まず、**問題のあるコード(interface導入前)**を見ていきましょう。
src/dataAccess/prismaBookClient.ts(Data Access層)
データベースとの実際のやり取りを担当します。
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient, type Book } from "../../prisma/generated/prisma/client";
const adapter = new PrismaBetterSqlite3({
url: process.env.DATABASE_URL || "file:./dev.db",
});
export class PrismaBookRepository {
private prisma: PrismaClient;
constructor(){
this.prisma = new PrismaClient({ adapter });
}
createBook = async (title: string): Promise<Book> => {
return await this.prisma.book.create({
data:{
title,
isAvailable: true,
}
})
}
findBookByID = async (id: string): Promise<Book | null> => {
return await this.prisma.book.findUnique({
where: { id },
});
};
findAllBooks = async (): Promise<Book[]> => {
return await this.prisma.book.findMany();
}
}
役割: Prismaを使ってSQLiteデータベースに対するCRUD操作を行います。
src/buisinessLogic/bookService.ts(Business Logic層)
ビジネスロジックを担当します。
import { PrismaBookRepository } from "../dataAccess/prismaBookClient";
import type { Book } from "../../prisma/generated/prisma/client";
export class BookService {
private bookRepository: PrismaBookRepository;
constructor(){
this.bookRepository = new PrismaBookRepository; // 直接インスタンス化
}
addBook = async (title: string): Promise<Book> => {
return await this.bookRepository.createBook(title);
}
findBookByID = async (id: string): Promise<Book | null> => {
return await this.bookRepository.findBookByID(id);
}
findAllBooks = async (): Promise<Book[]> => {
return await this.bookRepository.findAllBooks();
}
}
問題点: constructor内でPrismaBookRepositoryを直接newしています。
src/presentation/bookController.ts(Presentation層)
HTTPリクエストを受け取り、レスポンスを返します。
import type { Request, Response } from "express";
import { BookService } from "../buisinessLogic/bookService";
export class BookController {
private bookSerivce: BookService;
constructor(){
this.bookSerivce = new BookService(); // 直接インスタンス化
}
addBook = async (req: Request, res: Response): Promise<void> => {
try {
const title = req.body.title as string;
const book = await this.bookSerivce.addBook(title);
res.status(201).json(book);
} catch (error) {
console.log(error);
res.status(500).json({ error: "書籍の登録に失敗しました" });
}
}
findBookByID = async (req: Request, res: Response): Promise<void> => {
try {
const id = req.params.id as string;
const book = await this.bookSerivce.findBookByID(id);
if (book) {
res.status(200).json(book);
} else {
res.status(404).json({ error: "書籍が見つかりませんでした" });
}
} catch (error) {
console.log(error);
res.status(500).json({ error: "書籍の検索に失敗しました" });
}
}
findAllBooks = async (req: Request, res: Response): Promise<void> => {
try {
const books = await this.bookSerivce.findAllBooks();
res.status(200).json(books);
} catch (error) {
console.log(error);
res.status(500).json({ error: "書籍の検索に失敗しました" });
}
}
}
問題点: ここでもBookServiceを直接newしています。
src/app.ts(エントリーポイント)
import express from 'express';
import { BookController } from './presentation/bookController';
const app = express();
app.use(express.json());
const PORT = process.env.PORT || 3009;
app.get('/', (req, res) => {
res.send('Hello World');
});
const bookController = new BookController();
app.post('/books', (req, res) => bookController.addBook(req, res));
app.get('/books/:id', (req, res) => bookController.findBookByID(req, res));
app.get('/books', (req, res) => bookController.findAllBooks(req, res));
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
クラス間の強い依存関係とは
上記のコードには**強い依存関係(密結合)**があります。図で表すと以下のようになります。
┌──────────────────┐
│ BookController │
│ │
│ constructor(){ │
│ this.service │──────────┐
│ = new │ │ 具体クラスを直接new
│ BookService() │ │
│ } │ ▼
└──────────────────┘ ┌──────────────────┐
│ BookService │
│ │
│ constructor(){ │
│ this.repo │──────────┐
│ = new │ │ 具体クラスを直接new
│ PrismaBook │ │
│ Repository() │ ▼
│ } │ ┌──────────────────────┐
└──────────────────┘ │ PrismaBookRepository │
│ │
│ 実際のDB接続 │
│ SQLiteを操作 │
└──────────────────────┘
「強い依存関係」とは:
- 上位のクラス(BookService)が下位のクラス(PrismaBookRepository)の具体的な実装クラスを直接知っている
-
newキーワードで直接インスタンスを生成している - 下位クラスを変更すると、上位クラスにも影響が及ぶ
なぜこのコードはテストしにくいのか
最大の問題: BookServiceをテストしようとすると、必ず実際のデータベースが必要になる
// BookServiceをテストしたい
const service = new BookService();
// しかし、constructor内で PrismaBookRepository を new している
// → PrismaBookRepository は実際のDBに接続しようとする
// → テスト用のDBを用意しないとテストできない!
この設計がもたらす問題:
| 問題 | 説明 |
|---|---|
| テストが遅い | 毎回実際のDBにアクセスするため、テスト実行に時間がかかる |
| テストが不安定 | DB接続の問題やデータ状態に依存してテストが失敗する可能性がある |
| テストの独立性がない | テスト間でDBのデータが共有され、テストの順序に依存してしまう |
| 差し替えができない | 本番用のDBを使わざるを得ず、モックに差し替えられない |
【テストしたい範囲】
┌─────────────────────────────────────────┐
│ BookService │ ← ここだけテストしたい
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ PrismaBookRepository │ ← でもこれも動いてしまう
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 実際のデータベース │ ← これも動いてしまう!
└─────────────────────────────────────────┘
4. interfaceを導入して改善(After)
依存性逆転の原則とは
**依存性逆転の原則(Dependency Inversion Principle: DIP)**は、SOLID原則のひとつです。
「上位モジュールは下位モジュールに依存してはならない。両者は抽象(interface)に依存すべきである」
【Before: 具体に依存】
BookService ───────────→ PrismaBookRepository(具体クラス)
【After: 抽象に依存】
BookService ───────────→ BookRepositoryInterface(抽象)
△
│ implements
PrismaBookRepository(具体クラス)
ポイント: 矢印の向き(依存の方向)が逆転しています。
interfaceを使ったリファクタリング手順
Step 1: interfaceを定義する
まず、Repository層のinterfaceを作成します。
src/dataAccess/bookRepositoryInterface.ts
import type { Book } from "../../prisma/generated/prisma/client";
export interface BookRepositoryInterface {
createBook: (title: string) => Promise<Book>;
findBookByID: (id: string) => Promise<Book | null>;
findAllBooks: () => Promise<Book[]>;
}
同様に、Service層のinterfaceも作成します。
src/buisinessLogic/bookServiceInterface.ts
import type { Book } from "../../prisma/generated/prisma/client";
export interface BookServiceInterface {
addBook: (title: string) => Promise<Book>;
findBookByID: (id: string) => Promise<Book | null>;
findAllBooks: () => Promise<Book[]>;
}
Step 2: 具体クラスにinterfaceを実装させる
src/dataAccess/prismaBookRepository.ts
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient, type Book } from "../../prisma/generated/prisma/client";
import type { BookRepositoryInterface } from "./bookRepositoryInterface";
const adapter = new PrismaBetterSqlite3({
url: process.env.DATABASE_URL || "file:./dev.db",
});
// interfaceを実装(implements)
export class PrismaBookRepository implements BookRepositoryInterface {
private prisma: PrismaClient;
constructor(){
this.prisma = new PrismaClient({ adapter });
}
createBook = async (title: string): Promise<Book> => {
return await this.prisma.book.create({
data:{
title,
isAvailable: true,
}
})
}
findBookByID = async (id: string): Promise<Book | null> => {
return await this.prisma.book.findUnique({
where: { id },
});
};
findAllBooks = async (): Promise<Book[]> => {
return await this.prisma.book.findMany();
}
}
依存性注入(DI)パターンの導入
次に、**依存性注入(Dependency Injection)**を導入します。
依存性注入とは: クラスが必要とする依存オブジェクトを、外部から渡す(注入する)設計パターン
Before(自分でnewする):
class BookService {
private bookRepository: PrismaBookRepository;
constructor(){
this.bookRepository = new PrismaBookRepository(); // 自分で作る
}
}
After(外部から受け取る):
class BookService {
constructor(private bookRepository: BookRepositoryInterface){ // 外部から受け取る
}
}
改善後のコード全体像
src/buisinessLogic/bookService.ts
import type { Book } from "../../prisma/generated/prisma/client";
import type { BookRepositoryInterface } from "../dataAccess/bookRepositoryInterface";
import type { BookServiceInterface } from "./bookServiceInterface";
export class BookService implements BookServiceInterface {
// コンストラクタで外部からRepositoryを受け取る(依存性注入)
constructor(private bookRepository: BookRepositoryInterface){
}
addBook = async (title: string): Promise<Book> => {
return await this.bookRepository.createBook(title);
}
findBookByID = async (id: string): Promise<Book | null> => {
return await this.bookRepository.findBookByID(id);
}
findAllBooks = async (): Promise<Book[]> => {
return await this.bookRepository.findAllBooks();
}
}
改善点: BookRepositoryInterfaceという抽象に依存し、具体的な実装は外部から注入されます。
src/presentation/bookController.ts
import type { Request, Response } from "express";
import type { BookServiceInterface } from "../buisinessLogic/bookServiceInterface";
export class BookController {
// コンストラクタで外部からServiceを受け取る(依存性注入)
constructor(private readonly bookService: BookServiceInterface){
}
addBook = async (req: Request, res: Response): Promise<void> => {
try {
const title = req.body.title as string;
const book = await this.bookService.addBook(title);
res.status(201).json(book);
} catch (error) {
console.log(error);
res.status(500).json({ error: "書籍の登録に失敗しました" });
}
}
findBookByID = async (req: Request, res: Response): Promise<void> => {
try {
const id = req.params.id as string;
const book = await this.bookService.findBookByID(id);
if (book) {
res.status(200).json(book);
} else {
res.status(404).json({ error: "書籍が見つかりませんでした" });
}
} catch (error) {
console.log(error);
res.status(500).json({ error: "書籍の検索に失敗しました" });
}
}
findAllBooks = async (req: Request, res: Response): Promise<void> => {
try {
const books = await this.bookService.findAllBooks();
res.status(200).json(books);
} catch (error) {
console.log(error);
res.status(500).json({ error: "書籍の検索に失敗しました" });
}
}
}
src/app.ts(組み立てを行う場所)
import express from 'express';
import { BookController } from './presentation/bookController';
import { BookService } from './buisinessLogic/bookService';
import { PrismaBookRepository } from './dataAccess/prismaBookRepository';
const app = express();
app.use(express.json());
const PORT = process.env.PORT || 3009;
app.get('/', (req, res) => {
res.send('Hello World');
});
// ここで依存関係を組み立てる(Composition Root)
const bookRepository = new PrismaBookRepository(); // 具体的な実装を生成
const bookService = new BookService(bookRepository); // Serviceに注入
const bookController = new BookController(bookService); // Controllerに注入
app.post('/books', (req, res) => bookController.addBook(req, res));
app.get('/books/:id', (req, res) => bookController.findBookByID(req, res));
app.get('/books', (req, res) => bookController.findAllBooks(req, res));
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
ポイント: 依存関係の組み立てはapp.ts(Composition Root)で一元管理されます。
改善後の依存関係図:
┌──────────────────┐ ┌──────────────────────────┐
│ BookController │───────→│ BookServiceInterface │ ← 抽象に依存
└──────────────────┘ └──────────────────────────┘
△
│ implements
┌──────────────────────────┐
│ BookService │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ BookRepositoryInterface │ ← 抽象に依存
└──────────────────────────┘
△
│ implements
┌──────────────────────────┐
│ PrismaBookRepository │
└──────────────────────────┘
5. テストを書いてメリットを実感
Jest + ts-jestのセットアップ
ESMプロジェクトでJestを使用するための設定を行います。
jest.config.js
/** @type {import('jest').Config} */
export default {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testMatch: ['**/__tests__/**/*.ts', '**/*.test.ts', '**/*.spec.ts'],
transform: {
'^.+\\.tsx?$': ['ts-jest', {
useESM: true,
}],
},
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
};
注意:
package.jsonに"type": "module"が設定されているため、.cjsではなくESM形式の.jsで設定ファイルを作成しています。export defaultを使用しているのがポイントです。
tsconfig.jsonの確認
tsconfig.jsonのtypes配列にjestが含まれていることを確認してください。
{
"compilerOptions": {
"types": ["jest", "node"],
// ...
}
}
これにより、describe、it、expectなどのJestのグローバル関数がTypeScriptで認識されます。
モックとは何か
**モック(Mock)**とは、テスト対象が依存しているオブジェクトの「偽物」です。
【本番環境】
BookService ────→ PrismaBookRepository ────→ 実際のDB
【テスト環境】
BookService ────→ モック(偽のRepository) ────→ DBアクセスなし
モックの役割:
| 機能 | 説明 |
|---|---|
| 呼び出しの記録 | どのメソッドが、何回、どんな引数で呼ばれたかを記録 |
| 戻り値の制御 | 任意の値を返すように設定できる |
| 外部依存の排除 | 実際のDB、API、ファイルシステムへのアクセスを回避 |
// jest.fn() でモック関数を作成
const mockFunction = jest.fn();
// 呼び出すと記録される
mockFunction("hello");
// 呼び出されたことを検証できる
expect(mockFunction).toHaveBeenCalledWith("hello");
// 戻り値を設定することもできる
mockFunction.mockReturnValue("world");
実際にモックを使ったテストを書く
src/buisinessLogic/bookService.test.ts
import type { Book } from "../../prisma/generated/prisma/client";
import type { BookRepositoryInterface } from "../dataAccess/bookRepositoryInterface";
import { BookService } from "./bookService";
// モックオブジェクトを作成
// BookRepositoryInterfaceの各メソッドにjest.fn()を割り当てる
const mockBookRepository: jest.Mocked<BookRepositoryInterface> = {
createBook: jest.fn(), // 仮の関数を作成
findBookByID: jest.fn(),
findAllBooks: jest.fn(),
};
describe('BookService', () => {
let bookService: BookService;
beforeEach(() => {
// 各テストの前に、モックを注入したBookServiceを作成
bookService = new BookService(mockBookRepository);
});
afterEach(() => {
// 各テストの後に、モックの呼び出し履歴をクリア
jest.clearAllMocks();
});
it('書籍の登録が成功する', async () => {
// テスト用のデータを準備
const newBook: Book = {
id: '1',
title: 'test book',
isAvailable: true,
createdAt: new Date(),
updatedAt: new Date(),
};
// モックの戻り値を設定
// createBookが呼ばれたら、newBookを返すように設定
mockBookRepository.createBook.mockResolvedValue(newBook);
// テスト対象のメソッドを実行
const result = await bookService.addBook('test book');
// 検証1: 戻り値が正しいこと
expect(result).toEqual(newBook);
// 検証2: Repositoryのメソッドが正しい引数で呼ばれたこと
expect(mockBookRepository.createBook).toHaveBeenCalledWith('test book');
});
});
テストを実行:
npm test
実行結果:
PASS src/buisinessLogic/bookService.test.ts
BookService
✓ 書籍の登録が成功する (3 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
なぜこの方法でモックするのか
答え: BookServiceがinterfaceに依存しているため、クリーンにモックを注入できる
// BookServiceのコンストラクタ
constructor(private bookRepository: BookRepositoryInterface){
}
BookRepositoryInterfaceという抽象に依存しているため、その実装は何でも良いのです。
【本番】BookRepositoryInterfaceを満たす → PrismaBookRepository を渡す
【テスト】BookRepositoryInterfaceを満たす → モックオブジェクトを渡す
┌─────────────────────────────────────────┐
│ BookRepositoryInterface │ ← 契約(約束事)
│ ・createBook()を持つこと │
│ ・findBookByID()を持つこと │
│ ・findAllBooks()を持つこと │
└─────────────────────────────────────────┘
△ △
│ │
│implements │満たしている
│ │
┌─────────────────┐ ┌─────────────────┐
│ PrismaBook │ │ モック │
│ Repository │ │ オブジェクト │
│ │ │ │
│ 本物のDB接続 │ │ 呼び出し記録のみ │
└─────────────────┘ └─────────────────┘
補足: interfaceがなくてもモック自体は可能
注意: 実はjest.mock()を使えば、interfaceがなくてもモックは可能です。
// jest.mock()を使った方法(interfaceなし)
import { BookService } from "./bookService";
import { PrismaBookRepository } from "../dataAccess/prismaBookClient";
jest.mock("../dataAccess/prismaBookClient"); // モジュール全体をモック化
const service = new BookService();
// → PrismaBookRepositoryが自動的にモックに置き換わる
では、なぜわざわざinterfaceを導入するのか?
| 観点 | jest.mock()のみ | interface + DI |
|---|---|---|
| 可読性 | 何がモックか分かりにくい | 明示的で分かりやすい |
| リファクタリング耐性 | ファイルパスに依存や具体の実装に依存 | interfaceのみに依存(安定) |
| 拡張性 | 実装の差し替えが困難 | 新しい実装を簡単に追加可能 |
結論: モック「できるかどうか」ではなく、「クリーンにできるかどうか」と「設計が改善されるかどうか」が重要です。
interface + DIパターンを使うことで、テストのためだけでなく、本番コードの設計自体が良くなります。例えば、将来「PostgreSQLに変更したい」「インメモリDBでテストしたい」となった場合も、新しい実装クラスを作って差し替えるだけで対応できます。
6. まとめと今後の展望
今回の改善で得られたもの
| Before | After |
|---|---|
| 具体クラスに直接依存 | interfaceに依存 |
| クラス内でnewする | 外部から注入される(DI) |
| モックは可能だが設定が複雑 | クリーンにモックを注入できる |
| テストにDBが必要(または複雑なjest.mock()が必要) | DBなしで高速テスト |
| 変更に弱い(密結合) | 変更に強い(疎結合) |
今回学んだ3つの重要な概念:
- 依存性逆転の原則: 具体ではなく抽象に依存する
- 依存性注入(DI): 必要なものは外部から受け取る
- モック: interfaceを満たす偽オブジェクトでテストを効率化
クリーンアーキテクチャへの道(次回予告)
今回実装したレイヤードアーキテクチャは、クリーンアーキテクチャへの第一歩です。
次回の記事では、以下の内容を扱う予定です。
- ドメイン層の導入: ビジネスルールをより明確に分離
- ユースケース層: アプリケーション固有のビジネスロジックの整理
- 依存関係のルール: 「内側から外側への依存禁止」の徹底
- より柔軟な設計: フレームワークやDBの差し替えが容易なアーキテクチャ
┌─────────────────────────────────────────┐
│ Frameworks & Drivers │
│ (Express, Prisma, etc.) │
│ ┌───────────────────────────────────┐ │
│ │ Interface Adapters │ │
│ │ (Controllers, Repositories) │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Application Layer │ │ │
│ │ │ (Use Cases) │ │ │
│ │ │ ┌───────────────────────┐ │ │ │
│ │ │ │ Domain Layer │ │ │ │
│ │ │ │ (Entities, Rules) │ │ │ │
│ │ │ └───────────────────────┘ │ │ │
│ │ └─────────────────────────────┘ │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
乞うご期待!