はじめに
MongoDBはスキーマレスなドキュメントDBです。自由度が高い反面、「このコレクションにどんなフィールドが入っているか」をコード側で保証してくれないため、プロジェクトが大きくなるほど「いつの間にか型が揺れたドキュメントが混在する」という悩みに直面しがちです。
Mongooseは、そんなMongoDBに対して「スキーマ」という秩序を持ち込むオブジェクトモデリングツールです。Node.js上でMongoDBのドキュメントをJavaScript/TypeScriptのオブジェクトとして扱えるようにし、バリデーション・デフォルト値・型変換・リレーションの疑似再現(populate)・ミドルウェアといった、素のMongoDBドライバにはない機能を一通り揃えています。
Guillermo Rauch氏が作り、現在はAutomattic社がメンテナンスするこのライブラリは、GitHubで27,500以上のスターを獲得し、npmでの週間ダウンロード数も非常に多い、MongoDBを使うNode.jsプロジェクトでは事実上の定番と言える存在です。
Mongooseとは
Mongooseは「MongoDB object modeling designed to work in an asynchronous environment」を掲げる、Node.js向けのMongoDB ODM(Object Document Mapper)です。2026年8月時点の最新バージョンは9.9.1で、Node.js 20.19以降を前提に開発が続けられています(Denoへの対応もアルファ版として進行中です)。
主な特徴
- スキーマベースのモデル定義 - コレクションに入るドキュメントの形をスキーマとして明示し、型の揺れを防げる
- 組み込みバリデーション -
requiredやmin/max、正規表現、カスタム関数によるバリデーションを宣言的に書ける - ミドルウェア(フック) -
saveやfindなどの操作の前後に処理を差し込める - populateによる疑似JOIN - 参照先ドキュメントのIDを保存しておき、クエリ時に関連ドキュメントを展開できる
- 豊富なクエリビルダー - MongoDBの集計パイプラインやクエリ演算子をチェーンで書ける
インストール方法
npm、yarn、pnpmのいずれでもインストールできます。
# npm
npm install mongoose
# yarn
yarn add mongoose
# pnpm
pnpm add mongoose
MongooseはNode.js専用のライブラリで、実際に稼働しているMongoDBインスタンスへの接続を前提としています。そのため本記事のサンプルコードはブラウザ上では動作せず、Node.js環境での実行を想定しています。
基本的な使い方
接続の確立
mongoose.connect()にMongoDBの接続文字列を渡すだけで接続できます。
const mongoose = require("mongoose");
async function main() {
await mongoose.connect("mongodb://127.0.0.1:27017/myapp");
console.log("MongoDBに接続しました");
}
main().catch((err) => console.error(err));
スキーマとモデルの定義
Schemaでドキュメントの形を定義し、mongoose.model()でそのスキーマに対応するモデルを作成します。
const { Schema, model } = require("mongoose");
const userSchema = new Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
age: { type: Number, min: 0 },
createdAt: { type: Date, default: Date.now },
});
const User = model("User", userSchema);
CRUD操作
モデルが持つメソッドで、作成・取得・更新・削除がそのまま書けます。
// 作成
const user = await User.create({ name: "田中太郎", email: "tanaka@example.com", age: 28 });
// 取得
const found = await User.findById(user._id);
const list = await User.find({ age: { $gte: 20 } });
// 更新
await User.updateOne({ _id: user._id }, { age: 29 });
// 削除
await User.deleteOne({ _id: user._id });
find系のメソッドはPromiseベースなので、async/awaitと組み合わせるだけで直感的に書けます。
実践的なユースケース
スキーマバリデーション
MongoDBは本来どんな形のドキュメントでも受け入れてしまいますが、Mongooseのスキーマにrequiredやmin、カスタムバリデータを書いておくと、保存前に不正なデータを弾けます。
const productSchema = new Schema({
name: { type: String, required: [true, "商品名は必須です"] },
price: {
type: Number,
required: true,
min: [0, "価格は0以上である必要があります"],
},
sku: {
type: String,
validate: {
validator: (v) => /^[A-Z]{3}-\d{4}$/.test(v),
message: (props) => `${props.value}はSKUの形式(例: ABC-1234)ではありません`,
},
},
});
const Product = model("Product", productSchema);
try {
await Product.create({ name: "マグカップ", price: -100, sku: "invalid" });
} catch (err) {
console.log(err.errors.price.message); // 価格は0以上である必要があります
console.log(err.errors.sku.message); // invalidはSKUの形式(例: ABC-1234)ではありません
}
priceを負の値にしたり、skuを規定のフォーマット以外にすると、ValidationErrorが発生してDBへの書き込み自体がブロックされます。アプリケーション側でのif文チェックに頼らず、モデル定義の時点でルールを一元化できるのがポイントです。
populateによるリレーションの疑似再現
MongoDBにJOINは存在しませんが、Mongooseのpopulate()を使うと、参照先のドキュメントIDを保存しておいて、クエリ時に自動で展開できます。
const authorSchema = new Schema({ name: String });
const bookSchema = new Schema({
title: String,
author: { type: Schema.Types.ObjectId, ref: "Author" },
});
const Author = model("Author", authorSchema);
const Book = model("Book", bookSchema);
const author = await Author.create({ name: "夏目漱石" });
await Book.create({ title: "吾輩は猫である", author: author._id });
// populateでauthorフィールドを実データに展開
const book = await Book.findOne({ title: "吾輩は猫である" }).populate("author");
console.log(book.author.name); // 夏目漱石
refに対象モデル名を指定しておくだけで、.populate("author")を呼んだクエリだけ関連ドキュメントを取得できます。populateを呼ばなければauthorフィールドはIDのままなので、必要な箇所だけ結合コストをかけられるのが利点です。
ミドルウェア(フック)による横断処理
schema.pre() / schema.post()を使うと、saveやfindなどの操作の前後に共通処理を差し込めます。パスワードのハッシュ化やログ出力など、モデル横断で必要になる処理をスキーマ側に閉じ込められます。
const bcrypt = require("bcrypt");
const accountSchema = new Schema({
email: String,
password: String,
});
// 保存前にパスワードをハッシュ化
accountSchema.pre("save", async function (next) {
if (!this.isModified("password")) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
// 保存後にログを出力
accountSchema.post("save", function (doc) {
console.log(`アカウントが作成されました: ${doc.email}`);
});
const Account = model("Account", accountSchema);
isModified()を使うことで、パスワード以外のフィールドを更新したときに再ハッシュ化してしまうバグを防いでいます。フックはモデルの利用側から意識する必要がないため、呼び出し忘れも起きません。
集計パイプラインによる分析クエリ
単純なCRUDだけでなく、aggregate()を使えばMongoDBの集計パイプラインをそのままチェーンで書けます。
const salesSchema = new Schema({
product: String,
amount: Number,
region: String,
});
const Sale = model("Sale", salesSchema);
const result = await Sale.aggregate([
{ $match: { region: "asia" } },
{ $group: { _id: "$product", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } },
]);
console.log(result); // 商品ごとの売上合計が降順で並ぶ
$matchで絞り込み、$groupで集計し、$sortで並び替えるという流れは、SQLのWHERE・GROUP BY・ORDER BYに対応します。ダッシュボード用の集計処理をアプリケーション側でループを書かずに済ませられます。
まとめ
Mongooseは、MongoDBの自由度の高さと引き換えに失われがちな「型の一貫性」を、スキーマという形でアプリケーション側に取り戻すためのツールです。バリデーション、populate、ミドルウェア、集計パイプラインといった機能が揃っているため、素のMongoDBドライバを直接扱うよりも、はるかに保守しやすいコードになります。
すでにMongoDBを使っているプロジェクトで「ドキュメントの形がバラバラになってきた」と感じているなら、Mongooseの導入を検討してみる価値は十分にあります。