Chapter 66: Node.js MongoDB Create Collection
creating collections in MongoDB using Node.js.
We will go through everything step by step, as if I am sitting next to you right now:
- I open the terminal and VS Code
- I show every command I type
- I explain why we do it this way
- I show what most beginners do wrong
- I show what intermediate developers often forget
- I show what real production code looks like in 2025–2026
Goal of this lesson
Understand exactly how MongoDB collections are created in practice, and how to do it safely and correctly from a Node.js application.
We will create two complete, realistic examples:
- Creating collections automatically via Mongoose schemas (most common & recommended way)
- Creating collections manually using the MongoDB native driver (when you need full control)
Important concept first: How collections are created in MongoDB
In MongoDB:
- Collections are created automatically the first time you insert a document into them
- You do not have to run CREATE COLLECTION like in MySQL/PostgreSQL
- Mongoose does this automatically when you call .create(), .save(), .insertOne(), etc.
- If you want to pre-create a collection (with indexes, validation rules, etc.) → you can do it explicitly
Most real applications never create collections manually — they let Mongoose or native driver do it on first insert.
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 21 22 23 24 25 26 |
mkdir task-api-mongodb-collection cd task-api-mongodb-collection npm init -y npm pkg set type=module # Core dependencies npm install express dotenv mongoose zod # Development 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" } |
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 (modern & 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 |
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}`) } 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 |
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 – Example 1: Automatic collection creation via Mongoose schema (most common way)
In 95% of real Node.js + MongoDB projects, collections are created automatically when you first save a document.
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) |
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 |
import { Request, Response } from 'express' import { z } from 'zod' import { Task } from '../models/task.model.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 { const input = createTaskSchema.parse(req.body) // When we call .create() → MongoDB automatically creates the 'tasks' collection const task = await Task.create({ ...input, dueDate: input.dueDate ? new Date(input.dueDate) : null }) res.status(201).json({ success: true, data: task }) } catch (err) { if (err instanceof z.ZodError) { return res.status(400).json({ success: false, errors: err.errors }) } res.status(500).json({ success: false, message: 'Server error' }) } } |
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}`) }) |
What happens when you run the server and create a task?
- Mongoose connects to MongoDB
- When you first call Task.create(…) → MongoDB automatically creates the collection named “tasks”
- All schema rules (required, max length, enum) are enforced
- createdAt and updatedAt are automatically set
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"}' |
You will get a full task 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.
Step 5 – Explicit collection creation (rare, but sometimes needed)
Sometimes you want to pre-create a collection with custom options (capped collection, validation rules, collation, etc.)
src/setup/create-collection.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 |
import mongoose from 'mongoose' async function createExplicitCollection() { await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/taskdb') const db = mongoose.connection.db try { // Create a collection with options 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 6 – 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. 😊
