T-CREATOR

NestJS Monorepo 構築:Nx/Yarn Workspaces で API・Lib を一元管理

NestJS Monorepo 構築:Nx/Yarn Workspaces で API・Lib を一元管理

大規模な NestJS プロジェクトを複数チームで開発する際、コードの重複や依存関係の管理に悩むことはありませんか?そんな課題を解決するのが Monorepo(モノレポ)構成です。本記事では、Nx と Yarn Workspaces を使って、API アプリケーションと共通ライブラリを一元管理する実践的な方法を解説します。初めて Monorepo に触れる方でも理解できるよう、段階的に構築手順を紹介していきますので、ぜひ最後までお読みください。

Monorepo とは何か

Monorepo(モノレポ)は、複数のプロジェクトやパッケージを単一のリポジトリで管理する開発手法です。

従来の方法では、API サーバー、管理画面、モバイルアプリなどをそれぞれ別のリポジトリで管理していました。しかし、この方法では共通コードの重複や、バージョン管理の複雑さが課題となります。

Monorepo を採用すると、以下のような構造で複数のアプリケーションとライブラリを一つのリポジトリにまとめられます。

mermaidflowchart TD
  repo["Monorepo<br/>ルート"]
  apps["apps/<br/>アプリケーション群"]
  libs["libs/<br/>共通ライブラリ群"]

  repo --> apps
  repo --> libs

  apps --> api["api/<br/>メイン API"]
  apps --> admin["admin-api/<br/>管理者 API"]
  apps --> batch["batch/<br/>バッチ処理"]

  libs --> auth["auth/<br/>認証ライブラリ"]
  libs --> db["database/<br/>DB アクセス"]
  libs --> util["util/<br/>共通ユーティリティ"]

  api -.->|使用| auth
  api -.->|使用| db
  admin -.->|使用| auth
  admin -.->|使用| db
  batch -.->|使用| util

図で理解できる要点

  • apps フォルダには実行可能なアプリケーションを配置
  • libs フォルダには共通ライブラリを配置
  • 各アプリは必要なライブラリを参照して開発

Monorepo では、共通ロジックをライブラリとして切り出し、複数のアプリケーションから参照できます。これにより、コードの重複を減らし、変更の影響範囲を最小限に抑えられるのです。

Monorepo のメリット

Monorepo を採用すると、開発効率が大幅に向上します。

まず、コードの共有が容易になります。認証ロジックやデータベースアクセス層などを一度実装すれば、すべてのアプリケーションで再利用可能です。

また、リファクタリングの影響範囲を把握しやすくなります。共通ライブラリを変更した際、どのアプリケーションに影響があるかを即座に確認できるでしょう。

さらに、バージョン管理が統一されるため、依存パッケージの競合や互換性問題が減少します。

mermaidflowchart LR
  change["ライブラリ変更"]
  test["一括テスト実行"]
  affected["影響範囲検出"]
  deploy["安全なデプロイ"]

  change --> test
  test --> affected
  affected --> deploy

  style change fill:#e1f5ff
  style deploy fill:#d4edda

図で理解できる要点

  • 変更は一箇所で行い、影響範囲を自動検出
  • CI/CD で一括テストが可能
  • デプロイ前にすべての依存関係を検証

Monorepo 管理の課題

一方で、Monorepo には特有の課題も存在します。

最も大きな課題は、リポジトリのサイズ増大です。複数のプロジェクトが含まれるため、クローンやビルドに時間がかかる場合があります。

また、依存関係の管理が複雑化します。どのアプリケーションがどのライブラリを使用しているか、明確に把握する必要があるでしょう。

さらに、ビルドやテストの実行範囲を最適化しないと、CI/CD のパフォーマンスが低下します。変更のないコードまで毎回ビルドしてしまうのは非効率ですね。

#課題影響対策
1リポジトリサイズ増大クローン・ビルド時間の増加増分ビルド、キャッシュ活用
2依存関係の複雑化影響範囲の把握困難依存関係グラフの可視化
3CI/CD のパフォーマンステスト・デプロイの遅延変更検出による選択的実行
4チーム間のコード衝突マージコンフリクト増加適切なディレクトリ分割

これらの課題を解決するために、Nx や Yarn Workspaces といった専門ツールが登場しました。

Nx と Yarn Workspaces による解決策

Nx と Yarn Workspaces を組み合わせることで、Monorepo の課題を効率的に解決できます。

