2026年版 NestJSとMongoDB完全ガイド:Mongoose、集計パイプライン、面接対策

NestJS 12とMongoose 9を使用したMongoDBアプリケーション開発の完全ガイド。スキーマ設計、集計パイプライン、トランザクション処理、そして技術面接で頻出する質問と回答を詳しく解説します。

NestJS and MongoDB integration diagram

NestJS 12とMongoose 9の組み合わせは、スケーラブルなNode.jsバックエンドを構築するための本番環境対応スタックを提供します。本ガイドでは、スキーマ設計パターン、集計パイプライン、そしてシニアエンジニアとジュニアエンジニアを区別する面接質問について解説します。

クイックリファレンス

Mongoose 9.9.4はNode.js 18以上が必要で、MongoDB 6.0から8.0までをサポートしています。NestJS 12はESM対応パッケージを同梱していますが、CommonJSプロジェクトとの下位互換性も維持されています。

NestJS 12アプリケーションでのMongooseセットアップ

@nestjs/mongooseパッケージは、MongooseをNestJSの依存性注入システムと統合します。必要な依存関係をインストールします。

bash
# Mongoose統合パッケージのインストール
npm install @nestjs/mongoose mongoose

ルートモジュールで接続を登録します。

app.module.tstypescript
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forRoot(process.env.MONGODB_URI, {
      // 本番ワークロード向けの接続プールサイズ
      maxPoolSize: 10,
      // 接続失敗時は10秒でタイムアウト
      serverSelectionTimeoutMS: 10000,
    }),
  ],
})
export class AppModule {}

forRootメソッドはすべてのMongoose接続オプションを受け入れます。maxPoolSizeを設定することで、負荷時の接続枯渇を防止できます。これは本番環境でよく発生する問題です。

TypeScriptデコレータによるスキーマ設計

NestJSのMongooseスキーマは@nestjs/mongooseのデコレータを使用します。各スキーマはMongoDBコレクションにマッピングされます。

user.schema.tstypescript
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Types } from 'mongoose';

// TypeScript自動補完用のドキュメント型
export type UserDocument = HydratedDocument<User>;

@Schema({
  timestamps: true, // createdAtとupdatedAtを追加
  collection: 'users', // 明示的なコレクション名
})
export class User {
  // MongoDB ObjectId、自動生成
  _id: Types.ObjectId;

  @Prop({ required: true, unique: true, index: true })
  email: string;

  @Prop({ required: true })
  passwordHash: string;

  @Prop({ type: String, enum: ['admin', 'user', 'guest'], default: 'user' })
  role: string;

  @Prop({ type: [String], default: [] })
  permissions: string[];
}

export const UserSchema = SchemaFactory.createForClass(User);

@Propデコレータはフィールド制約を定義します。頻繁にクエリされるフィールドにindex: trueを設定すると、書き込みが遅くなる代わりに読み取りパフォーマンスが向上します。

面接のポイント

面接官は、埋め込みドキュメントと参照の使い分けについて質問することがあります。埋め込みドキュメントは一緒にアクセスされるデータ(ユーザープロフィール+設定)に適しています。参照は無制限に増加するデータや独立したクエリが必要なデータ(ユーザー+注文)に適しています。

Injectableサービスによるリポジトリパターン

NestJSではデータベースロジックをサービスに分離することが推奨されています。@InjectModelデコレータはMongooseモデルへのアクセスを提供します。

user.service.tstypescript
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { User, UserDocument } from './user.schema';

@Injectable()
export class UserService {
  constructor(
    @InjectModel(User.name) private userModel: Model<UserDocument>,
  ) {}

  async findById(id: string): Promise<UserDocument> {
    // クエリ前にObjectId形式を検証
    if (!Types.ObjectId.isValid(id)) {
      throw new NotFoundException('無効なユーザーID形式です');
    }
    const user = await this.userModel.findById(id).exec();
    if (!user) {
      throw new NotFoundException(`ユーザー ${id} が見つかりません`);
    }
    return user;
  }

  async findByEmail(email: string): Promise<UserDocument | null> {
    // 大文字小文字を区別しないメール検索
    return this.userModel.findOne({ 
      email: { $regex: new RegExp(`^${email}$`, 'i') } 
    }).exec();
  }

  async create(data: Partial<User>): Promise<UserDocument> {
    const user = new this.userModel(data);
    return user.save();
  }
}

.exec()を呼び出すと、Mongoose Queryオブジェクトではなく適切なPromiseが返されます。これは正しいasync/awaitの動作とエラースタックトレースにとって重要です。

複雑なクエリのための集計パイプライン

MongoDB集計は、SQLデータベースがJOINやGROUP BYで解決するレポート、分析、データ変換を処理します。

analytics.service.tstypescript
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, PipelineStage } from 'mongoose';
import { Order, OrderDocument } from './order.schema';

interface MonthlyRevenue {
  month: number;
  revenue: number;
  orderCount: number;
}

@Injectable()
export class AnalyticsService {
  constructor(
    @InjectModel(Order.name) private orderModel: Model<OrderDocument>,
  ) {}

  async getRevenueByMonth(year: number): Promise<MonthlyRevenue[]> {
    const pipeline: PipelineStage[] = [
      // ステージ1: 年でフィルタ
      {
        $match: {
          createdAt: {
            $gte: new Date(`${year}-01-01`),
            $lt: new Date(`${year + 1}-01-01`),
          },
          status: 'completed',
        },
      },
      // ステージ2: 月ごとにグループ化
      {
        $group: {
          _id: { $month: '$createdAt' },
          revenue: { $sum: '$totalAmount' },
          orderCount: { $sum: 1 },
        },
      },
      // ステージ3: 月順でソート
      { $sort: { _id: 1 } },
      // ステージ4: 出力形式を整形
      {
        $project: {
          _id: 0,
          month: '$_id',
          revenue: 1,
          orderCount: 1,
        },
      },
    ];

    return this.orderModel.aggregate(pipeline).exec();
  }
}

集計パイプラインは順番に実行されます。各ステージは次のステージにドキュメントを渡します。パフォーマンスを最適化するために、$matchステージをできるだけ早い段階に配置してください。

$lookupを使用したコレクション間の結合

MongoDBはドキュメントデータベースですが、$lookupを使用して異なるコレクション間でデータを結合できます。

order-details.service.tstypescript
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, PipelineStage } from 'mongoose';
import { Order, OrderDocument } from './order.schema';

@Injectable()
export class OrderDetailsService {
  constructor(
    @InjectModel(Order.name) private orderModel: Model<OrderDocument>,
  ) {}

  async getOrdersWithUserDetails(): Promise<any[]> {
    const pipeline: PipelineStage[] = [
      // usersコレクションと結合
      {
        $lookup: {
          from: 'users',
          localField: 'userId',
          foreignField: '_id',
          as: 'user',
        },
      },
      // 配列から単一オブジェクトに変換
      { $unwind: '$user' },
      // 必要なフィールドのみ選択
      {
        $project: {
          orderNumber: 1,
          totalAmount: 1,
          'user.email': 1,
          'user.role': 1,
        },
      },
    ];

    return this.orderModel.aggregate(pipeline).exec();
  }
}

$lookupは左外部結合を実行します。大規模なコレクションで使用する場合は、結合フィールドにインデックスがあることを確認してください。

パフォーマンス注意

$lookupは、結合されるコレクションが大きい場合にパフォーマンスの問題を引き起こす可能性があります。読み取り頻度の高いデータの場合は、データの非正規化を検討してください。

MongoDBトランザクションの実装

Mongoose 9はマルチドキュメントトランザクションをサポートしており、複数のドキュメントにわたるアトミックな操作を保証します。

transfer.service.tstypescript
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectModel, InjectConnection } from '@nestjs/mongoose';
import { Model, Connection } from 'mongoose';
import { Account, AccountDocument } from './account.schema';

@Injectable()
export class TransferService {
  constructor(
    @InjectModel(Account.name) private accountModel: Model<AccountDocument>,
    @InjectConnection() private connection: Connection,
  ) {}

  async transfer(
    fromAccountId: string,
    toAccountId: string,
    amount: number,
  ): Promise<void> {
    // セッションを開始
    const session = await this.connection.startSession();

    try {
      await session.withTransaction(async () => {
        // 送金元から減額
        const fromAccount = await this.accountModel.findByIdAndUpdate(
          fromAccountId,
          { $inc: { balance: -amount } },
          { session, new: true },
        );

        if (!fromAccount || fromAccount.balance < 0) {
          throw new BadRequestException('残高不足です');
        }

        // 送金先に加算
        await this.accountModel.findByIdAndUpdate(
          toAccountId,
          { $inc: { balance: amount } },
          { session },
        );
      });
    } finally {
      await session.endSession();
    }
  }
}

