Episode 3 — NodeJS MongoDB Backend Architecture / 3.8 — Database Basics MongoDB

3.8 — Database Basics: MongoDB: Quick Revision

Episode 3 supplement -- print-friendly.

How to use

Skim -> drill weak spots in 3.8.a through 3.8.g -> 3.8-Exercise-Questions.md.


SQL vs MongoDB Terminology

SQLMongoDB
DatabaseDatabase
TableCollection
RowDocument
ColumnField
Primary Key_id (ObjectId)
JOINpopulate() / $lookup
Schema (DDL)Schema (Mongoose, app-level)
INDEXIndex

Connection

const mongoose = require('mongoose');
require('dotenv').config();

mongoose.connect(process.env.MONGODB_URI)
  .then(() => console.log('MongoDB connected'))
  .catch(err => { console.error(err); process.exit(1); });
# .env
MONGODB_URI=mongodb://localhost:27017/myapp
# Atlas: mongodb+srv://user:pass@cluster.xxxxx.mongodb.net/myapp

Schema Definition

const { Schema } = mongoose;

const userSchema = new Schema({
  name:    { type: String, required: true, trim: true, minlength: 2 },
  email:   { type: String, required: true, unique: true, lowercase: true },
  age:     { type: Number, min: 0, max: 150 },
  role:    { type: String, enum: ['user', 'admin'], default: 'user' },
  isActive:{ type: Boolean, default: true },
  hobbies: [String],
  address: { street: String, city: String, zip: String },
  author:  { type: Schema.Types.ObjectId, ref: 'User' }
}, { timestamps: true, toJSON: { virtuals: true } });

const User = mongoose.model('User', userSchema);

Schema Types

TypeExample
String'Alice'
Number25, 3.14
Booleantrue
Datenew Date()
ObjectIdSchema.Types.ObjectId
Array[String], [{ name: String }]
Decimal128Schema.Types.Decimal128
MixedSchema.Types.Mixed (any, no validation)
BufferBinary data
MapDynamic key-value

Validators

ValidatorApplies ToExample
requiredAllrequired: [true, 'Name required']
min / maxNumber, Datemin: 0, max: 150
minlength / maxlengthStringminlength: 2
enumString, Numberenum: ['a', 'b', 'c']
matchStringmatch: /^\S+@\S+\.\S+$/
validateAll (custom)validate: { validator: fn, message: '...' }

Note: unique is a MongoDB index, not a Mongoose validator. Catch with error code 11000.


CRUD Operations

Create

const user = await User.create({ name: 'Alice', email: 'alice@example.com' });
// or: const user = new User({...}); await user.save();

Read

const all      = await User.find({});
const filtered = await User.find({ role: 'admin' });
const one      = await User.findOne({ email: 'alice@example.com' });
const byId     = await User.findById(id);
const count    = await User.countDocuments({ role: 'user' });

Update

const updated = await User.findByIdAndUpdate(id,
  { name: 'New Name' },
  { new: true, runValidators: true }
);
// or: findById + modify + save() (runs pre-save middleware)

Delete

const deleted = await User.findByIdAndDelete(id);
// Soft delete: set isDeleted: true instead

Query Operators

OperatorMeaningExample
$gtGreater than{ age: { $gt: 25 } }
$gteGreater or equal{ age: { $gte: 18 } }
$ltLess than{ age: { $lt: 30 } }
$lteLess or equal{ age: { $lte: 65 } }
$neNot equal{ role: { $ne: 'banned' } }
$inIn array{ role: { $in: ['admin', 'mod'] } }
$ninNot in array{ role: { $nin: ['banned'] } }
$orLogical OR{ $or: [{...}, {...}] }
$andLogical AND{ $and: [{...}, {...}] }
$regexPattern match{ name: { $regex: /^A/i } }
$existsField exists{ field: { $exists: true } }
$elemMatchArray element match{ arr: { $elemMatch: {...} } }

Update Operators

