Chapter 73: Node.js MongoDB Update
updating documents (updateOne, updateMany, findOneAndUpdate, etc.) in MongoDB using Node.js + Mongoose.
We are going to learn this topic properly — as if I am sitting next to you, sharing my screen, typing code together, running it, looking at the result in Compass, explaining every decision, warning about very common mistakes, and showing real production patterns that experienced developers use in 2025–2026.
1. Quick reality check: Why UPDATE is one of the most dangerous operations
In real applications:
- Wrong UPDATE → can silently corrupt thousands or millions of documents
- Missing filter → can update the entire collection
- Not using atomic operators ($set, $inc, $push, etc.) → can cause race conditions
- Forgetting to validate → can insert invalid data
- No audit trail → impossible to know who changed what and when
So we will learn safe, auditable, atomic, logged update patterns — not just “how to update one field”.
2. Project setup (realistic & modern)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
mkdir mongodb-update-lesson cd mongodb-update-lesson npm init -y npm pkg set type=module npm install express dotenv mongoose zod npm install -D typescript tsx nodemon @types/express @types/node |
tsconfig.json
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "outDir": "./dist", "rootDir": "./src", "sourceMap": true }, "include": ["src/**/*"] } |
package.json scripts
|
0 1 2 3 4 5 6 7 8 9 |
"scripts": { "dev": "tsx watch src/index.ts", "start": "node dist/index.js" } |
.env
|
0 1 2 3 4 5 6 7 |
PORT=5000 MONGODB_URI=mongodb://localhost:27017/update_demo |
3. Database connection (production-ready)
src/config/mongodb.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import mongoose from 'mongoose' import env from './env.js' export async function connectDB() { try { await mongoose.connect(env.MONGODB_URI, { maxPoolSize: 10, minPoolSize: 2, serverSelectionTimeoutMS: 5000, socketTimeoutMS: 45000, family: 4 }) console.log('MongoDB connected →', mongoose.connection.db.databaseName) } catch (err) { console.error('MongoDB connection failed:', err) process.exit(1) } } connectDB() // Graceful shutdown process.on('SIGTERM', async () => { console.log('SIGTERM → closing MongoDB') await mongoose.connection.close() process.exit(0) }) |
src/config/env.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
import { z } from 'zod' import 'dotenv/config' export const env = z.object({ PORT: z.coerce.number().default(5000), MONGODB_URI: z.string().url().startsWith('mongodb') }).parse(process.env) |
4. Realistic Mongoose model (Task)
src/models/task.model.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
import mongoose from 'mongoose' const taskSchema = new mongoose.Schema({ title: { type: String, required: true, trim: true, maxlength: 150 }, description: { type: String, trim: true, maxlength: 1000 }, priority: { type: String, enum: ['low', 'medium', 'high'], default: 'medium' }, completed: { type: Boolean, default: false }, dueDate: Date, points: { type: Number, default: 0, min: 0 }, user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, lastUpdatedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', default: null } }) taskSchema.pre('save', function (next) { this.updatedAt = new Date() next() }) // Indexes for common queries taskSchema.index({ user: 1, createdAt: -1 }) taskSchema.index({ priority: 1, dueDate: 1 }) export const Task = mongoose.model('Task', taskSchema) |
5. Seed some test data (run once)
src/seed.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import { connectDB } from './config/mongodb.js' import { Task } from './models/task.model.js' async function seed() { await connectDB() await Task.deleteMany({}) await Task.insertMany([ { title: 'Finish quarterly report', priority: 'high', dueDate: new Date('2025-03-05'), user: new mongoose.Types.ObjectId(), points: 100 }, { title: 'Call client', priority: 'medium', completed: true, user: new mongoose.Types.ObjectId(), points: 20 }, { title: 'Design logo', priority: 'high', dueDate: new Date('2025-02-28'), user: new mongoose.Types.ObjectId(), points: 80 } ]) console.log('Test data inserted') process.exit(0) } seed().catch(console.error) |
Run:
|
0 1 2 3 4 5 6 |
npx tsx src/seed.ts |
6. Real UPDATE examples – from basic to advanced
6.1 Basic update – mark task as completed
src/controllers/task.controller.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import { Request, Response } from 'express' import { Task } from '../models/task.model.js' import { AppError } from '../middleware/error.middleware.js' export const markTaskCompleted = async (req: Request<{ id: string }>, res: Response) => { try { const taskId = req.params.id // 1. Find and update (atomic) const updatedTask = await Task.findByIdAndUpdate( taskId, { $set: { completed: true, lastUpdatedBy: req.user?.userId // optional audit field } }, { new: true, // return updated document runValidators: true // enforce schema rules } ) if (!updatedTask) { throw new AppError(404, 'Task not found') } res.json({ success: true, message: 'Task marked as completed', data: updatedTask }) } catch (err) { next(err) } } |
Why findByIdAndUpdate is usually better than findById + save()
- Atomic (no race condition)
- One round-trip to database
- Can use $inc, $push, $pull, etc.
6.2 Update multiple fields + $inc operator
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
export const addPointsToTask = async (req: Request<{ id: string }>, res: Response) => { try { const taskId = req.params.id const { points } = req.body if (typeof points !== 'number' || points <= 0) { throw new AppError(400, 'Points must be a positive number') } const updated = await Task.findByIdAndUpdate( taskId, { $inc: { points }, // atomic increment $set: { lastUpdatedBy: req.user?.userId } }, { new: true } ) if (!updated) throw new AppError(404, 'Task not found') res.json({ success: true, message: `Added ${points} points`, newPoints: updated.points }) } catch (err) { next(err) } } |
6.3 Update many documents (batch update)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
export const completeAllOverdueTasks = async (req: Request, res: Response) => { try { const result = await Task.updateMany( { dueDate: { $lt: new Date() }, completed: false }, { $set: { completed: true, lastUpdatedBy: req.user?.userId } } ) res.json({ success: true, matchedCount: result.matchedCount, modifiedCount: result.modifiedCount }) } catch (err) { next(err) } } |
When to use updateMany
- Bulk status changes
- Reset flags
- Compliance updates
- Re-categorize old records
Step 7 – Soft update + audit trail (very common in production)
Add audit fields to model
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
taskSchema.add({ lastUpdatedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, updateHistory: [{ updatedAt: Date, updatedBy: mongoose.Schema.Types.ObjectId, changes: mongoose.Schema.Types.Mixed }] }) |
Update with history
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
export const updateTaskWithHistory = async (req: Request<{ id: string }>, res: Response) => { try { const taskId = req.params.id const updates = req.body // { title: "...", priority: "..." } const task = await Task.findById(taskId) if (!task) throw new AppError(404, 'Task not found') // Record what changed const changes = Object.keys(updates).reduce((acc, key) => { if (task[key] !== updates[key]) { acc[key] = { old: task[key], new: updates[key] } } return acc }, {} as Record<string, any>) // Apply updates Object.assign(task, updates) task.updateHistory.push({ updatedAt: new Date(), updatedBy: req.user?.userId, changes }) await task.save() res.json({ success: true, data: task }) } catch (err) { next(err) } } |
Why keep update history?
- Audit trail for compliance
- Debugging “who changed what”
- Revert mistaken updates
- Very common in financial, healthcare, legal apps
Step 8 – Summary – MongoDB Update best practices (Node.js 2025–2026)
| Best Practice | Why it matters | Recommended pattern |
|---|---|---|
| Prefer .findOneAndUpdate() | Atomic find + update | Task.findOneAndUpdate(filter, update, { new: true }) |
| Use $set, $inc, $push, etc. | Atomic field operations | $set: { completed: true }, $inc: { points: 10 } |
| Always filter by ownership | Prevents IDOR / unauthorized updates | { _id: id, user: userId } |
| Use transactions for multi-document | Atomicity across documents/collections | session.startTransaction() + commit/abort |
| Prefer soft update + audit trail | Recoverable, auditable, compliance | deletedAt, updateHistory array |
| Use .lean() for read-heavy updates | 2–5× faster when you don’t need Mongoose document | .findOneAndUpdate(…, { lean: true }) |
| Use indexes on filter fields | 10×–1000× faster updates | index: true on user, priority, etc. |
| Validate with Zod first | Better error messages, prevents bad data | schema.parse(req.body) before update |
Which direction would you like to go much deeper into next?
- Full task CRUD (create/read/update/delete + ownership)
- Soft-delete + restore + audit log complete system
- Advanced update patterns (bulk, array operators, positional $)
- Login + JWT authentication with MongoDB
- Pagination + filtering + sorting + search
- Docker + production deployment checklist
Just tell me what you want to build or understand next — I’ll continue with complete, secure, production-ready code and explanations. 😊