Yarn Workspaces の役割

Yarn Workspaces は、パッケージ管理を一元化するツールです。

複数のパッケージの node_modules を統合し、ディスク容量を節約します。また、ローカルパッケージ間の依存関係を自動的に解決してくれるのです。

mermaidflowchart TB
  root["ルート<br/>node_modules"]

  subgraph workspace ["Yarn Workspaces"]
    pkg1["apps/api<br/>package.json"]
    pkg2["apps/admin<br/>package.json"]
    pkg3["libs/auth<br/>package.json"]
  end

  pkg1 -.->|依存解決| root
  pkg2 -.->|依存解決| root
  pkg3 -.->|依存解決| root

  pkg1 -->|参照| pkg3
  pkg2 -->|参照| pkg3

図で理解できる要点

  • すべてのパッケージが共通の node_modules を使用
  • ローカルパッケージ間の依存を自動解決
  • インストール時間とディスク容量を大幅削減

Nx の役割

Nx は、Monorepo 専用の開発ツールです。

変更されたコードだけをビルド・テストする「増分ビルド」機能を提供します。これにより、大規模プロジェクトでも高速な開発サイクルを維持できるでしょう。

また、依存関係グラフを自動生成し、視覚的に把握できます。コード生成機能も充実しており、新しいアプリやライブラリを素早く追加可能です。

#機能説明メリット
1増分ビルド変更部分のみビルドビルド時間 50-90% 削減
2依存関係グラフ視覚的な依存関係表示影響範囲の即座把握
3コード生成テンプレートからの自動生成開発速度向上
4キャッシュ機能ローカル・リモートキャッシュ再ビルド時間ほぼゼロ
5並列実行タスクの並列処理CI/CD 時間短縮

Yarn Workspaces がパッケージ管理を担当し、Nx がビルド最適化と開発体験を向上させるという役割分担になります。

具体例:NestJS Monorepo の構築手順

それでは、実際に NestJS Monorepo を構築していきましょう。ここでは、メイン API、管理者 API、認証ライブラリを含む構成を作成します。

ステップ 1:Nx Workspace の初期化

まず、Nx を使って新しい Workspace を作成します。

bash# Nx の最新版をグローバルインストール
yarn global add nx

# NestJS プリセットで Workspace を作成
npx create-nx-workspace@latest my-nestjs-monorepo \
  --preset=nest \
  --packageManager=yarn

コマンドを実行すると、いくつかの質問が表示されます。

  • Application name: api(最初のアプリケーション名)
  • Use Nx Cloud: No(後で設定可能)

これで基本的な Workspace が作成され、最初の NestJS アプリケーション api が生成されます。

ステップ 2:Yarn Workspaces の設定

次に、Yarn Workspaces を有効化します。

ルートの package.json を確認しましょう。

json{
  "name": "my-nestjs-monorepo",
  "version": "1.0.0",
  "private": true,
  "workspaces": ["apps/*", "libs/*"]
}

コードの説明

  • private: true により、このパッケージ自体を npm に公開しないよう設定
  • workspaces 配列で、アプリケーションとライブラリのディレクトリを指定
  • ワイルドカード * により、配下のすべてのパッケージを自動認識

これで、appslibs 配下のすべてのパッケージが Yarn Workspaces の管理対象になります。

ステップ 3:共通ライブラリの作成

認証機能を共通ライブラリとして作成します。

bash# 認証ライブラリを生成
nx generate @nx/nest:library auth \
  --directory=libs/auth \
  --buildable \
  --publishable=false

コマンドオプションの説明

  • @nx​/​nest:library: NestJS 用ライブラリ生成ジェネレータ
  • --directory: ライブラリの配置ディレクトリ
  • --buildable: 独立してビルド可能にする設定
  • --publishable=false: npm への公開を無効化

これで libs​/​auth ディレクトリが作成され、基本的な NestJS モジュール構造が生成されます。

mermaidflowchart LR
  cmd["nx generate<br/>コマンド"]
  files["ファイル生成"]
  config["設定更新"]
  ready["開発準備完了"]

  cmd --> files
  files --> config
  config --> ready

  files -.->|作成| module["auth.module.ts"]
  files -.->|作成| service["auth.service.ts"]
  files -.->|作成| spec["テストファイル"]

  config -.->|更新| tsconfig["tsconfig.json"]
  config -.->|更新| nx_json["nx.json"]

