T-CREATOR

Monorepo で始める Convex:Turborepo/PNPM 連携セットアップ実践ガイド

Monorepo で始める Convex:Turborepo/PNPM 連携セットアップ実践ガイド

近年、フロントエンドとバックエンドを一つのリポジトリで管理する Monorepo アーキテクチャが注目を集めています。 その中でも、リアルタイムバックエンドプラットフォームの Convex と、高速な Monorepo ツールである Turborepo、そして効率的なパッケージマネージャーの PNPM を組み合わせることで、開発体験を飛躍的に向上させることができるんです。

本記事では、Turborepo と PNPM を使った Monorepo 環境で Convex をセットアップする実践的な手順を、初心者の方にもわかりやすく解説していきますね。 実際のコード例とともに、躓きやすいポイントやエラー対処法まで詳しくご紹介します。

背景

Monorepo とは何か

Monorepo(モノレポ)は、複数のプロジェクトやパッケージを一つのリポジトリで管理する開発手法です。 Google や Microsoft などの大企業でも採用されており、コードの共有や依存関係の管理が容易になります。

従来の複数リポジトリ(Polyrepo)では、共通のコンポーネントやユーティリティを別々のリポジトリで管理する必要がありました。 しかし Monorepo なら、すべてのコードを一箇所で管理できるため、変更の影響範囲が把握しやすく、リファクタリングもスムーズに行えるんですね。

以下の図は、Monorepo の基本的な構造を示しています。

mermaidflowchart TB
    root["Monorepo ルート"]
    apps["apps/<br/>(アプリケーション群)"]
    packages["packages/<br/>(共有パッケージ群)"]

    web["apps/web<br/>(Next.js アプリ)"]
    mobile["apps/mobile<br/>(React Native アプリ)"]

    ui["packages/ui<br/>(共有 UI コンポーネント)"]
    utils["packages/utils<br/>(共通ユーティリティ)"]
    convex["packages/convex<br/>(Convex バックエンド)"]

    root --> apps
    root --> packages
    apps --> web
    apps --> mobile
    packages --> ui
    packages --> utils
    packages --> convex

    web -.->|依存| ui
    web -.->|依存| utils
    web -.->|依存| convex
    mobile -.->|依存| ui

この図から、アプリケーションが共有パッケージに依存している構造が理解できますね。

Turborepo の特徴

Turborepo は Vercel が開発した高速な Monorepo ビルドシステムです。 主な特徴として、インテリジェントなキャッシング機能、並列タスク実行、依存関係の自動解決があります。

従来の Monorepo ツールと比べて、ビルド速度が圧倒的に速いのが魅力です。 一度ビルドしたパッケージの結果をキャッシュし、変更がない部分は再ビルドをスキップすることで、開発効率を大幅に向上させてくれるんですね。

PNPM の利点

PNPM は、npm や Yarn と比較して高速かつディスク容量を節約できるパッケージマネージャーです。 ハードリンクを使って node_modules を管理するため、同じパッケージを複数のプロジェクトで共有できます。

Monorepo 環境では、ワークスペース機能を使って複数のパッケージ間の依存関係を効率的に管理できるんです。 また、厳格な依存関係管理により、予期しないパッケージへのアクセスを防いでくれるのも安心ですね。

Convex とは何か

Convex は、リアルタイムデータベースとサーバーレス関数を統合したバックエンドプラットフォームです。 TypeScript で書いたバックエンドコードが自動的にデプロイされ、リアルタイム同期機能を簡単に実装できます。

従来の REST API や GraphQL と比べて、データの同期が自動的に行われるため、複雑な状態管理が不要になるんですね。 また、型安全性が保証されており、フロントエンドとバックエンドの連携がスムーズに行えます。

課題

従来の開発環境の問題点

従来の開発環境では、いくつかの課題がありました。

フロントエンドとバックエンドの分離による複雑さ

別々のリポジトリで管理していると、型定義の同期が難しく、API の変更時に手動での調整が必要でした。 バージョン管理も煩雑になり、デプロイのタイミングを合わせるのも一苦労でしたね。

