CRUD Operations with MongoDB
What is CRUD?
CRUD stands for Create, Read, Update, Delete — the four fundamental operations you perform on any data store. Every meaningful backend feature maps to one or more of these operations. Registering a user is Create. Displaying a profile is Read. Changing a password is Update. Closing an account is Delete.
In this lesson we will go deep on each operation using Mongoose with MongoDB, covering real-world patterns like filtering, sorting, pagination, and atomic update operators.
Setup: Sample Model
All examples in this lesson use a Product model:
// models/Product.js
const mongoose = require('mongoose');
const productSchema = new mongoose.Schema(
{
name: { type: String, required: true, trim: true },
category: { type: String, enum: ['electronics', 'clothing', 'food', 'books'] },
priceKobo: { type: Number, required: true, min: 0 },
stock: { type: Number, default: 0 },
tags: [String],
isAvailable: { type: Boolean, default: true },
},
{ timestamps: true }
);
module.exports = mongoose.model('Product', productSchema);
Create
Use Model.create() or new Model().save() to insert documents.
// Create a single product
const product = await Product.create({
name: 'Wireless Headphones',
category: 'electronics',
priceKobo: 1500000, // 15,000 naira
stock: 50,
tags: ['audio', 'wireless', 'bluetooth'],
});
// Create multiple products at once
const products = await Product.insertMany([
{ name: 'T-Shirt', category: 'clothing', priceKobo: 250000, stock: 200 },
{ name: 'Novel', category: 'books', priceKobo: 150000, stock: 80 },
]);
insertMany() is much faster than calling create() in a loop because it sends a single network request to MongoDB.
Read
find() — Retrieve Multiple Documents
// All products
const all = await Product.find();
// Only electronics that are available
const electronics = await Product.find({
category: 'electronics',
isAvailable: true,
});
// Products costing more than 5,000 naira (500,000 kobo)
const expensive = await Product.find({ priceKobo: { $gt: 500000 } });
Projection — Select Specific Fields
// Return only name and priceKobo — exclude _id and everything else
const slim = await Product.find({}, { name: 1, priceKobo: 1, _id: 0 });
// Result: [{ name: 'Wireless Headphones', priceKobo: 1500000 }, ...]
Sorting
// Sort by price ascending (cheapest first)
const byPrice = await Product.find().sort({ priceKobo: 1 });
// Sort by createdAt descending (newest first)
const newest = await Product.find().sort({ createdAt: -1 });
Pagination with skip() and limit()
Pagination is essential for APIs — you never want to return thousands of records in one response.
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const products = await Product.find()
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit);
const total = await Product.countDocuments();
res.json({
data: products,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
A request to GET /products?page=2&limit=5 returns documents 6 to 10.
Update
updateOne() — Update the First Matching Document
// Reduce stock by 1 when a product is purchased
await Product.updateOne(
{ _id: productId }, // filter
{ $set: { stock: 49 } } // what to change
);
findByIdAndUpdate() — Update and Return the Document
// Update price and return the UPDATED document
const updated = await Product.findByIdAndUpdate(
productId,
{ $set: { priceKobo: 1200000 } },
{ new: true, runValidators: true }
// ^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
// Return the new document | Run schema validators on update
);
Without { new: true }, Mongoose returns the document before the update — which is almost never what you want.
Update Operators
MongoDB has powerful operators for modifying values without fetching the document first:
// $set — update specific fields
await Product.updateOne({ _id: id }, { $set: { isAvailable: false } });
// $inc — increment or decrement a number
await Product.updateOne({ _id: id }, { $inc: { stock: -1 } }); // reduce stock by 1
await Product.updateOne({ _id: id }, { $inc: { stock: 10 } }); // restock 10 units
// $push — add an item to an array
await Product.updateOne({ _id: id }, { $push: { tags: 'sale' } });
// $pull — remove an item from an array
await Product.updateOne({ _id: id }, { $pull: { tags: 'sale' } });
// $addToSet — add to array only if value does not already exist
await Product.updateOne({ _id: id }, { $addToSet: { tags: 'featured' } });
updateMany() — Update All Matching Documents
// Mark all out-of-stock products as unavailable
await Product.updateMany(
{ stock: 0 },
{ $set: { isAvailable: false } }
);
Delete
// Delete one document by ID
await Product.findByIdAndDelete(productId);
// Delete one document matching a filter
await Product.deleteOne({ name: 'Old Product' });
// Delete all products in a category
await Product.deleteMany({ category: 'food' });
Full CRUD Route Handler Example
const express = require('express');
const router = express.Router();
const Product = require('../models/Product');
// READ: GET /products?page=1&limit=10&category=electronics
router.get('/', async (req, res) => {
try {
const { page = 1, limit = 10, category } = req.query;
const filter = category ? { category } : {};
const skip = (page - 1) * limit;
const [products, total] = await Promise.all([
Product.find(filter).sort({ createdAt: -1 }).skip(skip).limit(Number(limit)),
Product.countDocuments(filter),
]);
res.json({ data: products, total, page: Number(page), totalPages: Math.ceil(total / limit) });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// CREATE: POST /products
router.post('/', async (req, res) => {
try {
const product = await Product.create(req.body);
res.status(201).json(product);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// UPDATE: PATCH /products/:id
router.patch('/:id', async (req, res) => {
try {
const product = await Product.findByIdAndUpdate(
req.params.id,
{ $set: req.body },
{ new: true, runValidators: true }
);
if (!product) return res.status(404).json({ message: 'Product not found' });
res.json(product);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// DELETE: DELETE /products/:id
router.delete('/:id', async (req, res) => {
try {
const product = await Product.findByIdAndDelete(req.params.id);
if (!product) return res.status(404).json({ message: 'Product not found' });
res.json({ message: 'Product deleted successfully' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;
Practice Exercise
Build a fully functional notes API with CRUD and pagination:
- Create a
Notemodel with:title(required),body(required),tags(array of strings),isPinned(boolean, default false), and timestamps - Implement
POST /notesto create a note - Implement
GET /notes?page=1&limit=5with pagination, sorted bycreatedAtdescending - Implement
PATCH /notes/:idusing$setso only provided fields are updated - Implement
DELETE /notes/:id - Bonus: Add
POST /notes/:id/pinthat uses$set: { isPinned: true }to pin a note and$set: { isPinned: false }to unpin it
Try it yourself
Key Takeaways
- CRUD stands for Create, Read, Update, Delete — the four fundamental operations on any data store.
- Use find() with filter objects, sort(), skip(), and limit() to implement powerful querying and pagination.
- Always pass { new: true, runValidators: true } to findByIdAndUpdate() to get the updated document back and enforce schema rules.
- Update operators like $set, $inc, $push, and $pull modify specific parts of a document without replacing the whole thing.
- Pagination using skip() and limit() with countDocuments() is essential for keeping API responses fast and manageable.
Quick Quiz
1.What does passing { new: true } to findByIdAndUpdate() do?
2.Which operator would you use to add an item to an array field only if it does not already exist?
3.In MongoDB pagination, if you have 50 total items and want page 3 with 10 items per page, what skip value should you use?
4.Why is insertMany() preferred over calling create() in a loop?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx