Chapter 65: Node.js MongoDB Create Database
MongoDB database in a Node.js application.
We will go through everything step by step, as if I am sitting next to you right now:
- I open the terminal
- I open VS Code
- I type every command and every line of code live
- I explain why we do things this way (and what most people get wrong)
- I show real production patterns used in serious Node.js projects in 2025–2026
- We build a complete, small but realistic Task Management API from scratch
0. Why MongoDB + Node.js is still very popular in 2025–2026
- Extremely fast development speed
- Flexible schema = perfect for evolving products
- Great JavaScript / JSON match (no impedance mismatch)
- Horizontal scaling is easier than relational databases
- Huge ecosystem: Mongoose, MongoDB native driver, Typegoose, NestJS integration
- Many companies (especially startups & mid-size) still use it as their primary database
Most common stacks right now (early 2026)
- Mongoose → ~70–80% of Node.js MongoDB projects (best TypeScript support)
- MongoDB native driver → performance-critical apps
- Typegoose → people who want class-based models
- NestJS + Mongoose → enterprise / large teams
We will use Mongoose + TypeScript + Express — this is the most widely used and beginner-to-production friendly combination.
Step 1 – Project initialization (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 27 28 29 30 31 32 33 34 35 36 37 38 |
mkdir task-api-mongodb cd task-api-mongodb npm init -y npm pkg set type=module # Core runtime dependencies npm install \ express \ cors \ helmet \ compression \ dotenv \ zod \ jsonwebtoken \ bcryptjs \ mongoose # Development dependencies npm install -D \ typescript \ @types/node \ @types/express \ @types/cors \ @types/jsonwebtoken \ @types/bcryptjs \ 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" } |
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 31 |
task-api-mongodb/ ├── src/ │ ├── config/ ← env + MongoDB connection │ │ ├── env.ts │ │ └── mongodb.ts │ ├── controllers/ ← HTTP handlers (thin) │ │ └── task.controller.ts │ ├── middleware/ ← auth, validation, error │ │ ├── auth.middleware.ts │ │ ├── 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 │ │ └── task.service.ts │ ├── types/ ← shared types │ │ └── index.ts │ └── index.ts ← entry point ├── .env ├── .env.example ├── tsconfig.json └── package.json |
Step 3 – 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 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 16 |
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'), JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters') }) export const env = envSchema.parse(process.env) |
Why this connection style?
- Mongoose handles connection pooling automatically
- maxPoolSize / minPoolSize → controls resource usage
- serverSelectionTimeoutMS → fails fast when DB is down
- Graceful shutdown → closes connection on SIGTERM (Docker, Kubernetes, PM2)
- process.exit(1) on startup failure → fail-fast in dev
Step 4 – 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 50 51 52 53 54 55 56 |
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 on save taskSchema.pre('save', function (next) { this.updatedAt = new Date() next() }) // Create & export model export const Task = mongoose.model('Task', taskSchema) |
Why this schema style?
- required, maxlength, enum → built-in validation
- trim: true → removes unwanted spaces
- index: true on user → fast queries when finding tasks per user
- pre(‘save’) hook → auto-maintains updatedAt
- ref: ‘User’ → enables .populate() later
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 44 45 |
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 input const input = createTaskSchema.parse(req.body) // 2. Get authenticated user (from middleware) const userId = req.user!.userId // assume auth middleware sets req.user // 3. Create task (this is the INSERT) const task = await Task.create({ ...input, user: userId, dueDate: input.dueDate ? new Date(input.dueDate) : null }) // 4. Return clean response res.status(201).json({ success: true, data: task }) } catch (err) { if (err instanceof z.ZodError) { throw new AppError(400, 'Validation failed') } throw err } } |
Route example (in routes/task.routes.ts)
|
0 1 2 3 4 5 6 |
router.post('/', authenticate, createTask) |
Test it (after login → get token → send with Authorization: Bearer token)
|
0 1 2 3 4 5 6 7 8 9 |
curl -X POST http://localhost:5000/api/tasks \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-jwt-token" \ -d '{"title":"Finish quarterly report","priority":"high","dueDate":"2025-03-05T18:00:00Z"}' |
You should get a full task document back with _id, user, createdAt, updatedAt, etc.
Step 6 – Summary – MongoDB CREATE (INSERT) best practices in Node.js 2025–2026
| Best Practice | Why it matters | Code pattern example |
|---|---|---|
| Use Mongoose schema validation | Catches bad data early | required: true, maxlength: 150, enum: […] |
| Always validate input with Zod first | Runtime safety + better error messages | createTaskSchema.parse(req.body) |
| Use async/await & try/catch | Clean error handling | await Task.create(…) |
| Return created document | Client gets full record immediately | res.status(201).json(task) |
| Use 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() |
| Use connection pool (Mongoose does it) | Performance + safety | Automatic with mongoose.connect() |
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. 😊