パッケージ管理の非効率性

npm や Yarn を使った従来のパッケージ管理では、各プロジェクトごとに node_modules が作成されるため、ディスク容量を大量に消費していました。 同じパッケージが何度もインストールされるのは、明らかに無駄ですよね。

ビルド時間の長さ

大規模なプロジェクトでは、ビルド時間が長くなる傾向がありました。 変更していない部分まで毎回ビルドされるため、開発サイクルが遅くなってしまうんです。

Monorepo 導入時の課題

Monorepo を導入する際にも、新たな課題が生まれます。

適切なツール選択の難しさ

Lerna、Nx、Rush など、さまざまな Monorepo ツールが存在します。 それぞれに特徴があり、プロジェクトに最適なツールを選ぶのは簡単ではありませんでした。

セットアップの複雑さ

Monorepo の初期セットアップは、通常のプロジェクトよりも複雑です。 ワークスペースの設定、依存関係の管理、ビルドパイプラインの構築など、考慮すべき点が多いんですね。

バックエンドとの統合

フロントエンドの Monorepo 環境にバックエンドを統合する際、どのように構成すべきか悩むことが多いでしょう。 特にリアルタイム機能を持つバックエンドの統合は、さらに複雑になります。

解決策

Turborepo + PNPM + Convex の組み合わせ

これらの課題を解決するのが、Turborepo、PNPM、そして Convex の組み合わせです。

統合的な開発体験

Turborepo によって高速なビルドとキャッシングが実現し、PNPM で効率的なパッケージ管理が可能になります。 さらに Convex をバックエンドとして統合することで、型安全なフルスタック開発が一つのリポジトリで完結するんです。

リアルタイム機能の簡単な実装

Convex を使えば、WebSocket の設定やデータ同期のロジックを自分で書く必要がありません。 クエリを定義するだけで、自動的にリアルタイム同期が機能してくれるんですね。

開発効率の向上

コードの変更が即座に反映され、ビルド時間も最小限に抑えられます。 型定義も自動的に共有されるため、フロントエンドとバックエンドの連携がスムーズになるでしょう。

以下の図は、Turborepo、PNPM、Convex がどのように連携するかを示しています。

mermaidflowchart LR
    dev["開発者"]
    pnpm["PNPM<br/>(パッケージ管理)"]
    turbo["Turborepo<br/>(ビルド・タスク実行)"]

    frontend["Next.js アプリ"]
    convex_pkg["Convex パッケージ"]
    shared["共有パッケージ"]

    convex_cloud["Convex Cloud<br/>(デプロイ先)"]

    dev -->|"pnpm install"| pnpm
    dev -->|"turbo dev"| turbo

    pnpm -->|依存解決| frontend
    pnpm -->|依存解決| convex_pkg
    pnpm -->|依存解決| shared

    turbo -->|並列実行| frontend
    turbo -->|並列実行| convex_pkg

    frontend -.->|型安全な呼び出し| convex_pkg
    convex_pkg -->|自動デプロイ| convex_cloud

    convex_cloud -->|リアルタイム同期| frontend

図で理解できる要点:

  • PNPM がすべてのパッケージの依存関係を管理
  • Turborepo が並列でタスクを実行し、ビルド時間を短縮
  • Convex がクラウドに自動デプロイされ、フロントエンドとリアルタイム同期

アーキテクチャの設計方針

効果的な Monorepo を構築するには、明確な設計方針が重要です。

パッケージの分離

アプリケーション(apps)と共有パッケージ(packages)を明確に分離します。 この構造により、コードの再利用性が高まり、依存関係も整理されるんですね。

型定義の共有

Convex のスキーマ定義を共有パッケージとして管理することで、フロントエンドとバックエンドで同じ型を使用できます。 これにより、型の不一致によるバグを防げるでしょう。

段階的な移行

既存のプロジェクトを Monorepo に移行する場合は、段階的に進めることをお勧めします。 まず基本的な構造を作り、徐々にパッケージを追加していく方法が安全ですね。

具体例

環境のセットアップ

