【Node.js実務講座】番外編:なぜルーティング層で DI を行うのか?〜アーキテクチャの美しさと .env による環境分離〜

社員ブログ

酒本先輩!

第14回で src/config/database.ts を作って PrismaClient を外から注入する『True DI』に修正しましたよね。

でも、正直なところ『リポジトリの中で直接 new PrismaClient() を書くのと何が違うんだろう?』ってちょっと疑問に思ってて…

すごく良い視点ね!

一見するとコードの記述量が増えただけに思えるかもしれないけれど、実務の大きくなったプロジェクトや自動テスト(Unit Test)を書く段になると、この差が命取りになるよ 

1. 直接 new するコード(密結合)が引き起こす問題

もし、リポジトリ層の中で直接インスタンス化を行っていた場合を見てみましょう。

// 良くない例:リポジトリ内で直接インスタンスを作成(密結合)
export class PrismaCartRepository implements ICartRepository {
  private prisma = new PrismaClient(); // リポジトリが具体的なDB接続に固執している!

  async findAll() {
    return await this.prisma.cartItem.findMany();
  }
}

この設計には、主に 2つの大きな問題 があります

  1. 単体テスト(Unit Test)が困難になる
  • テストを実行するたびに本物の PostgreSQL データベースへ接続に行ってしまいます。
  • 「DBを使わずにリポジトリのロジックだけテストしたい」というときに、モック(擬似オブジェクト)に差し替える手段がありません。
  1. 接続インスタンスの増殖
  • リポジトリが複数(UserRepository, OrderRepository など)増えるたびに個別に new PrismaClient() が実行され、DBへのコネクションが過剰に消費されてしまいます。

2. ルーティング層(上位層)で DI(依存性の注入)を行う理由

そこで登場するのが DI(Dependency Injection / 依存性の注入) です。
リポジトリが必要とする PrismaClient を「自分の中で作る」のではなく、「外部から渡してもらう」仕組みに変更します。

// 良い例:外部から PrismaClient を受け取る(疎結合)
export class PrismaCartRepository implements ICartRepository {
  constructor(private readonly prisma: PrismaClient) {} // 外部から注入される!

  async findAll() {
    return await this.prisma.cartItem.findMany();
  }
}

そして、このオブジェクトの組み立てを担うのが ルーティング層(エントリーポイント付近) です

// src/routes/cart.route.ts
import { prisma } from '../config/database.js';

// 最下層(DB接続)から順にインスタンスを組み立てて注入する
const cartRepository = new PrismaCartRepository(prisma);
const cartService = new CartService(cartRepository);
const cartController = new CartController(cartService);

この設計がもたらす 3 つのメリット

  • テスト容易性(Testability)の向上
    テストコード側で「ダミーの PrismaClient」を渡すだけで、実際のDBに触れずに高速なテストを実行できます。
  • 単一責任の原則(SRP)の遵守
    リポジトリは「データの操作」だけに集中し、「接続の初期化」という関心事から解放されます。
  • ライフサイクル管理の一元化
    アプリ全体で 1 つの PrismaClient を共有(シングルトン化)できるため、コネクションプールを効率的に利用できます。

3. .env による環境分離とマルチ環境対応

依存性を外部から注入できるようにしておくと、実行環境(開発・テスト・本番)に応じた切り替え も柔軟に行えるようになります。

src/config/database.ts では、環境変数 DATABASE_URL を読み込んで接続先を決定しています。

# .env (ローカル開発環境)
DATABASE_URL="postgresql://postgres:password@localhost:5432/dev_db?schema=public"
# .env.test (自動テスト環境)
DATABASE_URL="postgresql://postgres:password@localhost:5432/test_db?schema=public"

このように設定とコードを切り離すことで、コードを 1 行も変更することなく、環境変数(.env)の切り替えだけで安全に動作環境を変更できるようになります。

番外編のまとめ

  • 密結合を避ける:クラス内部での new はテストや拡張の妨げになるため、コンストラクタ経由で受け取る。
  • DI による利点:単体テストが容易になり、データベース接続などの共通リソースを安全かつ効率的に管理できる。
  • 環境変数による柔軟性:DATABASE_URL などを .env で管理し、環境に応じた設定切り替えを安全に行う。

タイトルとURLをコピーしました