ステップ 4:認証ライブラリの実装

認証サービスを実装していきます。

まず、JWT 認証に必要なパッケージをインストールします。

bash# 認証関連パッケージのインストール
yarn add @nestjs/jwt @nestjs/passport passport passport-jwt
yarn add -D @types/passport-jwt

次に、認証サービスを実装します。

typescript// libs/auth/src/lib/auth.service.ts

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

/**
 * 認証処理を担当するサービス
 * JWT トークンの生成と検証を行います
 */
@Injectable()
export class AuthService {
  constructor(private readonly jwtService: JwtService) {}

  /**
   * ユーザー情報から JWT トークンを生成
   * @param userId ユーザー ID
   * @param email メールアドレス
   * @returns JWT トークン文字列
   */
  generateToken(userId: string, email: string): string {
    const payload = { sub: userId, email };
    return this.jwtService.sign(payload);
  }
}

実装のポイント

  • @Injectable() デコレータで DI コンテナに登録
  • JwtService を注入して JWT 操作を委譲
  • ペイロードには最小限の情報のみを含める

続いて、認証モジュールを設定します。

typescript// libs/auth/src/lib/auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';

/**
 * 認証機能を提供するモジュール
 * 他のアプリケーションから import して使用します
 */
@Module({
  imports: [
    JwtModule.register({
      secret: process.env.JWT_SECRET || 'default-secret',
      signOptions: {
        expiresIn: '1h', // トークンの有効期限
      },
    }),
  ],
  providers: [AuthService],
  exports: [AuthService], // 他のモジュールから使用可能にする
})
export class AuthModule {}

モジュール設定の説明

  • JwtModule.register() で JWT の基本設定を行う
  • exportsAuthService を指定して外部公開
  • 環境変数でシークレットキーを管理(本番環境では必須)

最後に、ライブラリのエントリポイントを設定します。

typescript// libs/auth/src/index.ts

/**
 * 認証ライブラリの公開インターフェース
 * 他のパッケージからは、このファイルを通じてインポートします
 */
export * from './lib/auth.module';
export * from './lib/auth.service';

これで、@my-nestjs-monorepo​/​auth という名前で他のアプリケーションから参照できるようになります。

ステップ 5:メイン API での認証ライブラリ使用

既存の api アプリケーションで認証ライブラリを使用します。

まず、apps​/​api​/​src​/​app​/​app.module.ts を編集します。

typescript// apps/api/src/app/app.module.ts

import { Module } from '@nestjs/common';
import { AuthModule } from '@my-nestjs-monorepo/auth'; // 共通ライブラリをインポート
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    AuthModule, // 認証モジュールを登録
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

インポートのポイント

  • ライブラリ名は @{workspace名}​/​{ライブラリ名} の形式
  • 相対パスではなくパッケージ名でインポート
  • TypeScript の path mapping により自動解決

次に、コントローラーで認証機能を使用します。

typescript// apps/api/src/app/app.controller.ts

import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from '@my-nestjs-monorepo/auth';

/**
 * 認証エンドポイントを提供するコントローラー
 */
@Controller('auth')
export class AppController {
  constructor(private readonly authService: AuthService) {}

  /**
   * ログインエンドポイント
   * ユーザー情報を受け取り、JWT トークンを返却します
   */
  @Post('login')
  login(
    @Body() loginDto: { userId: string; email: string }
  ) {
    const token = this.authService.generateToken(
      loginDto.userId,
      loginDto.email
    );

    return {
      success: true,
      token,
      message: '認証に成功しました',
    };
  }
}

エンドポイント実装の説明

  • AuthService を DI で注入
  • POST ​/​auth​/​login でトークン生成
  • 実際の環境では、データベースでの認証処理を追加

ステップ 6:管理者 API アプリケーションの追加

別の API アプリケーションを追加します。

bash# 管理者用 API アプリケーションを生成
nx generate @nx/nest:application admin-api

生成された apps​/​admin-api でも同じ認証ライブラリを使用できます。

typescript// apps/admin-api/src/app/app.module.ts

import { Module } from '@nestjs/common';
import { AuthModule } from '@my-nestjs-monorepo/auth'; // 同じライブラリを使用

@Module({
  imports: [AuthModule],
  // ... 管理者 API 固有の設定
})
export class AppModule {}

このように、共通ライブラリを複数のアプリケーションで再利用できます。