OperatorDescriptionExample
$setSet field{ $set: { name: 'New' } }
$unsetRemove field{ $unset: { temp: '' } }
$incIncrement{ $inc: { count: 1 } }
$pushAdd to array{ $push: { tags: 'new' } }
$pullRemove from array{ $pull: { tags: 'old' } }
$addToSetAdd if unique{ $addToSet: { tags: 'x' } }
$popRemove first/last{ $pop: { arr: 1 } }

Projection, Sorting, Pagination

await User.find({ role: 'user' })
  .select('name email -_id')          // projection
  .sort({ createdAt: -1 })            // sort (newest first)
  .skip((page - 1) * limit)           // pagination offset
  .limit(limit)                        // page size
  .lean();                             // plain JS objects

lean()

With leanWithout lean
Plain JS objectsMongoose Documents
No virtuals, no methods, no save()Full Mongoose features
2-5x fasterStandard speed
Use for: read-only, API responsesUse for: modify + save

Populate (Joins)

// Basic
const post = await Post.findById(id).populate('author');

// Select fields
const post = await Post.findById(id).populate('author', 'name email');

// Multiple paths
const comment = await Comment.findById(id)
  .populate('author', 'name')
  .populate('post', 'title');

// Nested (deep)
const comment = await Comment.findById(id)
  .populate({ path: 'post', populate: { path: 'author', select: 'name' } });

// With conditions
await Author.findById(id).populate({
  path: 'posts',
  match: { isPublished: true },
  options: { sort: { createdAt: -1 }, limit: 5 }
});

Virtual Populate

// Author does NOT store an array of post IDs
// Instead, Mongoose looks up posts dynamically

authorSchema.virtual('posts', {
  ref: 'Post',
  localField: '_id',
  foreignField: 'author'
});

// Must enable virtuals in output
// { toJSON: { virtuals: true }, toObject: { virtuals: true } }

const author = await Author.findById(id).populate('posts');

Embed vs Reference

CriteriaEmbedReference
Relationship1:1, 1:few1:many, many:many
Accessed together?YesNot always
Size growthBoundedUnbounded OK
ReadsSingle queryMultiple (populate)
WritesRisk of large updatesSmall, targeted
ConsistencyAtomic (one doc)May need transactions
1:1   → Embed (almost always)
1:Few → Embed (usually)
1:Many → Reference (child refs parent)
Many:Many → Reference (arrays or junction collection)

Middleware (Hooks)

// Hash password before save
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 10);
  next();
});

// Soft-delete filter on all finds
userSchema.pre('find', function() {
  this.where({ isDeleted: { $ne: true } });
});

Virtuals

userSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});
// Not stored in DB. Include: { toJSON: { virtuals: true } }

Error Handling

app.use((err, req, res, next) => {
  if (err.name === 'ValidationError') {
    const errors = Object.values(err.errors).map(e => e.message);
    return res.status(400).json({ error: 'Validation failed', details: errors });
  }
  if (err.code === 11000) {
    const field = Object.keys(err.keyValue)[0];
    return res.status(409).json({ error: `Duplicate ${field}` });
  }
  if (err.name === 'CastError') {
    return res.status(400).json({ error: `Invalid ${err.path}` });
  }
  res.status(500).json({ error: 'Internal server error' });
});
ErrorStatusCause
ValidationError400Required field missing, min/max fail
code: 11000409Duplicate unique key
CastError400Invalid ObjectId format

Model File Pattern

models/
├── User.js      // Schema + validators + virtuals + methods + hooks + export
├── Post.js
└── Comment.js
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({ /* ... */ }, { timestamps: true });
module.exports = mongoose.model('User', userSchema);

One-Liners

  • MongoDB = document database; flexible schema; stores BSON.
  • Mongoose = ODM that adds schemas, validation, middleware, populate.
  • ObjectId = 12-byte auto-generated _id; contains timestamp.
  • Embed = nested data in one document. Reference = ObjectId pointer to another collection.
  • populate() = application-level join; at least 2 queries.
  • lean() = skip Mongoose wrapper; return plain objects; faster reads.
  • pre('save') = middleware that runs before .save(); use for password hashing.
  • Virtuals = computed properties not stored in DB; need toJSON: { virtuals: true }.

End of 3.8 quick revision.