T-CREATOR

Prisma を Monorepo で使い倒す:パス解決・generate の共有・依存戦略

Prisma を Monorepo で使い倒す:パス解決・generate の共有・依存戦略

Monorepo 環境で Prisma を活用する際、パス解決や generate の共有、依存関係の管理など、特有の課題に直面することがあります。複数のパッケージで同じデータベーススキーマを共有したい、あるいは各パッケージで異なる Prisma スキーマを管理したいといったニーズに対応するには、適切な戦略が必要です。

本記事では、Monorepo における Prisma の実践的な運用方法を、実際のコード例とともに詳しく解説していきます。効率的な開発環境を構築し、チーム全体の生産性を向上させましょう。

背景

Monorepo は、複数のプロジェクトやパッケージを単一のリポジトリで管理する手法として、近年多くの開発チームで採用されています。フロントエンド、バックエンド、共有ライブラリなどを一元管理することで、コードの再利用性が高まり、依存関係の管理も容易になります。

一方、Prisma は TypeScript/JavaScript のための次世代 ORM として、型安全なデータベースアクセスを提供しています。スキーマファイルからクライアントコードを自動生成する仕組みにより、開発体験が大幅に向上します。

Monorepo における Prisma の位置づけ

以下の図は、典型的な Monorepo 構成と Prisma の配置パターンを示しています。

mermaidflowchart TB
  root["Monorepo ルート"]
  packages["packages/"]
  api["api<br/>(Backend)"]
  web["web<br/>(Frontend)"]
  shared["shared<br/>(共有ライブラリ)"]
  db["database<br/>(Prisma)"]

  root --> packages
  packages --> api
  packages --> web
  packages --> shared
  packages --> db

  api -.->|依存| db
  web -.->|依存| db
  shared -.->|依存| db

  style db fill:#e1f5ff
  style api fill:#fff4e1
  style web fill:#fff4e1
  style shared fill:#f0f0f0

このような構成では、データベーススキーマを専用のパッケージとして切り出し、他のパッケージから参照する形が一般的です。しかし、この際にパス解決や generate コマンドの実行タイミング、依存関係の適切な設定が課題となります。

Monorepo ツールの選択肢

現在、主流となっている Monorepo 管理ツールには以下のようなものがあります。

#ツール名特徴Prisma との相性
1Yarn Workspacesシンプルで軽量、npm/yarn と親和性が高い★★★
2Turborepoキャッシュ機能が強力、ビルド高速化に優れる★★★
3Nx大規模プロジェクト向け、豊富なプラグイン★★☆
4Lerna歴史が長い、パッケージ公開に強い★★☆

本記事では、Yarn Workspaces をベースに解説を進めますが、他のツールでも基本的な考え方は応用できます。

課題

Monorepo で Prisma を利用する際、以下のような課題に直面することがあります。

パス解決の問題

Prisma Client を import する際、各パッケージから正しいパスで参照できるようにする必要があります。特に @prisma​/​client のインポートパスと、実際の Prisma スキーマの配置場所の不一致が問題になりやすいです。

generate コマンドの実行タイミング

prisma generate コマンドをいつ、どこで実行するかという問題があります。各パッケージで個別に実行するのか、ルートで一括実行するのか、あるいはビルド時に自動実行するのか、最適な方法を選択する必要があります。

依存関係の管理

複数のパッケージで Prisma を共有する場合、バージョンの統一や依存関係の明示的な宣言が重要です。特に @prisma​/​clientprisma CLI のバージョンが一致していないと、予期しないエラーが発生することがあります。

以下の図は、よくある問題のパターンを示しています。

mermaidflowchart LR
  subgraph problem["❌ 問題のあるパターン"]
    api1["api パッケージ"]
    web1["web パッケージ"]
    db1["database パッケージ"]

    api1 -->|"バージョン 4.0.0"| db1
    web1 -->|"バージョン 5.0.0"| db1
    db1 -.->|"バージョン不一致<br/>でエラー"| err["generate 失敗"]
  end

  style err fill:#ffe1e1
  style problem fill:#fff9f0

バージョンの不一致や、パスの誤った参照により、開発環境が不安定になってしまいます。

マイグレーション戦略

スキーマの変更を複数のパッケージに反映させる際、マイグレーションの実行順序やタイミングも考慮しなければなりません。開発環境と CI/CD 環境での挙動の違いも注意が必要です。

解決策

これらの課題に対する実践的な解決策を、段階的に見ていきましょう。

基本的なディレクトリ構成

まず、Monorepo の基本構成を整えます。以下のようなディレクトリ構造を推奨します。

gomonorepo-root/
├── package.json
├── yarn.lock
└── packages/
    ├── database/
    │   ├── package.json
    │   ├── prisma/
    │   │   └── schema.prisma
    │   └── src/
    │       └── index.ts
    ├── api/
    │   ├── package.json
    │   └── src/
    │       └── index.ts
    └── web/
        ├── package.json
        └── src/
            └── index.ts

この構成により、Prisma スキーマを専用パッケージとして管理し、他のパッケージから参照できるようになります。

ルートの package.json 設定

Monorepo のルートディレクトリに配置する package.json を設定します。Yarn Workspaces を有効化し、共通のスクリプトを定義します。

json{
  "name": "monorepo-root",
  "version": "1.0.0",
  "private": true,
  "workspaces": ["packages/*"]
}

private: true を設定することで、ルートパッケージが誤って公開されるのを防ぎます。workspaces フィールドで、どのディレクトリをワークスペースとして扱うかを指定します。

次に、共通で使用する依存関係とスクリプトを追加します。

json{
  "name": "monorepo-root",
  "version": "1.0.0",
  "private": true,
  "workspaces": ["packages/*"],
  "scripts": {
    "db:generate": "yarn workspace @monorepo/database prisma generate",
    "db:migrate": "yarn workspace @monorepo/database prisma migrate dev",
    "db:studio": "yarn workspace @monorepo/database prisma studio"
  },
  "devDependencies": {
    "prisma": "^5.8.0",
    "typescript": "^5.3.0"
  }
}

ルートレベルで Prisma CLI をインストールし、各パッケージからコマンドを実行できるようにします。yarn workspace コマンドを使うことで、特定のパッケージ内でコマンドを実行できます。

database パッケージの設定

Prisma スキーマを管理する専用パッケージを作成します。まず packages​/​database​/​package.json を設定しましょう。

json{
  "name": "@monorepo/database",
  "version": "1.0.0",
  "main": "./src/index.ts",
  "types": "./src/index.ts"
}

パッケージ名には @monorepo​/​ のようなスコープを付けることで、他の npm パッケージとの衝突を防ぎます。maintypes フィールドで、エントリーポイントを明示的に指定します。

次に、Prisma 関連の依存関係を追加します。

json{
  "name": "@monorepo/database",
  "version": "1.0.0",
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "scripts": {
    "generate": "prisma generate",
    "migrate": "prisma migrate dev",
    "studio": "prisma studio"
  },
  "dependencies": {
    "@prisma/client": "^5.8.0"
  },
  "devDependencies": {
    "prisma": "^5.8.0"
  }
}

@prisma​/​client を dependencies に、prisma CLI を devDependencies に配置します。バージョンは必ず一致させてください。

Prisma スキーマの設定

packages​/​database​/​prisma​/​schema.prisma ファイルを作成し、データベーススキーマを定義します。

prisma// データソースの設定
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

datasource ブロックでは、使用するデータベースの種類と接続情報を定義します。url には環境変数を使用することで、環境ごとに異なる接続先を設定できます。

次に、Prisma Client の生成設定を追加します。

prisma// Prisma Client の生成設定
generator client {
  provider = "prisma-client-js"
  output   = "../node_modules/.prisma/client"
}

output フィールドが重要なポイントです。デフォルトでは node_modules​/​@prisma​/​client に生成されますが、Monorepo では明示的にパスを指定することで、他のパッケージからも確実に参照できるようにします。

データモデルの定義例を示します。

prisma// ユーザーモデルの定義
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

