Skip to content

MongoDB With Node.js

MongoDB is the most widely used NoSQL database in the Node.js ecosystem, storing flexible, JSON-like documents. This lesson covers connecting and querying MongoDB using the official mongodb driver.

Connecting to MongoDB

MongoDB stores data as "documents", JSON-like objects, grouped into "collections" (roughly analogous to tables in SQL). The official mongodb npm package provides a MongoClient for connecting and a query API that closely mirrors MongoDB's native query language.

Unlike SQL, MongoDB documents in the same collection don't need identical fields, which makes it a natural fit for rapidly evolving or naturally nested data, at the cost of the database itself enforcing less structure than a relational schema would.

import { MongoClient } from 'mongodb';

const client = new MongoClient(process.env.MONGODB_URI);
await client.connect();

const db = client.db('shop');
const products = db.collection('products');

const result = await products.insertOne({ name: 'Keyboard', price: 49.99 });
console.log('Inserted with id:', result.insertedId);

client.db(name) selects a database, and .collection(name) selects a collection within it, both are lightweight handles, not new connections.

Core MongoDB Operations

collection.insertOne(doc)
collection.find(filter).toArray()
collection.findOne(filter)
collection.updateOne(filter, { $set: fields })
collection.deleteOne(filter)
  • insertOne()/insertMany() add new documents to a collection.
  • find() returns a cursor, call .toArray() to materialize it into an array of documents.
  • updateOne() requires an update operator like $set, you can't just pass the new fields directly.
  • Filters use plain objects matching field values, e.g. { name: 'Keyboard' }.

MongoDB Driver Cheatsheet

The operations you'll use in almost every MongoDB-backed Node.js app.

Task Code
Insert one collection.insertOne(doc)
Find all collection.find({}).toArray()
Find with filter collection.find({ price: { $gt: 20 } }).toArray()
Find one collection.findOne({ _id: id })
Update fields collection.updateOne(filter, { $set: fields })
Delete one collection.deleteOne(filter)

Working With ObjectId

MongoDB automatically assigns each document a unique _id field of type ObjectId, not a plain string. When accepting an ID from a URL parameter (a string), you must convert it to an ObjectId before using it in a filter, or the query will silently match nothing.

import { ObjectId } from 'mongodb';

app.get('/products/:id', async (req, res) => {
  const product = await products.findOne({ _id: new ObjectId(req.params.id) });
  if (!product) return res.status(404).json({ error: 'Not found' });
  res.json(product);
});

Wrap the ObjectId conversion in a try/catch, an invalid ID format throws an error rather than simply returning no match.

Querying With MongoDB Operators

MongoDB's query language uses special $-prefixed operators for comparisons and logic beyond simple equality, letting you express ranges, inclusion checks, and combined conditions directly in a filter object.

products.find({
  price: { $gte: 10, $lte: 100 },
  category: { $in: ['electronics', 'accessories'] },
}).toArray();

Common Mistakes

  • Passing a raw string ID from a URL parameter directly into a filter instead of converting it to an ObjectId.
  • Calling updateOne() with plain fields instead of wrapping them in $set, which replaces the entire document unexpectedly.
  • Forgetting .toArray() after find(), and trying to use the cursor as if it were already an array.
  • Not closing or reusing the MongoClient connection properly, opening a new one per request.

Key Takeaways

  • MongoDB stores flexible, JSON-like documents grouped into collections.
  • The mongodb driver's API (insertOne, find, updateOne, deleteOne) closely mirrors MongoDB's native query language.
  • _id fields are ObjectId instances and must be explicitly converted from string IDs.
  • Update operations require operators like $set, plain field objects won't work as expected.

Pro Tip

Create a single shared MongoClient instance when your app starts and reuse it for the life of the process, the driver manages its own internal connection pool, so you don't need (or want) to create a new client per request.