MongoDB with Node.js and TypeScript — Complete 2026 Guide

Sanjeev SharmaSanjeev Sharma
5 min read

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: strict mode in the native driver to catch deprecated API usage
  • Use $project in 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: true in Mongoose schemas to auto-manage createdAt and updatedAt fields
  • Always validate MONGODB_URI at startup and fail fast rather than deferring connection errors

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading