Chapter 70: Node.js MongoDB Sort
MongoDB when using Node.js with Mongoose.
We will go through everything step by step, slowly and carefully — exactly as if I am sitting next to you right now:
- I open VS Code
- I open the terminal
- We type every line together
- We run the code
- We look at the results in the console and in MongoDB Compass
- I explain why we write each line this way
- I show what most beginners do wrong (and why it causes confusion or bugs)
- I show what intermediate developers often forget (leads to wrong order or slow queries)
- I show what real production code looks like in serious Node.js backends in 2025–2026
Goal of this lesson
Master sorting in MongoDB queries so that:
- Your API always returns data in the expected order
- Sorting is fast (uses indexes)
- Sorting is safe (no injection risk)
- Sorting is flexible (user-controlled sort field & direction)
- You understand when and why to sort in memory vs in the database
Step 1 – Project setup (minimal but realistic)
If you don’t have a project yet:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
mkdir mongodb-sort-demo cd mongodb-sort-demo 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.example
|
0 1 2 3 4 5 6 7 |
PORT=5000 MONGODB_URI=mongodb://localhost:27017/sort_demo |
Step 2 – MongoDB connection (safe & modern)
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) |
Step 3 – Realistic Mongoose model (Task) with indexes
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 |
import mongoose from 'mongoose' const taskSchema = new mongoose.Schema({ title: { type: String, required: true, trim: true, maxlength: 150 }, priority: { type: String, enum: ['low', 'medium', 'high'], default: 'medium' }, completed: { type: Boolean, default: false }, dueDate: { type: Date, default: null }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, points: { type: Number, default: 0 } }) // Auto-update updatedAt taskSchema.pre('save', function (next) { this.updatedAt = new Date() next() }) // Important indexes for sorting taskSchema.index({ createdAt: -1 }) // newest first taskSchema.index({ priority: 1, dueDate: 1 }) // priority + due date taskSchema.index({ title: 'text' }) // text search export const Task = mongoose.model('Task', taskSchema) |
Why these indexes?
- { createdAt: -1 } → very fast when sorting newest first
- { priority: 1, dueDate: 1 } → fast when sorting by priority then due date
- Text index → fast full-text search on title
Step 4 – 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 |
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'), completed: false, points: 100 }, { title: 'Call client', priority: 'medium', completed: true, points: 20 }, { title: 'Design logo', priority: 'high', dueDate: new Date('2025-02-28'), completed: false, points: 80 }, { title: 'Update website footer', priority: 'low', points: 10 }, { title: 'Prepare presentation', priority: 'high', dueDate: new Date('2025-02-25'), completed: false, points: 50 } ]) console.log('Test data inserted') process.exit(0) } seed().catch(err => { console.error(err) process.exit(1) }) |
Run once:
|
0 1 2 3 4 5 6 |
npx tsx src/seed.ts |
Now we have 5 tasks with different priorities, due dates, completion status, and points.
Step 5 – Basic sorting examples
5.1 Sort by createdAt – newest first
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
// src/controllers/task.controller.ts export const getTasksNewestFirst = async (_req: Request, res: Response) => { const tasks = await Task.find() .sort({ createdAt: -1 }) // -1 = descending = newest first .limit(10) res.json({ success: true, data: tasks }) } |
Result order Tasks appear from most recently created → oldest
5.2 Sort by priority (custom order: high → medium → low)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
export const getTasksByPriority = async (_req: Request, res: Response) => { const tasks = await Task.find() .sort({ priority: 1, // 1 = ascending (low → high) dueDate: 1 // then earliest due date first }) res.json({ success: true, data: tasks }) } |
Problem: MongoDB sorts strings alphabetically → ‘high’ < ‘low’ < ‘medium’
Solution: use FIELD() or CASE logic via aggregation (or map in JS)
Better – custom priority sort
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const priorityOrder = { high: 1, medium: 2, low: 3 } const tasks = await Task.find() .lean() // faster for read-only .sort((a, b) => { const priA = priorityOrder[a.priority] || 4 const priB = priorityOrder[b.priority] || 4 return priA - priB }) |
Best production way: use aggregation with $addFields
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
const tasks = await Task.aggregate([ { $addFields: { priorityScore: { $switch: { branches: [ { case: { $eq: ['$priority', 'high'] }, then: 1 }, { case: { $eq: ['$priority', 'medium'] }, then: 2 }, { case: { $eq: ['$priority', 'low'] }, then: 3 } ], default: 4 } } } }, { $sort: { priorityScore: 1, dueDate: 1 } } ]) |
Step 6 – Dynamic sorting (user-controlled – safe version)
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 |
export const getSortedTasks = async (req: Request, res: Response) => { try { const sortBy = req.query.sortBy as string || 'createdAt' const sortOrder = (req.query.sortOrder as string || 'desc').toLowerCase() === 'asc' ? 1 : -1 // Whitelist safe fields const allowedFields = ['createdAt', 'title', 'priority', 'dueDate', 'completed'] if (!allowedFields.includes(sortBy)) { throw new AppError(400, 'Invalid sort field') } const sortObj = { [sortBy]: sortOrder } const tasks = await Task.find() .sort(sortObj) .limit(20) res.json({ success: true, data: tasks, sortedBy: sortBy, sortOrder: sortOrder === 1 ? 'asc' : 'desc' }) } catch (err) { next(err) } } |
Safe URLs
- /api/tasks/sorted?sortBy=priority&sortOrder=asc
- /api/tasks/sorted?sortBy=title&sortOrder=desc
Security note
Never do:
|
0 1 2 3 4 5 6 |
.sort(req.query.sortBy) // ← injection possible! |
Always whitelist allowed sort fields.
Step 7 – Summary – MongoDB sorting best practices in Node.js 2025–2026
| Best Practice | Why it matters | Code pattern example |
|---|---|---|
| Always use .sort() | MongoDB does NOT guarantee order without it | .sort({ createdAt: -1 }) |
| Use .lean() for read-only lists | 2–5× faster – returns plain JS objects | .find().lean().sort({ … }) |
| Use indexes on sorted fields | 10×–100× faster | schema.index({ createdAt: -1 }) |
| Whitelist dynamic sort fields | Prevents injection in .sort() | if (allowedFields.includes(sortBy)) |
| Use aggregation for custom sorting | When simple .sort() is not enough (e.g. priority order) | $addFields + $sort |
| Combine sort + limit + skip | Essential for paginated lists | .sort(…).skip(offset).limit(pageSize) |
| Prefer cursor-based pagination | Avoid slow skip on large offsets | { _id: { $gt: lastId } } |
| Add text index for search + sort by relevance | Fast full-text + best-match-first | schema.index({ title: ‘text’ }) + $text + $meta: “textScore” |
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)
- Advanced pagination (cursor-based, infinite scroll)
- Text search + relevance sorting
- Aggregation pipeline examples (group, match, unwind, etc.)
- Performance tuning (indexes, explain(), profiling)
Just tell me what you want to build or understand next — I’ll continue with complete, secure, production-ready code and explanations. 😊