各フィールドには型とデコレーターを指定します。@id で主キー、@unique で一意制約、@default でデフォルト値を設定できます。

リレーションの定義も追加しましょう。

prisma// 投稿モデルの定義
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

@relation デコレーターで、User と Post の関係を定義しています。fields で外部キーフィールド、references で参照先のフィールドを指定します。

database パッケージのエクスポート設定

生成された Prisma Client を他のパッケージから使用できるようにするため、packages​/​database​/​src​/​index.ts を作成します。

typescript// Prisma Client のエクスポート
export { PrismaClient } from '@prisma/client';

シンプルに Prisma Client を再エクスポートすることで、他のパッケージから @monorepo​/​database を通じてアクセスできるようになります。

より便利にするため、シングルトンインスタンスも提供しましょう。

typescript// Prisma Client のシングルトンインスタンス
import { PrismaClient } from '@prisma/client';

// グローバルな型定義を拡張
declare global {
  var prisma: PrismaClient | undefined;
}

TypeScript のグローバル型定義を拡張することで、開発モードでのホットリロード時にインスタンスを再利用できます。

インスタンスの作成と管理のロジックを実装します。

typescript// Prisma インスタンスの作成と管理
export const prisma =
  global.prisma ||
  new PrismaClient({
    log: ['query', 'error', 'warn'],
  });

// 開発環境ではグローバルに保存
if (process.env.NODE_ENV !== 'production') {
  global.prisma = prisma;
}

開発環境では global.prisma にインスタンスを保存し、ホットリロード時に再利用します。本番環境では毎回新しいインスタンスを作成します。

型情報もエクスポートしておくと便利です。

typescript// Prisma の型をエクスポート
export type { User, Post } from '@prisma/client';

これにより、他のパッケージで型を使用する際に、直接 @prisma​/​client をインポートする必要がなくなります。

api パッケージの設定

バックエンド API パッケージから database パッケージを使用する設定を行います。まず packages​/​api​/​package.json を作成します。

json{
  "name": "@monorepo/api",
  "version": "1.0.0",
  "main": "./src/index.ts",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc"
  }
}

基本的なパッケージ情報とスクリプトを定義します。tsx は TypeScript を直接実行できる便利なツールです。

database パッケージへの依存関係を追加します。

json{
  "name": "@monorepo/api",
  "version": "1.0.0",
  "main": "./src/index.ts",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc"
  },
  "dependencies": {
    "@monorepo/database": "*"
  },
  "devDependencies": {
    "tsx": "^4.7.0",
    "typescript": "^5.3.0"
  }
}

"@monorepo​/​database": "*" により、同じワークスペース内のパッケージを参照します。バージョンを * にすることで、常に最新のローカルパッケージを使用できます。

api パッケージでの Prisma 使用例

実際に API パッケージで Prisma を使用するコード例を示します。packages​/​api​/​src​/​index.ts を作成しましょう。

typescript// Prisma Client のインポート
import { prisma, User } from '@monorepo/database';

database パッケージから Prisma Client と型をインポートします。パスは @monorepo​/​database だけで、シンプルに記述できます。

ユーザーを作成する関数の実装例です。

typescript// ユーザー作成関数
async function createUser(
  email: string,
  name: string
): Promise<User> {
  // Prisma Client を使ってユーザーを作成
  const user = await prisma.user.create({
    data: {
      email,
      name,
    },
  });

  return user;
}

prisma.user.create() メソッドは完全に型安全です。TypeScript が自動的に補完とエラーチェックを行ってくれます。

ユーザー一覧を取得する関数も実装しましょう。

typescript// ユーザー一覧取得関数
async function getAllUsers(): Promise<User[]> {
  // リレーションを含めてユーザーを取得
  const users = await prisma.user.findMany({
    include: {
      posts: true, // 投稿も含める
    },
  });

  return users;
}

include オプションで関連するデータも同時に取得できます。これもすべて型安全に記述できます。

メイン処理の実装例です。