mermaidflowchart TB
  subgraph libs ["libs/ 共有ライブラリ"]
    auth["@my-nestjs-monorepo/auth<br/>認証ライブラリ"]
  end

  subgraph apps ["apps/ アプリケーション"]
    api["api<br/>メインAPI"]
    admin["admin-api<br/>管理者API"]
  end

  auth -->|import| api
  auth -->|import| admin

  api -->|提供| endpoint1["POST /auth/login"]
  admin -->|提供| endpoint2["POST /admin/auth/login"]

  style auth fill:#ffe6cc
  style api fill:#cce5ff
  style admin fill:#cce5ff

図で理解できる要点

  • 一つの認証ライブラリを複数の API で共有
  • 各 API は独立してデプロイ可能
  • ライブラリの変更は全 API に自動反映

ステップ 7:依存関係グラフの確認

Nx の依存関係グラフを確認しましょう。

bash# 依存関係グラフを可視化
nx graph

このコマンドを実行すると、ブラウザが開き、アプリケーションとライブラリの依存関係が視覚的に表示されます。

どのアプリがどのライブラリに依存しているか、一目で把握できるでしょう。

ステップ 8:ビルドとテストの実行

個別のアプリケーションをビルドします。

bash# api アプリケーションのビルド
nx build api

# admin-api アプリケーションのビルド
nx build admin-api

# auth ライブラリのビルド
nx build auth

Nx は依存関係を自動的に解決し、必要なライブラリを先にビルドします。

変更されたコードのみをビルドする場合は以下のコマンドを使用します。

bash# 変更されたプロジェクトのみビルド
nx affected:build

# 変更されたプロジェクトのみテスト
nx affected:test

affected コマンドの動作

  • Git の差分から変更されたファイルを検出
  • 影響を受けるプロジェクトのみ処理
  • CI/CD で大幅な時間短縮を実現

ステップ 9:開発サーバーの起動

複数のアプリケーションを並行して開発できます。

bash# api を起動(ポート 3000)
nx serve api

# 別のターミナルで admin-api を起動(ポート 3001)
nx serve admin-api --port=3001

どちらのアプリケーションも、共通の認証ライブラリの変更をホットリロードで即座に反映します。

ステップ 10:TypeScript パス設定の確認

tsconfig.base.json を確認します。

json{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@my-nestjs-monorepo/auth": ["libs/auth/src/index.ts"]
    }
  }
}

パス設定の役割

  • エイリアスを使ったインポートを可能にする
  • IDE の補完機能を有効化
  • リファクタリング時のパス自動更新

Nx が自動的にこの設定を管理してくれるため、手動での編集は基本的に不要です。

実践的な運用パターン

Monorepo を実際のプロジェクトで運用する際のベストプラクティスを紹介します。

ライブラリの分割戦略

ライブラリは機能ごとに適切に分割しましょう。

#ライブラリ名責務依存関係
1@proj​/​auth認証・認可JWT, Passport
2@proj​/​databaseDB アクセスTypeORM, Prisma
3@proj​/​common共通型定義なし
4@proj​/​config設定管理dotenv
5@proj​/​loggerロギングWinston, Pino

分割の基準

  • 単一責任の原則に従う
  • 再利用性が高い機能を優先
  • 循環依存を避ける構造にする

CI/CD での最適化

GitHub Actions での設定例を示します。

yaml# .github/workflows/ci.yml

name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0 # Nx affected のために全履歴を取得

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'yarn'

      - name: Install dependencies
        run: yarn install --frozen-lockfile

      - name: Build affected projects
        run: yarn nx affected:build --base=origin/main

      - name: Test affected projects
        run: yarn nx affected:test --base=origin/main --coverage

      - name: Lint affected projects
        run: yarn nx affected:lint --base=origin/main

CI/CD 最適化のポイント

  • fetch-depth: 0 で全履歴を取得し、affected 検出を正確にする
  • --frozen-lockfile でロックファイルの整合性を保証
  • affected コマンドで変更部分のみ処理し、時間を短縮

環境変数の管理

各アプリケーションで共通の環境変数を管理します。

ルートに .env ファイルを配置します。

bash# .env

# 共通設定
NODE_ENV=development
LOG_LEVEL=debug

# データベース設定
DATABASE_URL=postgresql://user:password@localhost:5432/mydb

# 認証設定
JWT_SECRET=your-super-secret-key
JWT_EXPIRES_IN=1h

