Paging
Pagination is essential for APIs and user-facing applications. MongoDB provides three methods to implement paging: sort(), limit(), and skip().
const page = 2;
const numPerPage = 20;
const results = await collection
.find({ year: 2020 })
.sort({ title: 1 })
.limit(numPerPage)
.skip(page * numPerPage)
.toArray();
The basic formula is skip(pageNumber * documentsPerPage). For page 0 (first page), skip is 0. For page 1, skip is 20 if showing 20 per page.
Cursor-Based Paging
For large datasets, cursor-based paging is more efficient than skip-based paging because skip still needs to scan through all skipped documents.
async function getNextPage(lastSeenId, limit = 20) {
const query = lastSeenId ? { _id: { $gt: lastSeenId } } : {};
return await collection
.find(query)
.sort({ _id: 1 })
.limit(limit)
.toArray();
}
// First page
const page1 = await getNextPage(null);
// Second page
const page2 = await getNextPage(page1[page1.length - 1]._id);
Basic Writes
Upsert combines insert and update into a single atomic operation. If a matching document exists, it updates; otherwise, it creates a new document.
const upsertResult = await theaters.updateOne(
{
"location.address": newTheaterDoc.location.address,
},
{
$set: newTheaterDoc,
},
{ upsert: true },
);
console.log(
upsertResult.upsertedCount > 0
? "Inserted new theater"
: "Updated existing theater",
);
Insert Operations
insertOne inserts a single document into a collection. The driver returns an InsertOneResult containing the inserted document’s _id.
const { MongoClient } = require("mongodb");
async function addMovie() {
const client = new MongoClient("mongodb://localhost:27017");
try {
await client.connect();
const db = client.db("mflix");
const movies = db.collection("movies");
const newMovie = {
title: "The Lost City of Z",
year: 2016,
genres: ["Adventure", "Biography"],
cast: ["Charlie Hunnam", "Robert Pattinson", "Sienna Miller"],
runtime: 141,
};
const result = await movies.insertOne(newMovie);
console.log(`Inserted movie with _id: ${result.insertedId}`);
} catch (err) {
console.error("Insert failed:", err.message);
if (err.code === 11000) {
console.error("Duplicate key error.");
}
} finally {
await client.close();
}
}
insertMany inserts an array of documents. The driver returns an InsertManyResult with all insertedIds.
async function addBatchMovies() {
const client = new MongoClient("mongodb://localhost:27017");
try {
await client.connect();
const db = client.db("mflix");
const movies = db.collection("movies");
const batch = [
{ title: "Moonlight", year: 2016, genres: ["Drama"] },
{ title: "Arrival", year: 2016, genres: ["Sci-Fi", "Drama"] },
{ title: "La La Land", year: 2016, genres: ["Musical", "Romance"] },
];
const result = await movies.insertMany(batch, { ordered: false });
console.log(`Inserted ${result.insertedCount} movies`);
} catch (err) {
if (err.writeErrors) {
console.error(`${err.writeErrors.length} inserts failed`);
err.writeErrors.forEach((e) =>
console.error(` - Doc ${e.index}: ${e.errmsg}`),
);
}
} finally {
await client.close();
}
}
When using ordered: false, MongoDB continues inserting even if some documents fail. This is useful for bulk imports where individual failures should not abort the entire batch.
Write Concerns
Write concern describes the level of acknowledgment requested from MongoDB for write operations. In strict mode, the driver ensures most writes succeed before returning a success message.
Write concerns provide different levels of write durability in your application.
// w: number of nodes that must acknowledge
writeConcern: { w: 1 }
- w: 1 — Only requests an acknowledgement that one node (the primary) applied the write. This is the default writeConcern in MongoDB.
- w: “majority” — Requires acknowledgement from a majority of replica set members. This provides stronger durability guarantees.
writeConcern: { w: "majority" }
// w: 0 — No acknowledgement. The driver does not wait for confirmation.
writeConcern: { w: 0 }
Using w: 0 means the application does not wait for a write confirmation. This provides the lowest latency but offers no durability guarantee. Use this for non-critical operations such as analytics logging where data loss is acceptable.
// Combining wtimeout with write concern
writeConcern: { w: "majority", wtimeout: 5000 }
The wtimeout option specifies a time limit (in milliseconds) for the write concern. If the write cannot be acknowledged within the timeout, MongoDB returns an error. This prevents indefinite blocking when replica set members are unavailable.
Query Operators
MongoDB provides a rich set of query operators for filtering documents.
Comparison Operators
// $gt — greater than
const moviesAfter2000 = await movies
.find({ year: { $gt: 2000 } })
.toArray();
// $gte — greater than or equal
const moviesFrom2000 = await movies
.find({ year: { $gte: 2000, $lte: 2010 } })
.toArray();
// $lt — less than
const shortMovies = await movies
.find({ runtime: { $lt: 60 } })
.toArray();
// $lte — less than or equal
const affordable = await products
.find({ price: { $lte: 29.99 } })
.toArray();
// $ne — not equal
const notDrama = await movies
.find({ genres: { $ne: "Drama" } })
.toArray();
$in and $nin Operators
// $in — matches any value in an array
const selectedGenres = await movies
.find({ genres: { $in: ["Action", "Thriller", "Sci-Fi"] } })
.toArray();
// $in with an array of exact matches
const byMultipleIds = await movies
.find({ _id: { $in: [id1, id2, id3] } })
.toArray();
$regex Operator
// Case-insensitive search
const starWarsMovies = await movies
.find({ title: { $regex: /star wars/i } })
.toArray();
// Starting with a prefix
const startingWithThe = await movies
.find({ title: { $regex: /^The / } })
.toArray();
// $regex with options string syntax
const containsMatrix = await movies
.find({ title: { $regex: "matrix", $options: "i" } })
.toArray();
Logical Operators
// $and — all conditions must match
const actionMoviesFrom90s = await movies
.find({
$and: [{ genres: "Action" }, { year: { $gte: 1990, $lt: 2000 } }],
})
.toArray();
// $or — at least one condition must match
const moviesByYear = await movies
.find({
$or: [{ year: 1999 }, { year: 2001 }],
})
.toArray();
// $not — negates a query operator
const notStartingWith = await movies
.find({ title: { $not: /^The/ } })
.toArray();
Element Operators
// $exists — field exists
const moviesWithMetacritic = await movies
.find({ metacritic: { $exists: true } })
.toArray();
// $type — field is of a specific BSON type
const moviesWithNumericRating = await movies
.find({ rated: { $type: "string" } })
.toArray();
Array Operators
// $size — array has exact length
const moviesWithThreeGenres = await movies
.find({ genres: { $size: 3 } })
.toArray();
// $all — array contains all specified elements
const actionComedy = await movies
.find({ genres: { $all: ["Action", "Comedy"] } })
.toArray();
// $elemMatch — at least one array element matches all conditions
const reviewsWithHighScore = await movies
.find({
reviews: {
$elemMatch: { score: { $gte: 8 }, author: "Verified" },
},
})
.toArray();
Projection
Projection controls which fields MongoDB returns in query results. This reduces network overhead and improves performance.
// Include only specific fields (1 = include)
const movieTitles = await movies
.find({}, { projection: { title: 1, year: 1, _id: 0 } })
.toArray();
// Exclude specific fields (0 = exclude)
const moviesWithoutComments = await movies
.find({}, { projection: { comments: 0 } })
.toArray();
// Projection in embedded documents
const userContacts = await users
.find(
{},
{ projection: { name: 1, "address.city": 1, "address.country": 1 } },
)
.toArray();
Rules for projection:
- You can either include fields (set to 1) or exclude fields (set to 0), but not a mix except for
_id. _idis included by default. Set_id: 0to exclude it.- Excluding fields is useful when documents have many large fields you do not need.
Sorting
Sorting determines the order of returned documents.
// Ascending sort (1) by a single field
const moviesByYearOldest = await movies
.find({})
.sort({ year: 1 })
.toArray();
// Descending sort (-1)
const moviesByYearNewest = await movies
.find({})
.sort({ year: -1 })
.toArray();
// Compound sort — multiple sort keys
const sortedMovies = await movies
.find({})
.sort({ year: -1, title: 1 })
.toArray();
Sorting with indexes is critical for performance. An unindexed sort triggers a blocking sort operation that fails if it exceeds 32 MB of memory.
// Create an index to support the sort
await movies.createIndex({ year: -1, title: 1 });
// Now sort uses the index efficiently
const efficientSort = await movies
.find({ year: { $gte: 2000 } })
.sort({ year: -1, title: 1 })
.toArray();
Basic Updates
MongoDB provides two primary update methods and a variety of update operators.
updateOne() // Updates the first matching document
updateMany() // Updates all matching documents
Update Operators
// $set — sets the value of a field
await movies.updateOne(
{ title: "The Matrix" },
{ $set: { year: 1999, runtime: 136 } },
);
// $inc — increments a numeric field
await movies.updateOne(
{ title: "The Matrix" },
{ $inc: { "ratings.count": 1 } },
);
// $push — appends an element to an array
await movies.updateOne(
{ title: "The Matrix" },
{ $push: { genres: "Cyberpunk" } },
);
// $pull — removes elements from an array
await movies.updateOne(
{ title: "The Matrix" },
{ $pull: { genres: "Action" } },
);
// $addToSet — adds an element only if it does not already exist
await movies.updateOne(
{ title: "The Matrix" },
{ $addToSet: { genres: "Sci-Fi" } },
);
// $unset — removes a field from a document
await movies.updateMany(
{ metacritic: { $exists: false } },
{ $unset: { obsoleteField: "" } },
);
// $rename — renames a field
await movies.updateMany(
{},
{ $rename: { "imdb.rating": "imdbRating" } },
);
Upsert Pattern
Upsert is particularly useful for counters, session tracking, and ensuring idempotent writes.
async function incrementViewCount(movieId) {
const result = await db.collection("views").updateOne(
{ movieId, date: today },
{ $inc: { count: 1 } },
{ upsert: true },
);
if (result.upsertedCount > 0) {
console.log("Created new view counter");
} else {
console.log("Incremented existing counter");
}
}
Basic Deletes
When performing a delete, MongoDB modifies the collection data, updates relevant indexes, and records the operation in the oplog for replication.
// deleteOne — deletes the first matching document in $natural order
const deleteResult = await movies.deleteOne({ title: "The Lost City of Z" });
console.log(`Deleted ${deleteResult.deletedCount} document(s)`);
// deleteMany — deletes all matching documents
const bulkDeleteResult = await movies.deleteMany({
year: { $lt: 1950 },
});
console.log(`Deleted ${bulkDeleteResult.deletedCount} old movies`);
Safe Delete Patterns
async function safeDelete(collection, filter, options = {}) {
const doc = await collection.findOne(filter);
if (!doc) {
throw new Error("Document not found");
}
// Archive before deleting
await collection.bulkWrite([
{
insertOne: {
document: { ...doc, deletedAt: new Date(), originalId: doc._id },
},
},
{ deleteOne: { filter: { _id: doc._id } } },
]);
return { archived: true, doc };
}
Real-World Use Cases
E-Commerce Product Search
async function searchProducts(filters) {
const query = {};
if (filters.category) {
query.category = filters.category;
}
if (filters.minPrice || filters.maxPrice) {
query.price = {};
if (filters.minPrice) query.price.$gte = filters.minPrice;
if (filters.maxPrice) query.price.$lte = filters.maxPrice;
}
if (filters.tags && filters.tags.length > 0) {
query.tags = { $all: filters.tags };
}
if (filters.searchTerm) {
query.$or = [
{ name: { $regex: filters.searchTerm, $options: "i" } },
{ description: { $regex: filters.searchTerm, $options: "i" } },
];
}
const sortOption = {};
if (filters.sortBy === "price") {
sortOption.price = filters.sortOrder === "desc" ? -1 : 1;
} else if (filters.sortBy === "rating") {
sortOption.rating = -1;
}
const projection = {
name: 1,
price: 1,
rating: 1,
category: 1,
"inventory.stock": 1,
};
const skip = (filters.page || 0) * (filters.limit || 20);
return await collection
.find(query)
.project(projection)
.sort(sortOption)
.skip(skip)
.limit(filters.limit || 20)
.toArray();
}
Aggregation Pipeline Combined with CRUD
async function getMovieRecommendations(userId) {
const user = await db.collection("users").findOne({ _id: userId });
const userGenres = user.preferredGenres || [];
const pipeline = [
{
$match: {
genres: { $in: userGenres },
year: { $gte: 2015 },
},
},
{
$addFields: {
genreMatchCount: {
$size: { $setIntersection: ["$genres", userGenres] },
},
},
},
{ $sort: { genreMatchCount: -1, "imdb.rating": -1 } },
{ $limit: 10 },
{
$project: {
title: 1,
year: 1,
genres: 1,
"imdb.rating": 1,
poster: 1,
genreMatchCount: 1,
},
},
];
return await db.collection("movies").aggregate(pipeline).toArray();
}
Comments