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 ObjectId
s 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 ObjectId
s on your server.
In general, it’s often simpler to use string
s for the _id
field when communicating between your web client and server. This avoids the need for conversion between ObjectId
s 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 validObjectId
, which can help prevent errors and improve data integrity. - It makes it clear to other developers that the
id
field is anObjectId
, 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
ObjectId
s 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 toObjectId
s 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 } } ])