MongoDB with NodeJS

This article describes how to use MongoDB NodeJS driver to manage your data. It does not mention anything about Mongoose as a Object Data Modelling library. MongoDb node 5.1 is used.

Establishing connections

Establish a connection

import { MongoClient } from 'mongodb';

// Connection URL
const url = 'mongodb://localhost:27017';

// Database Name
const dbName = 'myproject';

// Create a new MongoClient
const client = new MongoClient(url);

async function main() {
  // Use connect method to connect to the server
  await client.connect();
  await client.db("admin").command({ ping: 1 });
  console.log("Pinged your deployment. You successfully connected to MongoDB!");

  // const db = client.db(dbName);
  // let r = await db.collection('inserts').insertOne({ a: 1 });
  // console.log(r.insertedCount);

  // Close connection
  await client.close();
}

main();

Establish a connection to a MongoDB replica set

Creating

Creating, adding or inserting are used interchangeably here.

Creating a database

Databases cannot be created separately on their own, instead they are created implicitly as soon as one document is created within a collection:

await client.db("my-db").collection("my-collection").insertOne({
  hello: "World",
});

Creating a collection (default)

A collection is implicitly created as soon as one document is created within the collection:

await client.db("my-db").collection("my-collection").insertOne({
  hello: "World",
});

Creating a collection with custom options

const myCollection = client.db("my-db").createCollection("my-collection", {autoIndexId: false});

Creating a document

Creating a document automatically creates a _id property of type ObjectId.

await client.db("my-db").collection("my-collection").insertOne({
  hello: "World",
});
{
  acknowledged: true,
  insertedId: new ObjectId("641797db6e7a74847c6ae06a")
}

Creating an item in a document array property

See updating a document array property

Creating multiple documents

const myCollection = client.db("my-db").collection("my-collection");
const result = await myCollection.insertMany([
  {a: "1"},
  {b: "2"},
  {c: "3"},
]);

Reading

Reading, finding, filtering, selecting, getting, retrieving are used interchangeably here. Pagination is a special case that is its own section.

Finding all documents within a collection

Finding all documents in a collection with find() or find({}) returns a cursor not the results. Only after you call toArray(), next() or forEach() on the cursor will you get the results.

const myCollection = client.db("my-db").collection("my-collection");
await myCollection.insertMany([{ a: "1" }, { b: "2" }, { c: "3" }]);
const cursor = myCollection.find({});
const result = await cursor.toArray();

// const result = await cursor.next();
// const result = await cursor.forEach((item) => console.log(item));
[
  { _id: new ObjectId("64179df46045dbc940128e58"), a: '1' }
  { _id: new ObjectId("64179df46045dbc940128e59"), b: '2' }
  { _id: new ObjectId("64179df46045dbc940128e5a"), c: '3' }
]

Finding all documents that match a property’s value exactly

const cursor = myCollection.find({ b: "2" });

Finding documents with or-condition

See “Finding all documents that match a property’s value partly” for an example.

Finding all documents that match a property’s value partly

const myCollection = client.db("my-db").collection("my-collection");
await myCollection.insertMany([{ name: "ananas" }, { name: "banana" }, { name: "citrus" }]);
const cursor = myCollection.find({
  name: { $regex: /ana/ },
});
const result = await cursor.toArray();

Results in:

[
  { _id: new ObjectId("6417a2aefd768bb94e500657"), name: 'ananas' },
  { _id: new ObjectId("6417a2aefd768bb94e500658"), name: 'banana' }
]

Finding at most one document that match a property’s value

In contrast to find() the findOne() method does not return a cursor and only at most a single result, even if the filter criteria matches many documents:

const myCollection = client.db("my-db").collection("my-collection");
await myCollection.insertMany([{ name: "ananas" }, { name: "banana" }, { name: "citrus" }]);
const result = await myCollection.findOne({
  name: "banana",
});
{ _id: new ObjectId("6417a3855745e977407cd6a6"), name: 'banana' }

Finding at most x numbers of documents (limit)

Because we use find() only a cursor is returned, not the results. limit() is added to the cursor, limit the results to max 2.

const myCollection = client.db("my-db").collection("my-collection");
await myCollection.insertMany([{ name: "ananas" }, { name: "banana" }, { name: "citrus" }]);
const cursor = myCollection.find({}).limit(2);
const result = await cursor.toArray();
[
  { _id: new ObjectId("6417a472595fb8f9a0b7489a"), name: 'ananas' },
  { _id: new ObjectId("6417a472595fb8f9a0b7489b"), name: 'banana' }
]

Counting documents

// Do not use, because deprecated:
const cursor = myCollection.find({}).count();

// instead use
const myCollection = client.db("my-db").collection("my-collection");
await myCollection.insertMany([{ name: "ananas" }, { name: "banana" }, { name: "citrus" }]);
const result = await myCollection.countDocuments(); // returns 3

// or pass in a filter
const result = await myCollection.countDocuments({ name : {$regex: /ana/}});

Reading the id of a created document

const result = await myCollection.insertOne({
  myArray: [],
});

const id = result.insertedId;

Paginating

Cursor-based pagination

import { MongoClient } from 'mongodb';

async function getDocuments(cursor: string, limit: number) {
  const client = await MongoClient.connect('mongodb://localhost:27017');
  const db = client.db('mydb');
  const collection = db.collection('mycollection');

  const results = await collection.find({ _id: { $gt: cursor } })
    .limit(limit)
    .toArray();

  return results;
}

