MongoDB with Node.js and TypeScript — Complete 2026 Guide
Advertisement
Introduction
Why MongoDB with TypeScript
MongoDB is the most popular document database for Node.js backends. With TypeScript, you get full type safety across your data layer — catching schema mismatches at compile time rather than runtime. The MongoDB Node.js driver v7 is written in TypeScript and compiles against TypeScript 5.6+, making it a first-class citizen in modern stacks.
Use MongoDB when your data is hierarchical, semi-structured, or evolves rapidly. Combine it with TypeScript for schema discipline without sacrificing flexibility.
Installation and Setup
npm install mongodb mongoose
npm install --save-dev @types/node// Native driver connection
import { MongoClient, ServerApiVersion } from 'mongodb';
const client = new MongoClient(process.env.MONGODB_URI!, {
serverApi: {
version: ServerApiVersion.v1,
strict: true,
deprecationErrors: true,
},
});
await client.connect();
const db = client.db('myapp');Typed Collections with the Native Driver
import { Collection, ObjectId } from 'mongodb';
interface User {
_id?: ObjectId;
name: string;
email: string;
role: 'user' | 'admin';
createdAt: Date;
}
const users: Collection<User> = db.collection('users');
// Insert
const result = await users.insertOne({
name: 'Alice',
email: 'alice@example.com',
role: 'user',
createdAt: new Date(),
});
// Find with type inference
const user = await users.findOne({ _id: result.insertedId });
// user is User | null — fully typed
// Update
await users.updateOne(
{ _id: result.insertedId },
{ $set: { role: 'admin' } }
);
// Delete
await users.deleteOne({ _id: result.insertedId });
// Pagination
const page = await users
.find({ role: 'user' })
.sort({ createdAt: -1 })
.skip(0)
.limit(20)
.toArray();Mongoose with TypeScript
import mongoose, { Schema, Document, Model } from 'mongoose';
// Define interface
interface IUser extends Document {
name: string;
email: string;
age: number;
role: 'user' | 'admin';
createdAt: Date;
}
// Create schema
const userSchema = new Schema<IUser>({
name: { type: String, required: true, minlength: 2 },
email: { type: String, required: true, unique: true, lowercase: true },
age: { type: Number, min: 0, max: 150 },
role: { type: String, enum: ['user', 'admin'], default: 'user' },
}, { timestamps: true });
// Indexes
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ createdAt: -1 });
const User: Model<IUser> = mongoose.model('User', userSchema);
// CRUD
const user = await User.create({ name: 'Alice', email: 'alice@example.com', age: 30 });
const found = await User.findById(user._id);
await User.updateOne({ _id: user._id }, { $set: { age: 31 } });
await User.deleteOne({ _id: user._id });Aggregation Pipeline
// Sales summary by category
const summary = await db.collection('orders').aggregate([
{ $match: { status: 'completed' } },
{ $group: {
_id: '$category',
total: { $sum: '$amount' },
count: { $sum: 1 },
avgAmt: { $avg: '$amount' },
}},
{ $sort: { total: -1 } },
{ $limit: 10 },
]).toArray();Relationships and Population
const postSchema = new Schema({
title: String,
content: String,
author: { type: Schema.Types.ObjectId, ref: 'User', required: true },
tags: [String],
}, { timestamps: true });
const Post = mongoose.model('Post', postSchema);
// Populate author details
const posts = await Post.find()
.populate('author', 'name email')
.sort({ createdAt: -1 })
.limit(10);Middleware (Hooks)
import bcrypt from 'bcrypt';
userSchema.pre('save', async function () {
if (this.isModified('password')) {
this.password = await bcrypt.hash(this.password as string, 12);
}
});
userSchema.post('save', function (doc) {
console.log(`User ${doc._id} saved`);
});Transactions
const session = await mongoose.startSession();
session.startTransaction();
try {
const user = await User.create([{ name: 'Alice', email: 'a@ex.com', age: 25 }], { session });
const post = await Post.create([{ title: 'Hello', author: user[0]._id }], { session });
await session.commitTransaction();
return { user: user[0], post: post[0] };
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
await session.endSession();
}Text Search and Geospatial
// Text search index
userSchema.index({ name: 'text', bio: 'text' });
const results = await User.find({ $text: { $search: 'engineer london' } });
// Geospatial — 2dsphere
const locationSchema = new Schema({
name: String,
location: { type: { type: String, default: 'Point' }, coordinates: [Number] },
});
locationSchema.index({ location: '2dsphere' });Common Mistakes
- Storing passwords or secrets in plain text inside documents
- Skipping indexes — queries on large unindexed collections cause full collection scans
- Using
find().toArray()without.limit()— can exhaust memory on large datasets - Not closing sessions after transactions, leading to connection leaks
- Embedding unbounded arrays in documents — use references for one-to-many relationships
- Ignoring
lean()in Mongoose when you only need plain objects (not Mongoose documents)
Best Practices
- Always define TypeScript interfaces for collection documents
- Use
lean()in Mongoose for read-heavy queries to skip hydration overhead - Add compound indexes to match your most common query patterns
- Use connection pooling — do not call
connect()on every request - Enable
serverApi: strictmode in the native driver to catch deprecated API usage - Use
$projectin aggregations to limit returned fields and reduce bandwidth
Key Takeaways
- The MongoDB Node.js driver v7 is written in TypeScript and supports generics for fully typed collections
- Mongoose adds schema validation, middleware hooks, and population on top of the native driver
- Use the native driver when you need maximum performance; use Mongoose for application-level validation
- Aggregation pipelines replace most multi-query patterns and run entirely on the server
- Transactions in MongoDB require a replica set or Atlas cluster — they do not work on standalone instances
- Indexes are the single biggest lever for query performance — index fields used in
find,sort, and$match - Use
timestamps: truein Mongoose schemas to auto-managecreatedAtandupdatedAtfields - Always validate
MONGODB_URIat startup and fail fast rather than deferring connection errors
Advertisement
Related reading
Prisma ORM Guide 2026 — Type-Safe Database Access with PostgreSQL5 min readRedis Caching Guide 2026 — Improve API Performance 10x5 min readPostgreSQL Guide 2026 — Performance, JSON, Full-Text Search, and Scaling5 min readAPI-First Development in 2026 — Design, Mock, Validate, Then Build6 min readbetter-auth — The Open-Source Auth Library That Replaces NextAuth6 min readData Corruption from Bad Serialization — When Your Data Silently Changes6 min read