トランザクションはレプリカセット環境が必要です。ローカル開発では、mongod --replSet rs0でMongoDBを起動してください。

インデックス戦略とパフォーマンス最適化

適切なインデックス設計は、MongoDBアプリケーションのパフォーマンスに大きな影響を与えます。

product.schema.tstypescript
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';

export type ProductDocument = HydratedDocument<Product>;

@Schema({
  timestamps: true,
  // スキーマレベルでインデックスを定義
  autoIndex: process.env.NODE_ENV !== 'production',
})
export class Product {
  @Prop({ required: true, index: true })
  name: string;

  @Prop({ required: true })
  price: number;

  @Prop({ index: true })
  category: string;

  @Prop({ type: [String], index: true })
  tags: string[];
}

export const ProductSchema = SchemaFactory.createForClass(Product);

// 複合インデックスの定義
ProductSchema.index({ category: 1, price: -1 });
ProductSchema.index({ name: 'text', tags: 'text' });

複合インデックスは、複数のフィールドでフィルタリングするクエリのパフォーマンスを向上させます。テキストインデックスは全文検索を可能にします。

技術面接でよく聞かれる質問

NestJSとMongoDB関連の技術面接で頻出する質問とその回答を紹介します。

Q1: MongoDBとSQLデータベースの違いは何ですか?

MongoDBはドキュメント指向のNoSQLデータベースで、柔軟なスキーマを持ちます。SQLデータベースは固定スキーマとACIDトランザクションを持ちます。MongoDBは水平スケーリングに優れ、非構造化データや急速に変化するデータモデルに適しています。

Q2: 埋め込みドキュメントと参照のどちらを使用すべきですか?

埋め込みドキュメントは、データが一緒に読み取られ、親ドキュメントのサイズが16MBを超えない場合に使用します。参照は、データが独立して更新される場合や、多対多の関係がある場合に使用します。

Q3: Mongooseのlean()メソッドは何をしますか?

lean()はプレーンなJavaScriptオブジェクトを返し、Mongooseドキュメントのオーバーヘッドを回避します。読み取り専用の操作では、パフォーマンスが大幅に向上します。

typescript
// lean()を使用した高速クエリ
const users = await this.userModel.find().lean().exec();

Q4: インデックスの選択基準は何ですか?

頻繁にクエリされるフィールド、ソートに使用されるフィールド、一意性制約が必要なフィールドにインデックスを作成します。ただし、インデックスは書き込み性能を低下させるため、必要最小限に抑えてください。

Q5: 集計パイプラインのパフォーマンスを最適化する方法は?

$match$projectをパイプラインの早い段階で使用してデータ量を削減します。allowDiskUse: trueオプションを使用して、100MBを超える集計結果を処理できます。

Node.js / NestJSの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

本番環境のベストプラクティス

NestJSとMongoDBを本番環境で運用する際のベストプラクティスを紹介します。

typescript
// 本番環境向けの接続設定
MongooseModule.forRoot(process.env.MONGODB_URI, {
  maxPoolSize: 50,
  minPoolSize: 10,
  serverSelectionTimeoutMS: 5000,
  socketTimeoutMS: 45000,
  retryWrites: true,
  w: 'majority',
});

接続プールサイズ、タイムアウト、書き込み確認設定を適切に調整することで、本番環境での安定性とパフォーマンスが向上します。

まとめ

NestJS 12とMongoose 9の組み合わせは、モダンなNode.jsバックエンド開発において強力な選択肢です。スキーマ設計、集計パイプライン、トランザクション処理の理解は、本番環境で信頼性の高いアプリケーションを構築するために不可欠です。技術面接では、これらのトピックに加えて、パフォーマンス最適化とスケーリング戦略についても質問されることが多いです。実践的な経験と理論的な理解の両方を身につけることで、面接での成功確率が高まります。

今日のチャレンジ

Node.js / NestJS のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年8月28日 更新

タグ

#NestJS
#MongoDB
#Mongoose
#Node.js
#Backend

共有

関連記事