Your backend speaks MongoDB but every time you write a find() or insertOne() you feel there's room for improvement? That happens. CRUD looks trivial, but missing an index, forgetting a try/catch, or not handling connection pooling costs a lot in production. We at Meteora Web have seen projects slow down by 300% due to a poorly written query and data lost because of missing atomicity. This guide takes you from basic CRUD to what actually works in production, using Node.js and Python drivers — ready to copy and paste into your code.
CRUD Is Not Just Insert-Read-Update-Delete
Let's start with a concrete scenario: you manage a product catalog for an e-commerce site. Daily, new items arrive, prices change, products get discontinued. Everything runs fine during development. Then in production, traffic spikes: 10,000 requests per minute, write operations timeout, the database crashes from too many open connections. The problem isn't MongoDB — it's how you talk to it. Operational CRUD isn't just writing working queries; it's writing efficient, atomic, and resilient queries.
Sponsored Protocol
Connection Management: The First Real CRUD
Never open a new connection for each operation. Use a shared connection pool. Here's the proper approach for Node.js with the mongodb driver:
const { MongoClient } = require('mongodb');
const uri = process.env.MONGODB_URI;
const client = new MongoClient(uri, {
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000
});
async function connectDB() {
if (!client.isConnected()) await client.connect();
return client.db('ecommerce');
}
module.exports = { connectDB };
For Python with pymongo:
from pymongo import MongoClient
import os
uri = os.getenv('MONGODB_URI')
client = MongoClient(uri, maxPoolSize=10, serverSelectionTimeoutMS=5000)
def get_db():
return client['ecommerce']
Why it pays off: the connection pool avoids opening and closing sockets per request. Average response time drops by 70% compared to one-shot connections.
Create: InsertOne and InsertMany with Control
When inserting documents, never trust input data without validation. A price field as a string instead of a number can break all aggregations. We almost always use insertMany with ordered: false for large batches: if one document fails, the rest still get inserted.
Sponsored Protocol
Node.js: ordered vs unordered insertMany
const db = await connectDB();
const products = db.collection('products');
// Bulk insert with ordered: false
const docs = [
{ sku: 'P001', name: 'T-shirt', price: 29.99 },
{ sku: 'P002', name: 'Jeans', price: 79.99 },
{ sku: 'P001', name: 'Duplicate' } // will fail on unique index, but rest pass
];
try {
const result = await products.insertMany(docs, { ordered: false });
console.log(`Inserted ${result.insertedCount} documents`);
} catch (e) {
console.error('Errors on some documents:', e.writeErrors);
}
Python: insert_many with error handling
db = get_db()
products = db['products']
docs = [
{'sku': 'P001', 'name': 'T-shirt', 'price': 29.99},
{'sku': 'P002', 'name': 'Jeans', 'price': 79.99},
{'sku': 'P001', 'name': 'Duplicate'}
]
try:
result = products.insert_many(docs, ordered=False)
print(f"Inserted {len(result.inserted_ids)} documents")
except pymongo.errors.BulkWriteError as e:
print('Failed documents:', e.details['writeErrors'])
Common mistake: not handling BulkWriteError crashes the app. With ordered: false you avoid total blocks.
Sponsored Protocol
Read: find, findOne and Efficient Projections
Reading without criteria is the fastest way to make the database cry. Always use projections to return only the fields you need and filters with indexes. In an e-commerce product listing, there's no need to include long description fields.
Node.js: query with projection and limit
const result = await products.find(
{ price: { $gte: 10, $lte: 100 } },
{ projection: { name: 1, price: 1, _id: 0 } }
).limit(20).toArray();
Python: same
result = list(products.find(
{'price': {'$gte': 10, '$lte': 100}},
{'name': 1, 'price': 1, '_id': 0}
).limit(20))
Warning: without limit in production, MongoDB reads all matching documents and then discards 20. Waste of I/O.
Update: updateOne, updateMany and Atomicity
When updating stock, you must be sure two simultaneous requests don't sell the last item twice. Use atomic operators like $inc and $set.
Sponsored Protocol
Node.js: atomic decrement with guard
const result = await products.updateOne(
{ sku: 'P001', stock: { $gt: 0 } },
{ $inc: { stock: -1 } }
);
if (result.modifiedCount === 0) {
console.log('Product out of stock or not found');
}
Python: same
result = products.update_one(
{'sku': 'P001', 'stock': {'$gt': 0}},
{'$inc': {'stock': -1}}
)
if result.modified_count == 0:
print('Product out of stock or not found')
Why it pays off: the $inc operator is atomic. If two requests arrive simultaneously, each decreases by 1. No duplicates.
Delete: deleteOne, deleteMany and More
Deleting is dangerous. We almost always use soft delete via a deletedAt field, especially in fiscal contexts (and we say this as former accountants). But when a true delete is necessary, do it with awareness.
Node.js: conditional deleteOne
const result = await products.deleteOne({ sku: 'P999', deletedAt: { $exists: false } });
if (result.deletedCount === 0) console.log('Document not found');
Python: same
result = products.delete_one({'sku': 'P999', 'deletedAt': {'$exists': False}})
if result.deleted_count == 0:
print('Document not found')
Common Mistakes We See in Client Projects
We've inherited projects where:
Sponsored Protocol
- Connections were opened and closed per request (slow).
- No index on
sku(every search was a full collection scan). - Bulk writes were ordered (one error rejected the whole batch).
- No check on
modifiedCount— updates failed silently.
For a comprehensive overview of design patterns, read our pillar guide on MongoDB and NoSQL.
In Summary — What to Do Now
- Review connection management: use a shared pool in both drivers.
- Always insert with
ordered: falsein batches and handleBulkWriteError. - Read with projection and limit — avoid carrying useless data.
- Use atomic operators (
$inc,$set,$push) for concurrent updates. - Prefer soft delete with a
deletedAtfield for reversibility.
CRUD is never just CRUD. It's the foundation of your system's solidity. We, at Meteora Web, see it every day: an app that writes well scales well. If you want to dive deeper into driver specifics, check the official documentation for Node.js or PyMongo.