NodeJSIntroduction to MongoDB

Introduction to MongoDB

MongoDB is the most widely used document database. Instead of tables and rows, it stores documents (JSON-like objects) grouped into collections. Its data model maps almost directly onto JavaScript objects, which is why it pairs so naturally with Node. This page covers MongoDB's vocabulary, the document model, BSON, and the design decisions — embedding vs referencing, indexing — that determine whether a Mongo schema performs well.

The vocabulary, translated from SQL

SQL term

MongoDB term

Database

Database

Table

Collection

Row

Document

Column

Field

Primary key

_id (auto ObjectId)

Join

$lookup / embedding / app-side

A document is essentially a JSON object with types
A MongoDB document looks like JSON and behaves like a JS object — nested objects and arrays included. It's stored as **BSON** (Binary JSON), which adds types JSON lacks: `ObjectId`, `Date`, `Decimal128`, binary data, and distinct int/double. Every document gets a unique `_id` (a 12-byte `ObjectId`) automatically if you don't supply one.
A document

JSON
{
  "_id": { "$oid": "65f1a2b3c4d5e6f7a8b9c0d1" },
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "roles": ["user", "editor"],
  "address": { "city": "London", "zip": "EC1" },
  "orders": [
    { "sku": "BK-1", "qty": 2, "price": 19.99 }
  ],
  "createdAt": { "$date": "2026-01-12T09:30:00Z" }
}
`_id` is an `ObjectId`, not a string — comparisons need conversion
A document's `_id` is a 12-byte `ObjectId`, not a plain string. When a value arrives from a URL or JSON (`req.params.id`), it's a *string* — querying `{ _id: req.params.id }` may not match, and an invalid hex string throws. Convert explicitly with `new ObjectId(id)` (and guard with `ObjectId.isValid(id)`). [Mongoose](/nodejs/mongoose) handles this casting for you; the raw [driver](/nodejs/mongodb-driver) does not.
The defining decision: embed vs reference

JS
// EMBED — related data inside the parent document (one read gets everything):
{ _id: 1, name: 'Ada', orders: [ { sku: 'BK-1', qty: 2 } ] }

// REFERENCE — store an id, look up separately (like a SQL foreign key):
{ _id: 1, name: 'Ada' }
{ _id: 9, userId: 1, sku: 'BK-1', qty: 2 }   // separate 'orders' collection

Embed when…

Reference when…

Data is read together

Data is queried independently

The relationship is "contains"/"part-of"

The relationship is "many-to-many"

The sub-data is bounded in size

The sub-data grows unboundedly

You want one fast read

The sub-data is shared/large

Don't embed unbounded, ever-growing arrays
Embedding is great for bounded sub-data (an address, a handful of line items), but a document has a **16MB limit** and large embedded arrays hurt performance — every update rewrites the whole document, and a comment list that grows forever will eventually overflow. For unbounded one-to-many (a post's comments, a user's activity), use a **separate collection with a reference**. The mantra: *data that's accessed together is stored together — but only if it's bounded.*
Querying — by example documents

JS
// Find — query is a document describing what to match:
db.users.find({ age: { $gte: 18 }, roles: 'editor' })

// Operators start with $:
{ age: { $gt: 18, $lt: 65 } }          // range
{ status: { $in: ['active', 'trial'] } } // membership
{ 'address.city': 'London' }            // nested field (dot notation)
{ tags: { $all: ['a', 'b'] } }          // array contains all
Queries are documents; operators are `$`-prefixed
You describe *what to match* with a query document. Comparison and logical operators are fields beginning with `$` (`$gt`, `$in`, `$or`, `$exists`). Dot notation (`'address.city'`) reaches into nested objects and arrays. This query-by-example style is expressive but DB-specific — unlike portable SQL, it's MongoDB's own language (covered with the [driver](/nodejs/mongodb-driver) and [Mongoose](/nodejs/mongoose-queries)).
Indexes — non-negotiable for performance

JS
// Without an index, a query SCANS every document (COLLSCAN):
db.users.find({ email: 'ada@example.com' })   // slow on a large collection

// Create an index so lookups are fast (and enforce uniqueness):
db.users.createIndex({ email: 1 }, { unique: true })
db.orders.createIndex({ userId: 1, createdAt: -1 })   // compound
Unindexed queries scan the whole collection — add indexes early
A query with no supporting index performs a **collection scan**, reading every document — fine with 100 docs, catastrophic with 10 million. Index the fields you filter and sort on (a `unique` index also enforces no-duplicates, e.g. on `email`). Indexes cost write performance and storage, so don't index everything — but the fields behind your common queries are mandatory. Use `.explain()` to confirm an index is actually used.
The aggregation pipeline (a glimpse)

JS
// Transform/summarize data through a sequence of stages:
db.orders.aggregate([
  { $match: { status: 'paid' } },                       // filter
  { $group: { _id: '$userId', total: { $sum: '$amount' } } }, // group + sum
  { $sort: { total: -1 } },                             // sort
  { $limit: 10 },                                       // top 10 spenders
])
Aggregation is MongoDB's answer to GROUP BY and joins
For analytics, grouping, and joins (`$lookup`), MongoDB uses the **aggregation pipeline** — an array of stages that each transform the stream of documents. It's powerful (the equivalent of SQL's `GROUP BY`, `JOIN`, and more) but has its own syntax to learn. You'll reach for it whenever a simple `find` can't express the question.
Running MongoDB
  • MongoDB Atlas — the managed cloud service; easiest for production and a free tier for learning.

  • Dockerdocker run -d -p 27017:27017 mongo for a local instance.

  • Local install — the Community Server for your OS.

  • Connect from Node with the official driver or the Mongoose ODM.

Next
Connect and run CRUD with the official driver: [Using the MongoDB Driver](/nodejs/mongodb-driver).