Skip to content

MongoDB CRUD

Documents in, documents out MongoDB's CRUD operations look simple on the surface - insert a JSON object , query with a filter , update some fields , delete what you don't need - but the operators and aggregation pipeline give you query power that rivals SQL once you know how to wield them. The problem is that the same flexibility that makes prototyping fast also makes it easy to write queries that scan every document in a collection because you forgot to add an index on the field you're filtering by

Insert - adding documents

// Insert a single document
db.users.insertOne({
    email: "omar@example.com",
    username: "omar_hacker",
    passwordHash: "$2b$12$LJ3m...",
    role: "user",
    createdAt: new Date()
});

// insertOne returns the inserted _id
// { acknowledged: true, insertedId: ObjectId("...") }

// Insert multiple documents - much faster than individual inserts
db.users.insertMany([
    {
        email: "ali@example.com",
        username: "ali_sec",
        passwordHash: "$2b$12$ABC...",
        role: "user",
        createdAt: new Date()
    },
    {
        email: "khaled@example.com",
        username: "khaled_pwn",
        passwordHash: "$2b$12$DEF...",
        role: "admin",
        createdAt: new Date()
    }
]);

// insertMany with ordered: false - continues on error
db.users.insertMany(
    [
        { email: "user1@example.com", username: "user1" },
        { email: "user2@example.com", username: "user2" },
        { email: ""  /* this will fail validation */ }
    ],
    { ordered: false }
    // Continues inserting user2 even though the third document failed
);

Find - querying documents

// Find all documents (returns a cursor - iterate with forEach or toArray)
db.users.find()

// Find with filter - basic field match
db.users.find({ role: "admin" })

// Find with multiple conditions (implicit AND)
db.users.find({
    role: "user",
    isActive: true
})

// Projection - specify which fields to return
db.users.find(
    { role: "admin" },
    { email: 1, username: 1, _id: 0 }  // 1 = include, 0 = exclude
    // Returns { email: "...", username: "..." } without _id
);

// Counting
db.users.countDocuments({ role: "admin" })

// Sorting and limiting
db.users.find()
    .sort({ createdAt: -1 })  // -1 = descending, 1 = ascending
    .limit(20)
    .skip(0)  // Pagination - gets slow at high skips, use _id-based instead

Query operators

The magic of MongoDB queries is in the operator system - prefixing field conditions with $ symbols gives you comparison, existence, pattern matching, and logical operations that would require verbose CASE statements in SQL

// Comparison operators
db.users.find({ age: { $gt: 18 } })           // age > 18
db.users.find({ age: { $gte: 18, $lte: 65 } }) // 18 <= age <= 65
db.users.find({ role: { $ne: "banned" } })     // role != "banned"
db.users.find({ role: { $in: ["admin", "moderator"] } })  // role in array

// Existence and type
db.users.find({ lastLogin: { $exists: true } })    // Field exists
db.users.find({ phoneNumber: { $exists: false } }) // Field doesn't exist

// Pattern matching with $regex
db.users.find({ username: { $regex: /^ali/i } })
// Case-insensitive: starts with "ali" (Ali, ali, ALI)

// Logical operators
db.users.find({
    $or: [
        { role: "admin" },
        { totalOrders: { $gte: 100 } }
    ]
});

db.users.find({
    $and: [
        { isActive: true },
        { lastLogin: { $gte: new Date("2024-01-01") } }
    ]
});

Update - modifying documents

// Update one document
db.users.updateOne(
    { email: "omar@example.com" },     // Filter
    { $set: { lastLogin: new Date() } }  // Update
);

// Update multiple documents
db.users.updateMany(
    { lastLogin: { $lt: new Date("2023-01-01") } },  // Inactive since 2023
    { $set: { isActive: false } }
);

// Update operators
db.users.updateOne(
    { email: "omar@example.com" },
    {
        $set: { username: "omar_new" },   // Set field
        $unset: { temporaryField: "" },   // Remove field
        $inc: { loginCount: 1 },           // Increment by 1
        $push: { roles: "beta_tester" },   // Add to array
        $pull: { roles: "old_role" }       // Remove from array
    }
);

