Back to Blog
Tutorial10 min read2026-07-04

How to Set Up MongoDB for Your App

From provisioning managed MongoDB to connecting, indexing, and avoiding the classic schema mistakes. A practical setup guide with Mongoose and the native driver.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

When MongoDB is the right call

MongoDB shines when your data is document-shaped, your schema evolves quickly, and you want to store nested objects without a pile of joins. It's a poor fit when your data is highly relational with lots of cross-entity transactions — that's Postgres territory. Pick it deliberately, not by habit.

This guide takes you from a provisioned managed MongoDB to a connected, indexed, production-ready setup using the Node native driver and Mongoose.

The connection string

mongodb://USER:PASSWORD@HOST:27017/DBNAME?authSource=admin
mongodb+srv://USER:PASSWORD@CLUSTER/DBNAME   # SRV form, common with clusters

Read it from MONGODB_URI in the environment. On PandaStack, provisioning a managed MongoDB injects the connection string into your service automatically.

Connect once, reuse the client

The number-one MongoDB-in-Node mistake is connecting on every request. The driver manages an internal connection pool; create one client at startup and reuse it.

// db.js (native driver)
import { MongoClient } from "mongodb";

const client = new MongoClient(process.env.MONGODB_URI, {
  maxPoolSize: 20,
  minPoolSize: 2,
  serverSelectionTimeoutMS: 5000,
});

let db;
export async function getDb() {
  if (!db) {
    await client.connect();
    db = client.db(); // db name from the URI
  }
  return db;
}

With Mongoose:

import mongoose from "mongoose";

await mongoose.connect(process.env.MONGODB_URI, {
  maxPoolSize: 20,
  serverSelectionTimeoutMS: 5000,
});

Call connect once at boot, not inside a request handler.

Define a schema (yes, even with Mongo)

"Schemaless" doesn't mean "no schema" — it means the database won't enforce one, so *you* should. Mongoose gives you validation:

import mongoose from "mongoose";

const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true, lowercase: true },
  name: { type: String, required: true },
  roles: { type: [String], default: ["member"] },
  createdAt: { type: Date, default: Date.now },
}, { timestamps: true });

export const User = mongoose.model("User", userSchema);

If you use the native driver, enforce shape in your code or with MongoDB's JSON Schema validation on the collection.

Indexes are not optional

The most common MongoDB performance disaster is a collection scan on a field you query constantly. Create indexes for every field you filter or sort on:

// native driver
await db.collection("users").createIndex({ email: 1 }, { unique: true });
await db.collection("orders").createIndex({ userId: 1, createdAt: -1 });
// mongoose — declare on the schema
userSchema.index({ email: 1 }, { unique: true });

Use a compound index when you filter on one field and sort on another (like { userId: 1, createdAt: -1 } above). Verify with explain():

const plan = await db.collection("orders")
  .find({ userId: id }).sort({ createdAt: -1 }).explain("executionStats");
// look for IXSCAN, not COLLSCAN

Seeing COLLSCAN means you're missing an index.

Schema design: embed vs. reference

  • Embed when data is accessed together and the sub-document is bounded (e.g., a user's address). One read, no join.
  • Reference when the related data is large, grows unboundedly, or is shared across documents (e.g., a user's orders — could be thousands).

The anti-pattern is an ever-growing embedded array (the "unbounded array" problem) — a document has a 16MB limit and large arrays kill performance. Reference instead.

CRUD basics

const users = db.collection("users");

await users.insertOne({ email: "a@b.com", name: "Ada" });
const u = await users.findOne({ email: "a@b.com" });
await users.updateOne({ _id: u._id }, { $set: { name: "Ada L." } });
await users.deleteOne({ _id: u._id });

Use $set for partial updates — passing a whole document replaces it and silently drops fields.

Transactions (when you need them)

MongoDB supports multi-document transactions on replica sets:

const session = client.startSession();
try {
  await session.withTransaction(async () => {
    await accounts.updateOne({ _id: from }, { $inc: { balance: -amt } }, { session });
    await accounts.updateOne({ _id: to }, { $inc: { balance: amt } }, { session });
  });
} finally {
  await session.endSession();
}

Reach for transactions only when you truly need atomicity across documents — they're heavier than single-document operations, which are already atomic.

Deploying on PandaStack

  1. 1Provision a managed MongoDB from the dashboard.
  2. 2Attach it to your service — the connection string is injected.
  3. 3Create indexes as part of your release/migration step.
  4. 4Deploy and verify connection in the live logs.
  5. 5Confirm scheduled backups are enabled (PandaStack supports scheduled + manual backups).

Checklist

  • One client, reused; pool sized under any connection cap.
  • Indexes on every queried/sorted field; verify with explain().
  • Schema validation in code or on the collection.
  • No unbounded embedded arrays — reference instead.
  • Backups enabled and restore tested.

References

  • MongoDB Node driver: https://www.mongodb.com/docs/drivers/node/current/
  • Mongoose docs: https://mongoosejs.com/docs/guide.html
  • MongoDB indexing strategies: https://www.mongodb.com/docs/manual/applications/indexes/
  • Data modeling (embed vs reference): https://www.mongodb.com/docs/manual/data-modeling/
  • MongoDB transactions: https://www.mongodb.com/docs/manual/core/transactions/

---

Want a managed MongoDB with the connection string wired into your app and backups handled? PandaStack provisions it in a click. Start free at https://dashboard.pandastack.io

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also