# API 固有設定
API_PORT=3000
ADMIN_API_PORT=3001

各アプリケーションで環境変数を読み込みます。

typescript// libs/config/src/lib/config.service.ts

import { Injectable } from '@nestjs/common';
import { ConfigService as NestConfigService } from '@nestjs/config';

/**
 * 環境変数を型安全に取得するサービス
 */
@Injectable()
export class ConfigService {
  constructor(private configService: NestConfigService) {}

  /**
   * JWT シークレットキーを取得
   */
  get jwtSecret(): string {
    return (
      this.configService.get<string>('JWT_SECRET') || ''
    );
  }

  /**
   * JWT 有効期限を取得
   */
  get jwtExpiresIn(): string {
    return (
      this.configService.get<string>('JWT_EXPIRES_IN') ||
      '1h'
    );
  }

  /**
   * データベース URL を取得
   */
  get databaseUrl(): string {
    return (
      this.configService.get<string>('DATABASE_URL') || ''
    );
  }
}

環境変数管理のベストプラクティス

  • .env はルートに配置し、全アプリで共有
  • .env.example でテンプレートを提供
  • .env.gitignore に追加し、コミットしない
  • 型安全な Config Service で取得

デプロイ戦略

アプリケーションごとに個別デプロイが可能です。

Docker を使用したデプロイ例を示します。

dockerfile# apps/api/Dockerfile

FROM node:18-alpine AS builder

WORKDIR /app

# 依存関係をインストール
COPY package.json yarn.lock ./
COPY tsconfig.base.json ./
RUN yarn install --frozen-lockfile

# ソースコードをコピー
COPY . .

# api アプリケーションとその依存ライブラリをビルド
RUN yarn nx build api --prod

# ランタイムイメージ
FROM node:18-alpine

WORKDIR /app

# ビルド成果物をコピー
COPY --from=builder /app/dist/apps/api ./
COPY --from=builder /app/node_modules ./node_modules

# 環境変数の設定
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

CMD ["node", "main.js"]

Docker ビルドの最適化

  • マルチステージビルドで最終イメージサイズを削減
  • 依存ライブラリを含めてビルド
  • 本番環境用の最適化オプションを使用

Kubernetes でのデプロイ設定例です。

yaml# k8s/api-deployment.yml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  labels:
    app: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: my-registry/api:latest
          ports:
            - containerPort: 3000
          env:
            - name: JWT_SECRET
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: jwt-secret
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: database-url
          resources:
            requests:
              memory: '256Mi'
              cpu: '250m'
            limits:
              memory: '512Mi'
              cpu: '500m'
---
apiVersion: v1
kind: Service
metadata:
  name: api-service
spec:
  selector:
    app: api
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: LoadBalancer

Kubernetes デプロイのポイント

  • 複数レプリカで冗長性を確保
  • Secret で機密情報を管理
  • Resource limits で安定稼働を実現

パフォーマンスモニタリング

Nx のキャッシュ機能を活用します。

json// nx.json

{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test", "lint"],
        "cacheDirectory": "node_modules/.cache/nx"
      }
    }
  }
}

キャッシュの効果

  • 同一コードの再ビルドをスキップ
  • ローカル開発で劇的な高速化
  • CI/CD でもキャッシュを活用可能

ビルド時間の比較例を示します。

#状況初回ビルドキャッシュ利用短縮率
1全プロジェクト180 秒5 秒97%
2affected のみ45 秒3 秒93%
3単一アプリ30 秒2 秒93%

まとめ

NestJS Monorepo の構築方法について、Nx と Yarn Workspaces を使った実践的な手順を解説しました。

Monorepo を採用することで、コードの重複を減らし、依存関係を明確に管理できます。特に大規模プロジェクトや複数チームでの開発において、その効果は顕著でしょう。

Yarn Workspaces がパッケージ管理を統一し、Nx が増分ビルドや依存関係グラフで開発体験を向上させます。この組み合わせにより、開発速度と品質の両立が可能になるのです。

まずは小さなプロジェクトから始めて、徐々に Monorepo の恩恵を実感してください。認証やロギングといった共通機能をライブラリ化するだけでも、大きな効果が得られます。

本記事で紹介した構成をベースに、チームの要件に合わせてカスタマイズしてみてください。Monorepo による効率的な開発を、ぜひ体験してみましょう。

関連リンク