typescript// メイン処理
async function main() {
  try {
    // ユーザーを作成
    const user = await createUser(
      'test@example.com',
      'テストユーザー'
    );
    console.log('ユーザーを作成しました:', user);

    // 全ユーザーを取得
    const users = await getAllUsers();
    console.log('全ユーザー:', users);
  } catch (error) {
    console.error('エラーが発生しました:', error);
    throw error;
  } finally {
    // 接続を切断
    await prisma.$disconnect();
  }
}

// プログラム実行
main();

finally ブロックで prisma.$disconnect() を呼び出し、データベース接続を適切にクリーンアップします。

パス解決の最適化

TypeScript のパス解決をさらに最適化するため、tsconfig.json を設定します。packages​/​api​/​tsconfig.json を作成しましょう。

json{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

基本的な TypeScript の設定を行います。strict: true で厳密な型チェックを有効にしています。

パスマッピングの設定を追加します。

json{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "paths": {
      "@monorepo/database": ["../database/src"]
    }
  }
}

paths オプションで、@monorepo​/​database を相対パスにマッピングします。これにより、IDE の補完やジャンプ機能が正しく動作するようになります。

generate コマンドの実行戦略

Prisma Client を生成するタイミングとして、以下の 3 つの戦略があります。

戦略 1: 手動実行

開発者が必要に応じて手動で実行する方法です。ルートディレクトリで以下のコマンドを実行します。

bash# database パッケージで generate を実行
yarn db:generate

シンプルですが、スキーマ変更後に実行を忘れるリスクがあります。小規模なチームや個人開発に適しています。

戦略 2: postinstall スクリプト

依存関係のインストール後に自動実行する方法です。packages​/​database​/​package.json に以下を追加します。

json{
  "scripts": {
    "postinstall": "prisma generate"
  }
}

yarn install を実行すると、自動的に prisma generate が実行されます。CI/CD 環境でも確実に実行されるため、おすすめの方法です。

戦略 3: prepare スクリプト

Git フックと連携する方法です。ルートの package.json に以下を追加します。

json{
  "scripts": {
    "prepare": "yarn db:generate"
  }
}

yarn install 後、git clone 後などに自動実行されます。ただし、実行頻度が高すぎる場合があるため、状況に応じて使い分けましょう。

以下の図は、各戦略の実行タイミングを示しています。

mermaidflowchart TB
  start["開発開始"]
  install["yarn install"]
  schema["schema.prisma<br/>編集"]

  subgraph strategy1["戦略1: 手動実行"]
    manual["yarn db:generate<br/>(手動)"]
  end

  subgraph strategy2["戦略2: postinstall"]
    auto1["prisma generate<br/>(自動)"]
  end

  subgraph strategy3["戦略3: prepare"]
    auto2["prisma generate<br/>(自動)"]
  end

  start --> install
  install --> strategy2
  install --> strategy3
  schema --> strategy1

  style strategy1 fill:#fff4e1
  style strategy2 fill:#e1f5ff
  style strategy3 fill:#e8f5e1

開発フローに合わせて、最適な戦略を選択してください。

環境変数の管理

データベース接続情報などの環境変数を適切に管理することも重要です。ルートディレクトリに .env ファイルを作成します。

bash# データベース接続URL
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"

本番環境とは異なる接続情報を使用します。このファイルは .gitignore に追加し、リポジトリにコミットしないようにしましょう。

.env.example ファイルも作成しておくと、他の開発者がセットアップしやすくなります。

bash# データベース接続URL(例)
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"

# 環境
NODE_ENV="development"

実際の値は含めず、キー名と形式だけを示します。これはリポジトリにコミットして共有します。

Turborepo との統合

ビルドパフォーマンスをさらに向上させたい場合、Turborepo を導入することをおすすめします。まず、Turborepo をインストールします。

bash# Turborepo のインストール
yarn add -D -W turbo

-W オプションでルートのワークスペースにインストールします。

ルートディレクトリに turbo.json を作成し、パイプラインを定義します。

json{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    }
  }
}

dependsOn で依存関係を定義し、outputs でキャッシュ対象を指定します。

Prisma generate をパイプラインに追加します。

