Chapter 69: Node.js MongoDB Query
1. Quick mental model – How MongoDB queries work in Mongoose
- .find() → returns many documents (array)
- .findOne() → returns first matching document (or null)
- .findById() → shortcut for _id lookup
- .findOneAndUpdate(), .findOneAndDelete() → find + modify in one atomic operation
- Queries are JavaScript objects — very natural
- Mongoose adds:
- schema validation
- middleware (pre/post hooks)
- population (like JOIN)
- lean queries (faster read-only)
- type safety with TypeScript
2. Project setup (modern & realistic)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
mkdir mongodb-queries-demo cd mongodb-queries-demo npm init -y npm pkg set type=module npm install express dotenv mongoose zod npm install -D typescript @types/node @types/express tsx nodemon |
tsconfig.json
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
{ "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/**/*"] } |
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.example
|
0 1 2 3 4 5 6 7 |
PORT=5000 MONGODB_URI=mongodb://localhost:27017/queries_demo |
3. 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 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 13 14 |
import { z } from 'zod' import 'dotenv/config' const envSchema = z.object({ PORT: z.coerce.number().default(5000), MONGODB_URI: z.string().url().startsWith('mongodb') }) export const env = envSchema.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 |
import mongoose from 'mongoose' const taskSchema = new mongoose.Schema({ title: { type: String, required: [true, 'Title is required'], trim: true, maxlength: [150, 'Title too long'] }, description: { type: String, trim: true, maxlength: [1000] }, priority: { type: String, enum: ['low', 'medium', 'high'], default: 'medium' }, completed: { type: Boolean, default: false }, dueDate: Date, tags: [{ type: String, trim: true }], createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }) // Auto-update updatedAt taskSchema.pre('save', function(next) { this.updatedAt = new Date() next() }) // Useful indexes taskSchema.index({ user: 1, createdAt: -1 }) // for user tasks sorted by date taskSchema.index({ priority: 1, dueDate: 1 }) // for priority + due date queries taskSchema.index({ title: 'text', description: 'text' }) // text search export const Task = mongoose.model('Task', taskSchema) |
5. Insert some test data (once)
src/seed.ts (run once: npx tsx 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 43 44 45 46 47 48 49 |
import { connectMongoDB } from './config/mongodb.js' import { Task } from './models/task.model.js' async function seed() { await connectMongoDB() await Task.deleteMany({}) await Task.insertMany([ { title: 'Finish quarterly report', description: 'Include sales figures and forecast', priority: 'high', dueDate: new Date('2025-03-05'), tags: ['work', 'urgent'], completed: false }, { title: 'Call client', priority: 'medium', completed: true, tags: ['work'] }, { title: 'Design new logo', priority: 'high', dueDate: new Date('2025-02-28'), tags: ['design', 'client'] }, { title: 'Update website footer', priority: 'low', tags: ['webdev'] } ]) console.log('Test data inserted') process.exit(0) } seed().catch(err => { console.error(err) process.exit(1) }) |
Run:
|
0 1 2 3 4 5 6 |
npx tsx src/seed.ts |
Now we have real data to query!
6. Real query examples – from basic to advanced
6.1 Find all documents
|
0 1 2 3 4 5 6 7 8 9 |
export const getAllTasks = async (_req: Request, res: Response) => { const tasks = await Task.find() res.json({ success: true, count: tasks.length, data: tasks }) } |
Better: lean + select only needed fields
|
0 1 2 3 4 5 6 7 8 |
const tasks = await Task.find() .lean() // faster – plain JS objects .select('title priority completed dueDate') |
6.2 Find documents that match conditions
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
export const getHighPriorityTasks = async (_req: Request, res: Response) => { const tasks = await Task.find({ priority: 'high' }) .sort({ dueDate: 1 }) // soonest due date first .limit(10) res.json({ success: true, data: tasks }) } |
6.3 Find one document (.findOne(), .findById())
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
export const getTaskById = async (req: Request<{ id: string }>, res: Response) => { try { const task = await Task.findById(req.params.id) if (!task) { return res.status(404).json({ success: false, message: 'Task not found' }) } res.json({ success: true, data: task }) } catch (err) { res.status(400).json({ success: false, message: 'Invalid ID' }) } } |
findById vs findOne
- findById(id) → automatically converts string → ObjectId
- findOne({ _id: id }) → you must do new mongoose.Types.ObjectId(id) yourself
6.4 Complex query – multiple conditions (AND / OR)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
export const getUrgentIncompleteTasks = async (_req: Request, res: Response) => { const tasks = await Task.find({ $or: [ { priority: 'high' }, { dueDate: { $lt: new Date() } } // overdue ], completed: false }) .sort({ dueDate: 1 }) .limit(20) res.json({ success: true, count: tasks.length, data: tasks }) } |
6.5 Text search (partial match on title & description)
First create text index (run once):
|
0 1 2 3 4 5 6 |
taskSchema.index({ title: 'text', description: 'text' }) |
Then:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
export const searchTasks = async (req: Request, res: Response) => { const search = req.query.q as string if (!search) { return res.status(400).json({ error: 'Search term required' }) } const tasks = await Task.find({ $text: { $search: search } }) .sort({ score: { $meta: 'textScore' } }) // best match first .limit(20) res.json({ success: true, count: tasks.length, data: tasks }) } |
Much faster than { title: { $regex: search, $options: ‘i’ } }
6.6 Pagination (skip + limit)
|
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 getPaginatedTasks = async (req: Request, res: Response) => { const page = Math.max(1, Number(req.query.page) || 1) const limit = Math.min(50, Math.max(5, Number(req.query.limit) || 10)) const tasks = await Task.find() .sort({ createdAt: -1 }) .skip((page - 1) * limit) .limit(limit) const total = await Task.countDocuments() res.json({ success: true, data: tasks, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), hasNext: page * limit < total, hasPrev: page > 1 } }) } |
Better performance tip Use cursor-based pagination (instead of skip) for large datasets:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
const lastId = req.query.lastId ? new mongoose.Types.ObjectId(req.query.lastId) : null const query = lastId ? { _id: { $gt: lastId } } : {} const tasks = await Task.find(query) .sort({ _id: 1 }) .limit(limit + 1) |
Summary – MongoDB Find best practices in Node.js 2025–2026
| Best Practice | Why it matters | Code pattern example |
|---|---|---|
| Use .lean() for read-only queries | 2–5× faster – returns plain JS objects | .find().lean() |
| Use .select() to limit fields | Less data over network | .select(‘title priority completed’) |
| Use .sort() explicitly | Predictable order | .sort({ createdAt: -1 }) |
| Use text indexes for search | Much faster than $regex | schema.index({ title: ‘text’ }) |
| Use .populate() for relations | Joins referenced documents | .populate(‘user’, ’email name’) |
| Use .skip() + .limit() for pagination | Essential for lists | .skip(offset).limit(pageSize) |
| Use cursor-based pagination for large data | Avoid slow skip on big offsets | { _id: { $gt: lastId } } |
| Always handle empty result | Good API UX | if (!task) throw new AppError(404, ‘Not found’) |
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 + full-text search
- Add refresh tokens + cookie-based auth
- 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. 😊