Updating

Updating a document successfully will return something like:

{
  acknowledged: true,
  modifiedCount: 1,
  upsertedId: null,
  upsertedCount: 0,
  matchedCount: 1
}

Updating a document by replacing it

const myCollection = client.db("my-db").collection("my-collection");
await myCollection.insertMany([{ name: "ananas" }, { name: "banana" }, { name: "citrus" }]);
await myCollection.replaceOne({name: "ananas"}, {name: "apple"});
const result = await myCollection.find().toArray();

Update a document or insert if it does not exist (upsert)

This code snippet wants to update banana to apple, but banana does not exist. With upsert: true the document will be created anyway, without upsert or upsert: false the document will not be created. Note, that no error is thrown if the document does not exist.

const myCollection = client.db("my-db").collection("my-collection");
await myCollection.insertOne({ name: "ananas" });
await myCollection.updateOne({name: "banana"}, {$set: { name: "apple" }} , {upsert: true});
const result = await myCollection.find().toArray();

If you want to throw an error instead of inserting a new document, you can set the upsert option to false and check the result.nModified property of the UpdateResult object. If result.nModified is 0, it means that no matching document was found, and you can throw an error.

if (result.nModified === 0) {
  throw new Error('Document not found');
}

Updating a (top-level) document property

const myCollection = client.db("my-db").collection("my-collection");
await myCollection.insertOne({ name: "ananas" });
await myCollection.updateOne({name: "ananas"}, {$set: { name: "apple" }});
const result = await myCollection.find().toArray();

Updating a nested document property

const myCollection = client.db("my-db").collection("my-collection");
await myCollection.insertOne({ name: "ananas" });
await myCollection.updateOne({name: "ananas"}, {$set: { 'fruit.name': "apple" }});
const result = await myCollection.find().toArray();
[
  {
    _id: new ObjectId("6418d6bf3f0d734c7371b731"),
    name: 'ananas',
    fruit: { name: 'apple' }
  }
]

Updating a document array property

const myCollection = client.db("my-db").collection("my-collection");
const result = await myCollection.insertOne({
  myArray: [],
});

const id = result.insertedId;

await myCollection.updateOne(
  { _id: id },
  { $push: { myArray: 27 } }
)

Updating a document number property by incrementing / decrementing

const filter = { item: 'apple' };
const update = { $inc: { quantity: 1 } };
const result = await collection.updateOne(filter, update);

Deleting

Deleting, removing or dropping are used interchangeably here.

Deleting a database

await client.db("my-db").dropDatabase();

A database is also deleted implicitly as soon as a database’s last document is deleted.

Deleting a collection

await client.db("my-db").dropCollection("my-collection");

Trying to delete a non-existing collection results in the error MongoServerError: ns not found.

Deleting all collections

Deleting all collections is the same as deleting a database.

Deleting a document

Deleting a single (top-level) document property

Deleting a nested document property

Deleting an item in a document array property

Deleting an array item means to $pull the field from it.

const result = await collection.updateOne(
  { _id: ObjectId("6068d52265d61428d7e75c31") }, // the ID of the document to update
  { $pull: { hobbies: "reading" } } // the field to remove the item from
);

ObjectId vs string in web clients

If you have defined the _id field as an ObjectId in your input types and are using mongoose.Types.ObjectId in your services and controllers, you should send an ObjectId from your web client. However, since ObjectIds are not natively supported by JSON, you will need to convert them to strings before sending them from your web client and then convert them back to ObjectIds on your server.

In general, it’s often simpler to use strings for the _id field when communicating between your web client and server. This avoids the need for conversion between ObjectIds and strings.

Using an ObjectIdScalar instead of a string for the id field in a NestJS GraphQL object type has some advantages and disadvantages.

Advantages of using an ObjectIdScalar:

  • It ensures that the id field is a valid ObjectId, which can help prevent errors and improve data integrity.
  • It makes it clear to other developers that the id field is an ObjectId, which can improve code readability and maintainability.

Disadvantages of using an ObjectIdScalar:

  • It requires you to create a custom scalar to represent an ObjectId, which can add complexity to your code.
  • Since ObjectIds are not natively supported by JSON, you will need to convert them to strings before sending them from your web client and then convert them back to ObjectIds on your server. This can add additional complexity to your code.

Whether you should use an ObjectIdScalar or a string for the id field depends on your specific use case and requirements. If you are using MongoDB as your database and want to ensure that the id field is a valid ObjectId, using an ObjectIdScalar might be a good choice. On the other hand, if you don’t need this level of validation or if you are not using MongoDB as your database, using a string for the id field might be simpler.

Populating fields from other collections

db.users.aggregate([
  {
    $lookup: {
      from: "posts",  // use 'posts' as collection to lookup data from
      localField: "_id",  // use '_id' as field in local 'users' collection
      foreignField: "author", // use 'author' as foreign field in posts collection that shall match localField
      as: "posts"  // alias for data in users collection
    }
  },
  {
    $project: { // use the $project operator to select the fields from users collection that should be returned from this aggregation
      _id: 1,
      username: 1,
      posts: 1
    }
  }
])

About Author

Mathias Bothe To my job profile

I am Mathias from Heidelberg, Germany. I am a passionate IT freelancer with 15+ years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I create Bosycom and initiated several software projects.