json{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build", "db:generate"],
      "outputs": ["dist/**"]
    },
    "db:generate": {
      "cache": false
    }
  }
}

db:generate タスクは cache: false にすることで、毎回実行されるようにします。これにより、スキーマ変更が確実に反映されます。

ルートの package.json にスクリプトを追加します。

json{
  "scripts": {
    "build": "turbo run build",
    "db:generate": "turbo run db:generate"
  }
}

これで yarn build を実行すると、Turborepo が依存関係を解析し、最適な順序でビルドを実行してくれます。

具体例

実際のプロジェクトを想定した、より実践的な例を見ていきましょう。

Next.js と Express API の統合例

フロントエンドに Next.js、バックエンドに Express を使用する構成を考えます。ディレクトリ構成は以下のようになります。

cssmonorepo-root/
├── package.json
├── turbo.json
└── packages/
    ├── database/
    │   ├── prisma/
    │   │   └── schema.prisma
    │   └── src/
    │       └── index.ts
    ├── api/
    │   ├── src/
    │   │   ├── index.ts
    │   │   └── routes/
    │   │       └── users.ts
    │   └── package.json
    └── web/
        ├── src/
        │   ├── app/
        │   │   └── page.tsx
        │   └── lib/
        │       └── api.ts
        └── package.json

この構成により、データベースアクセス層、API 層、UI 層を明確に分離できます。

Express API の実装

Express を使った API サーバーの実装例を示します。packages​/​api​/​src​/​routes​/​users.ts を作成しましょう。

typescript// 必要なモジュールのインポート
import { Router } from 'express';
import { prisma } from '@monorepo/database';

Express の Router と Prisma Client をインポートします。

ユーザー一覧取得エンドポイントの実装です。

typescript// ルーターの作成
export const usersRouter = Router();

// GET /users - ユーザー一覧取得
usersRouter.get('/', async (req, res) => {
  try {
    // Prisma でユーザーを取得
    const users = await prisma.user.findMany({
      select: {
        id: true,
        email: true,
        name: true,
        createdAt: true,
      },
    });

    // JSON レスポンスを返す
    res.json(users);
  } catch (error) {
    console.error('ユーザー取得エラー:', error);
    res
      .status(500)
      .json({ error: 'Internal Server Error' });
  }
});

select オプションで必要なフィールドだけを取得し、パフォーマンスを最適化しています。エラーハンドリングも適切に行っています。

ユーザー作成エンドポイントの実装です。

typescript// POST /users - ユーザー作成
usersRouter.post('/', async (req, res) => {
  try {
    const { email, name } = req.body;

    // バリデーション
    if (!email || typeof email !== 'string') {
      return res.status(400).json({
        error: 'Email is required and must be a string',
      });
    }

    // ユーザーを作成
    const user = await prisma.user.create({
      data: { email, name },
    });

    // 作成したユーザーを返す
    res.status(201).json(user);
  } catch (error) {
    console.error('ユーザー作成エラー:', error);
    res
      .status(500)
      .json({ error: 'Internal Server Error' });
  }
});

リクエストボディのバリデーションを行い、適切なステータスコードを返しています。

サーバーのメインファイル packages​/​api​/​src​/​index.ts を実装します。

typescript// Express アプリケーションのセットアップ
import express from 'express';
import { usersRouter } from './routes/users';

const app = express();
const PORT = process.env.PORT || 3001;

環境変数からポート番号を取得し、デフォルト値も設定しています。

ミドルウェアとルーターの設定です。

typescript// ミドルウェアの設定
app.use(express.json()); // JSON パース
app.use(express.urlencoded({ extended: true })); // URL エンコードされたボディをパース

// ルーターの登録
app.use('/api/users', usersRouter);

express.json() で JSON リクエストボディを自動的にパースするよう設定しています。

サーバーの起動処理です。

typescript// サーバー起動
app.listen(PORT, () => {
  console.log(
    `API サーバーが起動しました: http://localhost:${PORT}`
  );
});

これで API サーバーが完成しました。yarn workspace @monorepo​/​api dev で起動できます。

Next.js からの API 呼び出し

Next.js アプリケーションから API を呼び出す実装例を示します。packages​/​web​/​src​/​lib​/​api.ts を作成しましょう。

typescript// API クライアントの型定義
import type { User } from '@monorepo/database';

// API ベース URL
const API_BASE_URL =
  process.env.NEXT_PUBLIC_API_URL ||
  'http://localhost:3001';

環境変数で API の URL を設定できるようにしています。NEXT_PUBLIC_ プレフィックスにより、クライアントサイドでも使用可能になります。

ユーザー一覧取得関数の実装です。

typescript// ユーザー一覧を取得する関数
export async function getUsers(): Promise<User[]> {
  // API リクエストを送信
  const response = await fetch(
    `${API_BASE_URL}/api/users`,
    {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
      },
    }
  );

  // レスポンスチェック
  if (!response.ok) {
    throw new Error(
      `HTTP error! status: ${response.status}`
    );
  }

  // JSON をパース
  const users = await response.json();
  return users;
}

エラーハンドリングを適切に行い、HTTP ステータスコードもチェックしています。

ユーザー作成関数も実装しましょう。

typescript// ユーザーを作成する関数
export async function createUser(
  email: string,
  name: string
): Promise<User> {
  // API リクエストを送信
  const response = await fetch(
    `${API_BASE_URL}/api/users`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ email, name }),
    }
  );

  // レスポンスチェック
  if (!response.ok) {
    const error = await response.json();
    throw new Error(
      error.message || 'Failed to create user'
    );
  }

  // 作成されたユーザーを返す
  const user = await response.json();
  return user;
}

リクエストボディを JSON 文字列に変換し、適切なヘッダーを設定しています。

Next.js の Server Component で使用する例です。packages​/​web​/​src​/​app​/​page.tsx を作成します。

typescript// ユーザー一覧ページ
import { getUsers } from '../lib/api';

export default async function Home() {
  // サーバーサイドでユーザーを取得
  const users = await getUsers();

  return (
    <main>
      <h1>ユーザー一覧</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            {user.name} ({user.email})
          </li>
        ))}
      </ul>
    </main>
  );
}

Next.js 13+ の Server Component を使用することで、サーバーサイドでデータを取得し、ハイドレーション済みの HTML を返せます。

以下の図は、リクエストの流れを示しています。

mermaidsequenceDiagram
  participant Browser as ブラウザ
  participant Next as Next.js<br/>(web)
  participant API as Express API<br/>(api)
  participant Prisma as Prisma Client<br/>(database)
  participant DB as PostgreSQL

  Browser->>Next: ページアクセス
  Next->>API: GET /api/users
  API->>Prisma: prisma.user.findMany()
  Prisma->>DB: SQL クエリ実行
  DB-->>Prisma: ユーザーデータ
  Prisma-->>API: User[]
  API-->>Next: JSON レスポンス
  Next-->>Browser: HTML レンダリング

データの流れが明確になり、各層の責務が分離されています。これにより、保守性とテスタビリティが向上します。

マイグレーションの運用

スキーマ変更を本番環境に適用する際の手順を見ていきましょう。開発環境でのマイグレーション作成方法です。

bash# スキーマを編集した後、マイグレーションを作成
yarn workspace @monorepo/database prisma migrate dev --name add_user_profile

--name オプションでマイグレーションに分かりやすい名前を付けます。これにより、packages​/​database​/​prisma​/​migrations​/​ ディレクトリにマイグレーションファイルが生成されます。

生成されたマイグレーションファイルの内容を確認します。

