Mongoose ODM
Mongoose is the de-facto ODM (Object-Document Mapper) for MongoDB in Node. It sits on top of the official driver and adds the things the driver lacks: schemas with validation, type casting, defaults, middleware hooks, virtuals, and relationship population. In short, it brings structure to a schemaless database — enforced in your app. This page introduces Mongoose's building blocks; the next two pages go deep on schemas and queries.
Connecting
npm install mongoose
db.js
import mongoose from 'mongoose'
export async function connectDB() {
await mongoose.connect(process.env.MONGODB_URI) // ONCE at startup
console.log('MongoDB connected')
}
// Mongoose maintains a single pooled connection internally.
mongoose.connection.on('error', (err) => console.error('Mongo error', err))Schema → Model → Document
import mongoose from 'mongoose'
const { Schema } = mongoose
// 1. Schema — the shape, types, and rules:
const userSchema = new Schema({
name: { type: String, required: true, trim: true },
email: { type: String, required: true, unique: true, lowercase: true },
age: { type: Number, min: 0, max: 150 },
role: { type: String, enum: ['user', 'admin'], default: 'user' },
}, { timestamps: true }) // auto createdAt / updatedAt
// 2. Model — the interface to the 'users' collection:
const User = mongoose.model('User', userSchema)
// 3. Document — an instance you create/query:
const u = new User({ name: 'Ada', email: 'ADA@x.com' })
await u.save() // validated, lowercased, timestamped, insertedWhat Mongoose adds over the driver
Feature | What it does |
|---|---|
Schema validation | Rejects bad data before it hits the DB |
Type casting |
|
Defaults | Fill missing fields ( |
Middleware (hooks) | Run logic |
Virtuals | Computed properties not stored in the DB |
Population | Resolve referenced documents (join-like) |
Query helpers | Chainable, promise-based query builder |
CRUD with a model
// Create:
const user = await User.create({ name: 'Ada', email: 'ada@x.com' })
// Read — casting & null handling built in:
const all = await User.find({ role: 'admin' }).sort('name').limit(20)
const one = await User.findById(req.params.id) // string id auto-cast
const byEmail = await User.findOne({ email: 'ada@x.com' })
// Update — returns the NEW doc, runs validators:
const updated = await User.findByIdAndUpdate(
req.params.id,
{ name: 'Ada L.' },
{ new: true, runValidators: true },
)
// Delete:
await User.findByIdAndDelete(req.params.id)Instance and static methods
// Instance method — operates on one document:
userSchema.methods.isAdult = function () { return this.age >= 18 }
// Static method — operates on the model/collection:
userSchema.statics.findByEmail = function (email) {
return this.findOne({ email: email.toLowerCase() })
}
const u = await User.findByEmail('ada@x.com') // static
if (u.isAdult()) { /* ... */ } // instanceThe trade-off
Pro — schema safety, validation, casting, hooks, population, and clean models out of the box.
Pro — far less boilerplate than the driver for typical CRUD.
Con — overhead and "magic"; complex aggregations may still drop to the driver.
Con — schema lives in code, so it can drift from existing data if not migrated.