Chapter 68: Node.js MongoDB Find
Quick mental model: How “find” works in MongoDB + Mongoose
In MongoDB:
- A collection is like a table
- A document is like a row
- .find() returns many documents (always returns a cursor / array)
- .findOne() returns one document (or null)
- .findById() is a shortcut for finding by _id
- Queries are JavaScript objects — very natural for Node.js developers
- Mongoose adds schema validation, virtuals, population, middleware, type safety
Common real-world find use cases:
- Get all tasks of a user
- Find task by ID
- Search tasks by title (partial match)
- Filter by status / priority / date range
- Get recent tasks sorted by date
- Count documents (.countDocuments())
- Paginate results (.skip() + .limit())
Step 1 – Project setup (realistic & modern – 2025–2026 style)
|
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-find cd task-api-mongodb-find npm init -y npm pkg set type=module # Core runtime dependencies npm install express cors helmet dotenv zod mongoose # Development dependencies npm install -D \ typescript @types/node @types/express @types/cors \ 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" } |
Add .env.example
|
0 1 2 3 4 5 6 7 8 |
PORT=5000 NODE_ENV=development MONGODB_URI=mongodb://localhost:27017/taskdb |
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 34 35 |
import mongoose from 'mongoose' import env from './env.js' const MONGODB_URI = env.MONGODB_URI || 'mongodb://localhost:27017/taskdb' export async function connectMongoDB() { try { await mongoose.connect(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 connection') 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 15 |
import { z } from 'zod' import 'dotenv/config' const envSchema = z.object({ PORT: z.coerce.number().default(5000), NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), MONGODB_URI: z.string().url().startsWith('mongodb') }) export const env = envSchema.parse(process.env) |
Step 3 – 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 cannot be longer than 150 characters'] }, description: { type: String, trim: true, maxlength: [500, 'Description cannot be longer than 500 characters'] }, priority: { type: String, enum: ['low', 'medium', 'high'], default: 'medium' }, completed: { type: Boolean, default: false }, dueDate: { type: Date, default: null }, user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true // very important for performance }, 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() }) export const Task = mongoose.model('Task', taskSchema) |
Step 4 – Find examples – from simple to advanced
4.1 Find all tasks (.find())
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 |
import { Request, Response } from 'express' import { Task } from '../models/task.model.js' export const getAllTasks = async (_req: Request, res: Response) => { try { // Find all documents const tasks = await Task.find() res.json({ success: true, count: tasks.length, data: tasks }) } catch (err) { res.status(500).json({ success: false, message: 'Failed to fetch tasks' }) } } |
4.2 Find tasks of a specific user (.find() + filter)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
export const getUserTasks = async (req: Request, res: Response) => { try { const userId = req.params.userId || req.user?.userId if (!userId) throw new Error('User ID required') const tasks = await Task.find({ user: userId }).sort({ createdAt: -1 }) res.json({ success: true, count: tasks.length, data: tasks }) } catch (err) { res.status(500).json({ success: false, message: 'Failed to fetch tasks' }) } } |
Key points
- { user: userId } → filter by field
- .sort({ createdAt: -1 }) → newest first (-1 = DESC)
- .sort({ createdAt: 1 }) → oldest first
4.3 Find one task by ID (.findById() – shortcut)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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(500).json({ success: false, message: 'Invalid ID format or server error' }) } } |
findById vs findOne
- .findById(id) → automatically converts string to ObjectId
- .findOne({ _id: id }) → you must convert string to ObjectId yourself if needed
4.4 Find with multiple conditions (AND / OR)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
export const getHighPriorityIncompleteTasks = async (_req: Request, res: Response) => { try { const tasks = await Task.find({ priority: 'high', completed: false }).sort({ dueDate: 1 }) // soonest due date first res.json({ success: true, count: tasks.length, data: tasks }) } catch (err) { res.status(500).json({ success: false, message: 'Failed to fetch tasks' }) } } |
More complex filter with OR
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
await Task.find({ $or: [ { priority: 'high' }, { dueDate: { $lt: new Date() } } // overdue ], completed: false }) |
4.5 Find with partial text search (LIKE equivalent)
|
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 searchTasks = async (req: Request, res: Response) => { try { const search = req.query.search as string if (!search) { return res.status(400).json({ error: 'Search term required' }) } const tasks = await Task.find({ title: { $regex: search, $options: 'i' } // case-insensitive }).sort({ createdAt: -1 }) res.json({ success: true, count: tasks.length, data: tasks }) } catch (err) { res.status(500).json({ success: false, message: 'Search failed' }) } } |
Better text search (production tip) Create a text index:
|
0 1 2 3 4 5 6 |
taskSchema.index({ title: 'text', description: 'text' }) |
Then use:
|
0 1 2 3 4 5 6 |
await Task.find({ $text: { $search: search } }) |
Much faster than $regex.
4.6 Find + populate (join-like – get user info)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
export const getTasksWithUser = async (_req: Request, res: Response) => { try { const tasks = await Task.find() .populate('user', 'email name') // only fetch email & name .sort({ createdAt: -1 }) res.json({ success: true, count: tasks.length, data: tasks }) } catch (err) { res.status(500).json({ success: false, message: 'Failed to fetch tasks' }) } } |
Output example
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "title": "Finish report", "priority": "high", "user": { "_id": "user-uuid", "email": "aman@example.com", "name": "Aman" } } |
Step 7 – Summary – MongoDB Find best practices in Node.js 2025–2026
| Best Practice | Why it matters | Code pattern example |
|---|---|---|
| Use .find() for multiple docs | Returns array (cursor) | Task.find({ user: userId }) |
| Use .findOne() or .findById() | Returns single document or null | Task.findById(id) |
| Use .populate() for relations | Joins referenced documents | .populate(‘user’, ’email name’) |
| Add .sort() explicitly | Predictable order | .sort({ createdAt: -1 }) |
| Use text indexes for search | Much faster than $regex | schema.index({ title: ‘text’ }) |
| Use .lean() for read-only queries | 2–5× faster – returns plain JS objects | Task.find().lean() |
| Use .select() to limit fields | Less data over network | .select(‘title priority completed’) |
| 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, 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. 😊