それでは、実際に Turborepo + PNPM + Convex の環境を構築していきましょう。

前提条件の確認

以下のツールがインストールされている必要があります。

#ツール推奨バージョン確認コマンド
1Node.js18.x 以上node --version
2PNPM8.x 以上pnpm --version
3Git最新版git --version

PNPM がインストールされていない場合は、以下のコマンドでインストールできます。

bash# PNPM のグローバルインストール
npm install -g pnpm

# インストール確認
pnpm --version

プロジェクトの初期化

まず、Turborepo のテンプレートを使ってプロジェクトを作成します。

bash# Turborepo プロジェクトの作成
# --package-manager オプションで PNPM を指定
pnpm dlx create-turbo@latest my-convex-app

# プロジェクトディレクトリに移動
cd my-convex-app

このコマンドを実行すると、対話形式でいくつかの質問が表示されます。 パッケージマネージャーには PNPM を選択してくださいね。

プロジェクト構造の確認

作成されたプロジェクトの構造を確認しましょう。

bash# ディレクトリ構造の表示
tree -L 2 -I 'node_modules'

以下のような構造が生成されているはずです。

perlmy-convex-app/
├── apps/
│   ├── web/          # Next.js アプリケーション
│   └── docs/         # ドキュメントサイト
├── packages/
│   ├── ui/           # 共有 UI コンポーネント
│   ├── eslint-config/# ESLint 設定
│   └── typescript-config/ # TypeScript 設定
├── package.json
├── pnpm-workspace.yaml
└── turbo.json

この構造により、アプリケーションと共有パッケージが明確に分離されています。

PNPM ワークスペースの設定

PNPM ワークスペースの設定を確認・調整します。

pnpm-workspace.yaml の確認

ルートディレクトリの pnpm-workspace.yaml ファイルを開いてください。

yaml# pnpm-workspace.yaml
# ワークスペースのパッケージ場所を定義
packages:
  - 'apps/*' # apps ディレクトリ配下のすべてのパッケージ
  - 'packages/*' # packages ディレクトリ配下のすべてのパッケージ

この設定により、PNPM は指定されたディレクトリをワークスペースとして認識します。

ルート package.json の設定

ルートの package.json を開き、必要なスクリプトを確認しましょう。

json{
  "name": "my-convex-app",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "build": "turbo build",
    "dev": "turbo dev",
    "lint": "turbo lint",
    "format": "prettier --write \"**/*.{ts,tsx,md}\""
  }
}

これらのスクリプトは、Turborepo を通じてすべてのパッケージに対してタスクを実行します。

Convex のセットアップ

次に、Convex をプロジェクトに統合していきます。

Convex パッケージの作成

packages ディレクトリに Convex 用のパッケージを作成しましょう。

bash# packages ディレクトリに移動
cd packages

# Convex パッケージディレクトリを作成
mkdir convex-backend && cd convex-backend

Convex の初期化

Convex CLI を使って初期化します。

bash# Convex のインストールと初期化
pnpm add convex

# Convex プロジェクトの初期化
pnpm convex dev --once

このコマンドを実行すると、ブラウザが開いて Convex のセットアップページが表示されます。 サインインまたはアカウント作成を行ってくださいね。

エラーが発生した場合

以下のエラーが表示されることがあります。

perlError: ENOENT: no such file or directory, open 'convex.json'

エラーコード: ENOENT

発生条件: Convex が初期化されていないディレクトリで実行した場合

解決方法:

  1. 正しいディレクトリにいることを確認する
  2. pnpm convex dev コマンドを再実行する
  3. ブラウザでのセットアップを完了させる

package.json の作成

packages​/​convex-backend​/​package.json を作成します。

json{
  "name": "@my-app/convex-backend",
  "version": "0.0.1",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "convex dev",
    "build": "convex deploy --cmd 'echo Build complete'",
    "typecheck": "tsc --noEmit"
  }
}

パッケージ名に @my-app​/​ というスコープを付けることで、Monorepo 内での名前空間が明確になります。

Convex スキーマの定義

