Chapter 67: Node.js MongoDB Insert
INSERT operations (creating documents) in MongoDB using Node.js (2025–2026 style).
We will go through everything step by step, slowly and thoroughly, as if I am sitting next to you right now:
- I open VS Code and the terminal
- We create the project together from scratch
- I explain every single decision — why we do it this way, what alternatives exist, what most people get wrong
- I show common beginner traps, intermediate mistakes, and real production patterns
- We build a complete, small but realistic REST API that inserts data into MongoDB
Step 1 – Project Initialization (modern & realistic – what most teams do)
|
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 |
# Create project folder mkdir task-api-mongodb-insert cd task-api-mongodb-insert # Initialize with ESM (Node.js default in 2025–2026) npm init -y npm pkg set type=module # Core runtime dependencies npm install \ express \ cors \ helmet \ compression \ dotenv \ zod \ mongoose # Development dependencies (linting + TypeScript + fast dev) 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 – recommended for serious projects)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitAny": true, "skipLibCheck": true, "outDir": "./dist", "rootDir": "./src", "sourceMap": true, "noEmitOnError": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } |
package.json scripts (modern dev workflow)
|
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" } |
Step 2 – Folder structure (what most real teams use)
|
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 |
task-api-mongodb-insert/ ├── src/ │ ├── config/ ← env + MongoDB connection │ │ ├── env.ts │ │ └── mongodb.ts │ ├── controllers/ ← HTTP handlers (thin) │ │ └── task.controller.ts │ ├── middleware/ ← auth, validation, error │ │ ├── error.middleware.ts │ │ └── validate.middleware.ts │ ├── models/ ← Mongoose schemas & models │ │ └── task.model.ts │ ├── routes/ ← route definitions │ │ └── task.routes.ts │ ├── schemas/ ← Zod schemas / DTOs │ │ └── task.schema.ts │ ├── services/ ← business logic (optional layer) │ │ └── task.service.ts │ ├── types/ ← shared types │ │ └── index.ts │ └── index.ts ← entry point ├── .env ├── .env.example ├── tsconfig.json └── package.json |
Why this structure?
- Controllers → only HTTP concerns (thin)
- Services → business logic (testable)
- Models → Mongoose schema & queries
- Schemas → Zod runtime validation
- Very easy to grow → add auth, users, notifications later
Step 3 – MongoDB connection (modern & 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 36 37 38 39 40 41 42 43 44 45 |
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, { // Recommended options in 2025–2026 maxPoolSize: 10, minPoolSize: 2, serverSelectionTimeoutMS: 5000, socketTimeoutMS: 45000, family: 4, // Prefer IPv4 (usually faster) }) console.log('MongoDB connected successfully') console.log(`Database: ${mongoose.connection.db.databaseName}`) console.log(`Collection prefix: ${mongoose.modelNames().join(', ') || 'none yet'}`) } catch (err) { console.error('MongoDB connection failed:', err) process.exit(1) // fail fast in development } } // Connect on startup connectMongoDB() // Graceful shutdown (Docker, PM2, Kubernetes, Ctrl+C) process.on('SIGTERM', async () => { console.log('SIGTERM received – closing MongoDB connection') await mongoose.connection.close() process.exit(0) }) process.on('SIGINT', async () => { console.log('SIGINT received – closing MongoDB connection') await mongoose.connection.close() process.exit(0) }) |
src/config/env.ts (Zod validation – safety first)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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') .refine(uri => !uri.includes('password') || uri.includes('mongodb://'), { message: 'MONGODB_URI should use mongodb:// or mongodb+srv:// format' }) }) export const env = envSchema.parse(process.env) |
Important notes about MONGODB_URI
- Use mongodb:// for local / replica sets
- Use mongodb+srv:// for Atlas / cloud clusters
- Never commit real credentials to git
- Use secret managers (AWS SSM, Doppler, Infisical, 1Password) in production
Step 4 – Mongoose model (Task) – where collection is created
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 |
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 }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }) // Auto-update updatedAt on save taskSchema.pre('save', function (next) { this.updatedAt = new Date() next() }) // Create & export model // Collection name = 'tasks' (lowercase plural by default) export const Task = mongoose.model('Task', taskSchema) |
Very important fact
When you call Task.create() or new Task().save() for the first time, MongoDB automatically creates the collection named tasks (lowercase plural of the model name).
You do NOT need to create the collection manually in most cases.
Step 5 – Create a task (the INSERT / CREATE example)
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 42 43 |
import { Request, Response } from 'express' import { z } from 'zod' import { Task } from '../models/task.model.js' import { AppError } from '../middleware/error.middleware.js' const createTaskSchema = z.object({ title: z.string().min(1).max(150), description: z.string().max(500).optional(), priority: z.enum(['low', 'medium', 'high']).default('medium'), dueDate: z.string().datetime().optional().nullable(), completed: z.boolean().default(false) }) export const createTask = async (req: Request, res: Response) => { try { // 1. Validate incoming data (Zod) const input = createTaskSchema.parse(req.body) // 2. Create document (this is the INSERT) // MongoDB automatically creates the 'tasks' collection on first insert const task = await Task.create({ ...input, dueDate: input.dueDate ? new Date(input.dueDate) : null }) // 3. Return clean response res.status(201).json({ success: true, message: 'Task created successfully', data: task }) } catch (err) { if (err instanceof z.ZodError) { throw new AppError(400, 'Validation failed', true) } throw err } } |
src/routes/task.routes.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { Router } from 'express' import { createTask } from '../controllers/task.controller.js' const router = Router() router.post('/', createTask) export default router |
src/index.ts (minimal server)
|
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 |
import express from 'express' import cors from 'cors' import helmet from 'helmet' import env from './config/env.js' import taskRoutes from './routes/task.routes.js' import './config/mongodb.js' // connects on import const app = express() app.use(helmet()) app.use(cors()) app.use(express.json()) app.use('/api/tasks', taskRoutes) app.get('/health', (req, res) => { res.json({ status: 'ok', database: 'connected' }) }) app.listen(env.PORT, () => { console.log(`Server running → http://localhost:${env.PORT}`) }) |
Test it
|
0 1 2 3 4 5 6 7 8 |
curl -X POST http://localhost:5000/api/tasks \ -H "Content-Type: application/json" \ -d '{"title":"Finish quarterly report","priority":"high","dueDate":"2025-03-05T18:00:00Z"}' |
You should get back a full document with _id, createdAt, updatedAt, etc.
Check in MongoDB (using MongoDB Compass or terminal):
|
0 1 2 3 4 5 6 7 |
use taskdb db.tasks.find().pretty() |
You will see:
- The tasks collection was created automatically
- All documents follow the schema rules
Step 6 – Explicit collection creation (rare but sometimes needed)
Sometimes you want to pre-create a collection with custom options:
- capped collection (fixed size – oldest documents auto-deleted)
- custom collation (case-insensitive, language-specific)
- schema validation rules
src/setup/create-collection.ts (run once manually)
|
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 |
import mongoose from 'mongoose' async function createExplicitCollection() { await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/taskdb') const db = mongoose.connection.db try { await db.createCollection('audit_logs', { capped: true, // fixed size – oldest documents auto-deleted size: 5242880, // 5 MB max: 5000, // max 5000 documents validator: { $jsonSchema: { bsonType: 'object', required: ['action', 'timestamp'], properties: { action: { bsonType: 'string' }, timestamp: { bsonType: 'date' }, userId: { bsonType: ['string', 'null'] }, details: { bsonType: ['object', 'null'] } } } }, validationLevel: 'moderate', validationAction: 'warn' }) console.log('Collection "audit_logs" created successfully with options') } catch (err: any) { if (err.codeName === 'NamespaceExists') { console.log('Collection already exists') } else { console.error('Failed to create collection:', err) } } finally { await mongoose.connection.close() } } createExplicitCollection() |
Run it:
|
0 1 2 3 4 5 6 |
npx tsx src/setup/create-collection.ts |
When do real teams do explicit collection creation?
- Capped collections for logs / audit trails
- Custom collation (case-insensitive search, language-specific sorting)
- Schema validation rules (beyond Mongoose schema)
- Pre-creating collections in CI/CD pipelines
Step 7 – Summary – MongoDB collection creation best practices in Node.js 2025–2026
| Best Practice | Why it matters | Real pattern / code example |
|---|---|---|
| Let Mongoose create collections automatically | Simplest & most common way | await Task.create({ … }) |
| Use explicit createCollection only when needed | Capped collections, custom validation, collation | db.createCollection(‘audit_logs’, { capped: true }) |
| Always connect once on startup | Avoid multiple connections | mongoose.connect() in a separate module |
| Use graceful shutdown | Clean close in Docker / Kubernetes / PM2 | process.on(‘SIGTERM’, close connection) |
| Use Zod for input validation | Catch bad data before saving | createTaskSchema.parse(req.body) |
| Add indexes on frequent query fields | 10×–100× faster queries | index: true on user field |
| Use pre(‘save’) hooks | Auto-update fields like updatedAt | this.updatedAt = new Date() |
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. 😊
