Chapter 71: Node.js MongoDB Delete
DELETE operations in MongoDB using Node.js (with Mongoose – the most common & production-ready approach in 2025–2026).
We will go step by step, as if I’m sitting next to you right now:
- I open VS Code + terminal
- We create files one by one
- We run the code live
- We look at the console output and MongoDB Compass together
- I explain every decision, every line, why we do it this way
- I show what most beginners do wrong (very dangerous in real apps)
- I show what intermediate developers often forget (leads to bugs or security issues)
- I show what real production code actually looks like in serious Node.js applications today
Goal of this lesson
Learn how to safely, correctly, atomically and audibly delete documents in MongoDB from a Node.js application.
We will cover:
- Hard delete (.deleteOne(), .deleteMany(), .findOneAndDelete())
- Soft delete (most recommended in production)
- Delete with ownership check (prevents IDOR attacks)
- Delete with transaction (when deleting from multiple collections)
- Delete with audit logging
- Safe deletion patterns & rollback
- Performance & index considerations
Step 1 – Project setup (modern & realistic)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
mkdir task-api-mongodb-delete cd task-api-mongodb-delete npm init -y npm pkg set type=module # Core dependencies npm install express dotenv mongoose zod jsonwebtoken bcryptjs # Dev dependencies npm install -D \ typescript @types/node @types/express \ tsx nodemon \ eslint prettier eslint-config-standard-with-typescript \ @typescript-eslint/parser @typescript-eslint/eslint-plugin |
tsconfig.json (strict & modern)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitAny": true, "skipLibCheck": true, "outDir": "./dist", "rootDir": "./src", "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } |
package.json scripts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
"scripts": { "dev": "tsx watch src/index.ts", "start": "node dist/index.js", "build": "tsc", "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", "format": "prettier --write .", "typecheck": "tsc --noEmit" } |
.env.example
|
0 1 2 3 4 5 6 7 8 9 |
PORT=5000 NODE_ENV=development MONGODB_URI=mongodb://localhost:27017/taskdb JWT_SECRET=your-very-long-random-secret-here |
Step 2 – MongoDB connection (production-safe)
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 connectMongoDB() { 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) } } connectMongoDB() // Graceful shutdown process.on('SIGTERM', async () => { console.log('SIGTERM → closing MongoDB') await mongoose.connection.close() process.exit(0) }) |
Step 3 – 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 |
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, user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true }, deletedAt: { type: Date, default: null, index: true }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }) taskSchema.pre('save', function (next) { this.updatedAt = new Date() next() }) export const Task = mongoose.model('Task', taskSchema) |
Note: We added deletedAt field — we will use soft delete (recommended in production).
Step 4 – Basic hard delete (.deleteOne())
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 |
import { Request, Response } from 'express' import { Task } from '../models/task.model.js' import { AppError } from '../middleware/error.middleware.js' export const hardDeleteTask = async (req: Request<{ id: string }>, res: Response) => { try { const taskId = req.params.id const deleted = await Task.deleteOne({ _id: taskId }) if (deleted.deletedCount === 0) { throw new AppError(404, 'Task not found') } res.json({ success: true, message: 'Task permanently deleted', deletedId: taskId }) } catch (err) { next(err) } } |
Dangerous beginner mistake
|
0 1 2 3 4 5 6 |
await Task.deleteOne({ _id: req.params.id }) // ← works, but... |
Better — always convert to ObjectId if necessary (though Mongoose usually handles it)
|
0 1 2 3 4 5 6 |
await Task.deleteOne({ _id: new mongoose.Types.ObjectId(req.params.id) }) |
Even better — use .findByIdAndDelete()
|
0 1 2 3 4 5 6 7 8 |
const deletedTask = await Task.findByIdAndDelete(req.params.id) if (!deletedTask) throw new AppError(404, 'Task not found') |
Step 5 – Soft delete (most recommended in production)
Why soft delete?
- You can restore deleted data
- You can audit who deleted what and when
- Easier compliance (GDPR, data retention)
- No cascading delete issues when users are deleted
Soft delete controller
|
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 |
export const softDeleteTask = async (req: Request<{ id: string }>, res: Response) => { try { const taskId = req.params.id const userId = req.user!.userId // from auth middleware const task = await Task.findOneAndUpdate( { _id: taskId, user: userId, deletedAt: null }, { deletedAt: new Date() }, { new: true } ) if (!task) { throw new AppError(404, 'Task not found or already deleted or not yours') } res.json({ success: true, message: 'Task soft-deleted', deletedId: taskId }) } catch (err) { next(err) } } |
How to exclude soft-deleted documents by default
Add this to schema:
|
0 1 2 3 4 5 6 7 8 9 |
taskSchema.pre(/^find/, function (next) { this.where({ deletedAt: null }) next() }) |
Now every find(), findOne(), etc. automatically excludes deleted documents.
To include deleted ones (e.g. in admin panel):
|
0 1 2 3 4 5 6 |
Task.find().setOptions({ includeDeleted: true }) |
Step 6 – Delete multiple documents (batch delete)
Delete all completed tasks of a user
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
export const deleteCompletedTasks = async (req: Request, res: Response) => { try { const userId = req.user!.userId const result = await Task.deleteMany({ user: userId, completed: true }) res.json({ success: true, deletedCount: result.deletedCount, message: 'All completed tasks deleted' }) } catch (err) { next(err) } } |
When to use .deleteMany()
- Bulk cleanup
- Admin cleanup
- GDPR delete requests
- Archiving old records
Step 7 – Delete with transaction (when deleting from multiple collections)
Delete task + remove reference from user’s task list
|
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 |
export const deleteTaskWithCleanup = async (req: Request<{ id: string }>, res: Response) => { const session = await mongoose.startSession() try { session.startTransaction() const taskId = req.params.id const userId = req.user!.userId // 1. Delete task const deleted = await Task.findOneAndDelete( { _id: taskId, user: userId }, { session } ) if (!deleted) throw new AppError(404, 'Task not found') // 2. Remove task from user's task array (if you keep reference) await User.updateOne( { _id: userId }, { $pull: { tasks: taskId } }, { session } ) await session.commitTransaction() res.json({ success: true, message: 'Task and references deleted' }) } catch (err) { await session.abortTransaction() next(err) } finally { session.endSession() } } |
Why transaction?
- Either both operations succeed or neither does
- Prevents orphaned references or inconsistent state
Step 8 – Summary – MongoDB DELETE best practices in Node.js 2025–2026
| Best Practice | Why it matters | Code pattern example |
|---|---|---|
| Prefer soft delete (deletedAt) | Recoverable, auditable, GDPR-friendly | findOneAndUpdate(…, { deletedAt: new Date() }) |
| Always check ownership | Prevents IDOR attacks | { _id: id, user: userId } |
| Use .findOneAndDelete() | Atomic find + delete | Task.findOneAndDelete({ _id, user }) |
| Use transactions for multi-collection | Atomicity (all or nothing) | session.startTransaction() + commit/abort |
| Use .deleteMany() carefully | Bulk operations — be very sure | Task.deleteMany({ user, completed: true }) |
| Log deletions (audit trail) | Compliance & debugging | Insert into deleted_tasks collection |
| Use indexes on frequent delete fields | Faster delete & find | index: true on user & deletedAt |
| Never delete without confirmation | Prevents accidental data loss | Add confirmation step in UI / API |
Which direction would you like to go much deeper into next?
- Login + JWT authentication with MongoDB
- Full task CRUD (create/read/update/delete + ownership check)
- Add pagination, filtering, sorting, search
- Add soft-delete restore + audit log system
- Add unit & integration tests with Vitest
- 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. 😊