packages​/​convex-backend​/​convex​/​schema.ts を作成し、データベーススキーマを定義しましょう。

typescript// convex/schema.ts
// Convex のスキーマ定義
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
typescript// スキーマの定義
// テーブルとフィールドの型を指定
export default defineSchema({
  // タスクテーブルの定義
  tasks: defineTable({
    text: v.string(), // タスクのテキスト
    isCompleted: v.boolean(), // 完了状態
    createdAt: v.number(), // 作成日時(タイムスタンプ)
  }),
});

この定義により、Convex が自動的に型定義を生成してくれます。

Convex 関数の作成

次に、データを操作するための関数を作成します。

typescript// convex/tasks.ts
// タスクに関する Convex 関数
import { query, mutation } from './_generated/server';
import { v } from 'convex/values';
typescript// すべてのタスクを取得するクエリ
export const getTasks = query({
  // 引数なし
  args: {},
  // ハンドラー関数
  handler: async (ctx) => {
    // 作成日時の降順でタスクを取得
    return await ctx.db
      .query('tasks')
      .order('desc')
      .collect();
  },
});
typescript// 新しいタスクを作成するミューテーション
export const createTask = mutation({
  // 引数の型定義
  args: {
    text: v.string(),
  },
  // ハンドラー関数
  handler: async (ctx, args) => {
    // データベースに新しいタスクを挿入
    const taskId = await ctx.db.insert('tasks', {
      text: args.text,
      isCompleted: false,
      createdAt: Date.now(),
    });
    return taskId;
  },
});
typescript// タスクの完了状態を切り替えるミューテーション
export const toggleTask = mutation({
  // 引数の型定義
  args: {
    id: v.id('tasks'),
  },
  // ハンドラー関数
  handler: async (ctx, args) => {
    // タスクを取得
    const task = await ctx.db.get(args.id);
    if (!task) {
      throw new Error('Task not found');
    }

    // 完了状態を反転して更新
    await ctx.db.patch(args.id, {
      isCompleted: !task.isCompleted,
    });
  },
});

これらの関数は、型安全な方法でデータベース操作を行います。

フロントエンドとの連携

続いて、Next.js アプリケーションから Convex を利用する設定を行いましょう。

Convex クライアントのインストール

Next.js アプリケーションに Convex クライアントをインストールします。

bash# apps/web ディレクトリに移動
cd ../../apps/web

# Convex React クライアントのインストール
pnpm add convex

Convex プロバイダーの設定

apps​/​web​/​app​/​layout.tsx を編集し、Convex プロバイダーを追加します。

typescript// app/layout.tsx
// アプリケーションのルートレイアウト
'use client';

import {
  ConvexProvider,
  ConvexReactClient,
} from 'convex/react';
typescript// Convex クライアントの初期化
// 環境変数から Convex URL を取得
const convex = new ConvexReactClient(
  process.env.NEXT_PUBLIC_CONVEX_URL as string
);
typescript// ルートレイアウトコンポーネント
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='ja'>
      <body>
        {/* Convex プロバイダーでラップ */}
        <ConvexProvider client={convex}>
          {children}
        </ConvexProvider>
      </body>
    </html>
  );
}

この設定により、アプリケーション全体で Convex を使用できるようになります。

環境変数の設定

apps​/​web​/​.env.local ファイルを作成し、Convex の URL を設定しましょう。

bash# Convex の開発環境 URL を設定
# この値は convex dev 実行時にコンソールに表示されます
NEXT_PUBLIC_CONVEX_URL=https://your-project.convex.cloud

実際の URL は、packages​/​convex-backendpnpm convex dev を実行した際に表示される値を使用してください。

重要な注意点

Next.js でクライアントサイドから使用する環境変数には、NEXT_PUBLIC_ プレフィックスが必要です。 これがないと、ブラウザからアクセスできないので注意してくださいね。

タスク一覧コンポーネントの作成

Convex からデータを取得して表示するコンポーネントを作成します。

typescript// app/components/TaskList.tsx
'use client';

import { useQuery, useMutation } from 'convex/react';
import { api } from '@my-app/convex-backend';
typescript// タスク一覧コンポーネント
export function TaskList() {
  // タスク一覧を取得(リアルタイム同期)
  const tasks = useQuery(api.tasks.getTasks);

  // タスク作成ミューテーション
  const createTask = useMutation(api.tasks.createTask);

  // タスク切り替えミューテーション
  const toggleTask = useMutation(api.tasks.toggleTask);
typescript// 新しいタスクを追加する関数
const handleAddTask = async (text: string) => {
  try {
    await createTask({ text });
  } catch (error) {
    console.error('タスクの作成に失敗しました:', error);
  }
};
typescript// タスクの完了状態を切り替える関数
const handleToggle = async (id: string) => {
  try {
    await toggleTask({ id });
  } catch (error) {
    console.error('タスクの更新に失敗しました:', error);
  }
};
typescript  // ローディング中の表示
  if (!tasks) {
    return <div>読み込み中...</div>;
  }

  return (
    <div>
      <h2>タスク一覧</h2>
      <ul>
        {tasks.map((task) => (
          <li key={task._id}>
            <input
              type="checkbox"
              checked={task.isCompleted}
              onChange={() => handleToggle(task._id)}
            />
            <span>{task.text}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

このコンポーネントは、Convex からリアルタイムでデータを取得し、自動的に UI を更新します。

Turborepo の設定

最後に、Turborepo の設定を最適化しましょう。

turbo.json の設定

ルートディレクトリの turbo.json を編集します。

json{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    }
  }
}
json{
  "pipeline": {
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "typecheck": {
      "dependsOn": ["^typecheck"]
    }
  }
}

設定のポイント

  • dependsOn: ["^build"] - 依存パッケージを先にビルド
  • outputs - キャッシュ対象のディレクトリを指定
  • cache: false - 開発モードではキャッシュを無効化
  • persistent: true - 開発サーバーを永続的に実行

パッケージ間の依存関係設定

apps​/​web​/​package.json に Convex パッケージへの依存を追加します。

json{
  "name": "@my-app/web",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "@my-app/convex-backend": "workspace:*",
    "convex": "latest",
    "next": "14.x",
    "react": "18.x",
    "react-dom": "18.x"
  }
}

workspace:* を指定することで、Monorepo 内のパッケージを参照できます。

開発環境の起動

すべての設定が完了したので、開発環境を起動しましょう。

依存関係のインストール

まず、すべてのパッケージの依存関係をインストールします。

bash# ルートディレクトリで実行
pnpm install

PNPM は、ワークスペース内のすべてのパッケージの依存関係を効率的にインストールしてくれます。

開発サーバーの起動

Turborepo を使って、すべてのパッケージの開発サーバーを並列で起動します。

bash# すべてのパッケージの dev スクリプトを実行
pnpm dev

このコマンドを実行すると、以下が並列で起動します。

#パッケージポート説明
1apps/web3000Next.js アプリケーション
2packages/convex-backend-Convex 開発サーバー

ブラウザで http:​/​​/​localhost:3000 を開くと、アプリケーションが表示されるはずです。

プロジェクト構造の全体像

最終的なプロジェクト構造を図で確認しましょう。

mermaidflowchart TB
    root["my-convex-app/"]

    subgraph apps ["apps/"]
        web["web/<br/>(Next.js)"]
        docs["docs/<br/>(ドキュメント)"]
    end

    subgraph packages ["packages/"]
        convex["convex-backend/<br/>(Convex)"]
        ui["ui/<br/>(UI コンポーネント)"]
        tsconfig["typescript-config/<br/>(TS 設定)"]
    end

    subgraph config ["設定ファイル"]
        turbo["turbo.json<br/>(Turborepo 設定)"]
        pnpm_ws["pnpm-workspace.yaml<br/>(PNPM ワークスペース)"]
        pkg["package.json<br/>(ルート設定)"]
    end

    root --> apps
    root --> packages
    root --> config

    web -.->|依存| convex
    web -.->|依存| ui
    web -.->|依存| tsconfig

    style convex fill:#f9f,stroke:#333
    style web fill:#9ff,stroke:#333

図で理解できる要点:

  • apps と packages の明確な分離
  • web アプリが複数のパッケージに依存
  • ルートに設定ファイルを集約

この構造により、拡張性が高く保守しやすいプロジェクトが実現できます。

よくあるエラーと対処法

実際の開発で遭遇しやすいエラーと解決方法をご紹介しますね。

エラー 1: モジュールが見つからない

arduinoError: Cannot find module '@my-app/convex-backend'

エラーコード: MODULE_NOT_FOUND

発生条件: パッケージ間の依存関係が正しく設定されていない場合

解決方法:

  1. pnpm install をルートディレクトリで実行
  2. apps​/​web​/​package.json に依存関係が記載されているか確認
  3. パッケージ名のスペルミスがないか確認
  4. pnpm-workspace.yaml に正しいパスが設定されているか確認

エラー 2: Convex URL が未定義

javascriptTypeError: Cannot read property 'https' of undefined

エラーコード: TypeError

発生条件: NEXT_PUBLIC_CONVEX_URL 環境変数が設定されていない場合

解決方法:

  1. .env.local ファイルが存在するか確認
  2. 環境変数名に NEXT_PUBLIC_ プレフィックスがあるか確認
  3. 開発サーバーを再起動(環境変数の変更は再起動が必要)
  4. Convex ダッシュボードから正しい URL をコピー

エラー 3: Turborepo キャッシュエラー

vbnetError: Turborepo failed to read cache

エラーコード: CACHE_READ_ERROR

発生条件: Turborepo のキャッシュが破損している場合

解決方法:

  1. キャッシュをクリア: pnpm turbo daemon stop
  2. .turbo ディレクトリを削除: rm -rf .turbo
  3. 再度ビルド: pnpm build

デプロイの準備

開発環境が整ったら、本番環境へのデプロイを準備しましょう。

Convex のデプロイ

Convex を本番環境にデプロイします。

bash# packages/convex-backend ディレクトリで実行
cd packages/convex-backend

# 本番環境にデプロイ
pnpm convex deploy

デプロイが成功すると、本番環境の URL が表示されます。 この URL を Next.js の本番環境変数として設定してくださいね。

Next.js のビルド

Next.js アプリケーションをビルドします。

bash# ルートディレクトリで実行
pnpm build

Turborepo が依存関係を解決し、正しい順序でビルドを実行してくれます。

環境変数の設定

Vercel などのホスティングサービスに、以下の環境変数を設定します。

#変数名説明
1NEXT_PUBLIC_CONVEX_URL本番 Convex URLConvex の本番環境 URL

本番環境の URL は、pnpm convex deploy 実行時に表示される値を使用してください。

まとめ

本記事では、Turborepo と PNPM を使った Monorepo 環境で Convex をセットアップする方法を詳しく解説しました。

この構成の主なメリット

Turborepo によって、ビルド時間が大幅に短縮され、開発効率が向上します。 PNPM の効率的なパッケージ管理により、ディスク容量の節約とインストール速度の向上が実現できるんですね。 そして Convex を統合することで、型安全なフルスタック開発とリアルタイム機能が簡単に実装できるようになりました。

実装のポイント

プロジェクト構造を適切に設計し、アプリケーションと共有パッケージを明確に分離することが重要です。 パッケージ間の依存関係を workspace:* で管理し、Turborepo のキャッシング機能を活用することで、快適な開発体験が得られます。 また、環境変数の管理や型定義の共有にも注意を払うことで、バグの少ない堅牢なアプリケーションが構築できるでしょう。

今後の発展

この基本的なセットアップをベースに、認証機能の追加、複数のアプリケーションの追加、CI/CD パイプラインの構築など、さらに発展させることができます。 Monorepo のメリットを最大限に活かして、大規模なプロジェクトでも保守性の高い開発を続けていけますね。

Turborepo、PNPM、Convex の組み合わせは、モダンなフルスタック開発に最適な選択肢の一つです。 ぜひこのガイドを参考に、効率的な開発環境を構築してみてください。

関連リンク