// Upsert - update if exists, insert if not
db.users.updateOne(
    { email: "newuser@example.com" },
    {
        $set: { username: "newuser", lastLogin: new Date() },
        $setOnInsert: { createdAt: new Date(), isActive: true }
    },
    { upsert: true }  // Creates document if no match found
);

Delete - removing documents

// Delete one document
db.users.deleteOne({ email: "spam@example.com" });

// Delete many documents
db.users.deleteMany({ isActive: false });

// Delete all documents (but not the collection itself)
db.users.deleteMany({});

// Drop the entire collection (table + indexes + everything)
db.users.drop();

// deleteMany with result checking
const result = db.users.deleteMany({ lastLogin: { $lt: new Date("2020-01-01") } });
print(`Deleted ${result.deletedCount} stale accounts`);

Aggregation pipeline

The aggregation pipeline is MongoDB's equivalent of SQL's GROUP BY, JOIN, and window functions combined - it processes documents through a sequence of stages where each stage transforms the data before passing it to the next. This is where MongoDB's query capabilities go from "simple CRUD" to "real analytical power"

// Simple aggregation - group by role and count
db.users.aggregate([
    { $match: { isActive: true } },                    // Filter documents
    { $group: {
        _id: "$role",                                   // Group by role field
        count: { $sum: 1 },                             // Count per group
        avgLoginCount: { $avg: "$loginCount" }          // Average login count
    }},
    { $sort: { count: -1 } }                            // Sort by count descending
]);

// Aggregation with lookup (MongoDB's LEFT JOIN)
db.orders.aggregate([
    { $match: { status: "delivered" } },
    { $lookup: {
        from: "users",                                   // Collection to join
        localField: "userId",                            // Field from orders
        foreignField: "_id",                             // Field from users
        as: "user"                                       // Output array field
    }},
    { $unwind: "$user" },                                // Unwind the array
    { $project: {
        orderId: "$_id",
        userEmail: "$user.email",
        totalAmount: 1,
        createdAt: 1
    }}
]);

// Aggregation with computed fields
db.orders.aggregate([
    { $addFields: {
        year: { $year: "$createdAt" },
        month: { $month: "$createdAt" },
        discountedTotal: {
            $multiply: ["$totalAmount", { $subtract: [1, "$discountPercent"] }]
        }
    }},
    { $group: {
        _id: { year: "$year", month: "$month" },
        revenue: { $sum: "$discountedTotal" },
        orderCount: { $sum: 1 }
    }},
    { $sort: { "_id.year": -1, "_id.month": -1 } }
]);

NoSQL injection - the MongoDB attack surface

The same way SQL injection exploits string concatenation in SQL queries, NoSQL injection exploits the fact that MongoDB query operators like $gt, $ne, $regex, and $where can be injected through unsanitized user input

// VULNERABLE - direct interpolation of user input into query
app.post('/login', async (req, res) => {
    const { username, password } = req.body;
    // If attacker sends: { "username": { "$gt": "" }, "password": { "$gt": "" } }
    // This returns the FIRST user in the collection - authentication bypassed
    const user = await db.collection('users').findOne({
        username: username,
        passwordHash: hash
    });
});

// SECURE - type-check and sanitize
app.post('/login', async (req, res) => {
    const { username, password } = req.body;

    // Reject non-string values - operators are objects, not strings
    if (typeof username !== 'string' || typeof password !== 'string') {
        return res.status(400).json({ error: 'Invalid input' });
    }

    const user = await db.collection('users').findOne({
        username: username,
        passwordHash: hash
    });
});

prerequisites

db_06_mongo_intro.md - you need MongoDB running (mongosh accessible) to follow along with the examples, otherwise the aggregation pipeline section is just abstract theory


next → db_08_mongo_indexes.md