sql-- CreateTable
CREATE TABLE "Profile" (
  "id" SERIAL NOT NULL,
  "bio" TEXT,
  "userId" INTEGER NOT NULL,

  CONSTRAINT "Profile_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "Profile_userId_key" ON "Profile"("userId");

-- AddForeignKey
ALTER TABLE "Profile" ADD CONSTRAINT "Profile_userId_fkey"
  FOREIGN KEY ("userId") REFERENCES "User"("id")
  ON DELETE RESTRICT ON UPDATE CASCADE;

Prisma が自動生成した SQL を確認し、意図した通りになっているかチェックします。

本番環境へのデプロイ時は、以下のコマンドを使用します。

bash# 本番環境でマイグレーションを実行
yarn workspace @monorepo/database prisma migrate deploy

migrate deploy は本番環境用のコマンドで、未適用のマイグレーションのみを実行します。開発用の機能(スキーマのリセットなど)は含まれていないため、安全に実行できます。

CI/CD パイプラインの設定

GitHub Actions を使った CI/CD の設定例を示します。.github​/​workflows​/​ci.yml を作成しましょう。

yaml# ワークフロー名
name: CI

# トリガー設定
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

main ブランチと develop ブランチへのプッシュ、プルリクエスト作成時にワークフローが実行されます。

ジョブの基本設定です。

yamljobs:
  test:
    runs-on: ubuntu-latest

    # PostgreSQL サービスコンテナ
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: test_db
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

PostgreSQL をサービスコンテナとして起動し、ヘルスチェックも設定しています。

ステップの定義です。

yamlsteps:
  # リポジトリをチェックアウト
  - uses: actions/checkout@v4

  # Node.js のセットアップ
  - uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'yarn'

  # 依存関係のインストール
  - name: Install dependencies
    run: yarn install --frozen-lockfile

--frozen-lockfile オプションで、yarn.lock の内容を厳密に再現します。

Prisma の準備とマイグレーション実行です。

yaml# 環境変数の設定
- name: Setup environment
  run: |
    echo "DATABASE_URL=postgresql://postgres:postgres@localhost:5432/test_db" >> $GITHUB_ENV

# Prisma Client の生成
- name: Generate Prisma Client
  run: yarn db:generate

# マイグレーションの実行
- name: Run migrations
  run: yarn workspace @monorepo/database prisma migrate deploy

環境変数を設定し、Prisma Client を生成してからマイグレーションを実行します。

ビルドとテストの実行です。

yaml# ビルド実行
- name: Build
  run: yarn build

# テスト実行(オプション)
- name: Run tests
  run: yarn test

すべてのパッケージをビルドし、テストを実行します。Turborepo を使用している場合、キャッシュが効いて高速に実行されます。

まとめ

Monorepo 環境で Prisma を効果的に活用するためには、パス解決、generate の共有、依存関係の管理という 3 つの重要な要素を適切に設定する必要があります。本記事で紹介した手法を実践することで、以下のメリットが得られます。

まず、パス解決の最適化により、各パッケージから Prisma Client を一貫した方法でインポートできるようになります。@monorepo​/​database のような明確な名前空間を使用することで、コードの可読性と保守性が向上します。

次に、generate コマンドの実行タイミングを戦略的に管理することで、開発体験が大きく改善されます。postinstall スクリプトを活用すれば、チームメンバー全員が常に最新の Prisma Client を使用でき、「generate を忘れた」というミスを防げます。

さらに、依存関係を適切に宣言し、バージョンを統一することで、予期しないエラーを回避できます。Yarn Workspaces や Turborepo といったツールと組み合わせることで、ビルドパフォーマンスも最適化されます。

実際の開発現場では、Next.js と Express API の組み合わせのように、複数の技術スタックを Monorepo で管理するケースが増えています。database パッケージを中心に据えることで、フロントエンドとバックエンドで型情報を共有し、エンドツーエンドでの型安全性を実現できます。

マイグレーション戦略も重要なポイントです。開発環境では prisma migrate dev を、本番環境では prisma migrate deploy を使い分けることで、安全かつ確実にスキーマ変更を適用できます。CI/CD パイプラインに組み込むことで、デプロイの自動化も実現できます。

Monorepo と Prisma の組み合わせは、最初の設定こそ少し複雑に感じるかもしれませんが、一度構築してしまえば、開発効率が大きく向上します。型安全性、コードの再利用性、保守性のすべてを高いレベルで実現できるのです。

本記事で紹介した設定やコード例を参考に、ぜひ皆さんのプロジェクトでも Monorepo × Prisma の環境を構築してみてください。快適な開発体験を手に入れることができるはずです。

関連リンク