Connecting to MongoDB

Node.js · Lesson 20 of 30 8 min read Updated

MySQL stores data in tables with fixed columns. MongoDB stores documents: JSON-like objects that can contain nested objects and arrays, grouped into collections. A student document can hold the student's marks object and hobbies array directly, with no extra tables. Because documents look like JavaScript objects, MongoDB is a popular partner for Node.js; the "M" in the MERN stack (MongoDB, Express, React, Node.js) stands for it.

In this lesson you will connect with the official mongodb driver and run create, read, update and delete queries with MongoDB's query operators. Then you will use Mongoose, a library that adds schemas and validation, and build an Express API on top of it.

Getting a MongoDB server

You need a running MongoDB server. Two common options:

  • MongoDB Community Server on your own computer (the current major version is MongoDB 9.0, from September 2026). It listens on port 27017. Install MongoDB Shell (mongosh) and Compass (a graphical viewer) alongside it.
  • MongoDB Atlas, the hosted cloud service, which has a free tier. It gives you a connection string that starts with mongodb+srv://.

Put the connection string in .env. The part after the last / is the database name; MongoDB creates databases and collections automatically the first time you write to them.

Terminal
# .env
MONGODB_URI=mongodb://127.0.0.1:27017/school

Documents and the mongosh shell

You can try queries in mongosh before writing any Node.js code. Its syntax is almost the same as the Node.js driver's:

MongoDB Shell
use school
db.students.insertOne({ name: "Aarav", city: "Rohtak", marks: { maths: 88 } })
db.students.find({ city: "Rohtak" })

Every document gets an _id field. If you do not supply one, MongoDB creates an ObjectId: a 12-byte unique value, shown as a 24-character hex string such as 6ae4722caebc3f8fcf685d1c.

The official driver

Terminal
npm install mongodb
JavaScript
// driver.js  (run with: node --env-file=.env driver.js)
import { MongoClient, ObjectId } from "mongodb";

const client = new MongoClient(process.env.MONGODB_URI);
await client.connect();
const db = client.db();                     // the database named in the URI ("school")
const students = db.collection("students");

// Create
const one = await students.insertOne({ name: "Aarav", city: "Rohtak", marks: { maths: 88, science: 92 }, hobbies: ["cricket"] });
console.log(one.insertedId instanceof ObjectId);

await students.insertMany([
  { name: "Priya", city: "Jaipur", marks: { maths: 95, science: 89 }, hobbies: ["dance", "chess"] },
  { name: "Kabir", city: "Pune", marks: { maths: 67, science: 74 } },
  { name: "Meera", city: "Kochi", marks: { maths: 91, science: 97 }, hobbies: ["chess"] },
]);

// Read
const toppers = await students
  .find({ "marks.maths": { $gte: 85 } }, { projection: { _id: 0, name: 1, "marks.maths": 1 } })
  .sort({ "marks.maths": -1 })
  .toArray();
console.log(toppers);

console.log(await students.findOne({ hobbies: "chess", city: "Kochi" }, { projection: { _id: 0, name: 1 } }));
console.log(await students.countDocuments({ city: { $in: ["Pune", "Jaipur"] } }));

// Update
const upd = await students.updateOne({ name: "Kabir" }, { $set: { city: "Mumbai" }, $inc: { "marks.maths": 5 }, $push: { hobbies: "football" } });
console.log(upd.matchedCount, upd.modifiedCount);
console.log(await students.findOne({ name: "Kabir" }, { projection: { _id: 0 } }));

// Delete
const del = await students.deleteOne({ name: "Aarav" });
console.log(del.deletedCount);

await client.close();
Output
true
[
  { name: 'Priya', marks: { maths: 95 } },
  { name: 'Meera', marks: { maths: 91 } },
  { name: 'Aarav', marks: { maths: 88 } }
]
{ name: 'Meera' }
2
1 1
{
  name: 'Kabir',
  city: 'Mumbai',
  marks: { maths: 72, science: 74 },
  hobbies: [ 'football' ]
}
1

How MongoDB queries work:

  • A filter is an object describing the documents you want. { city: "Kochi" } means "city equals Kochi". Dot notation such as "marks.maths" reaches inside nested objects, and matching an array field such as hobbies: "chess" means "the array contains chess".
  • Operators start with $: $gt, $gte, $lt, $lte, $ne (not equal), $in (one of a list), $regex and more.
  • A projection chooses which fields to return: 1 includes, 0 excludes.
  • find() returns a cursor; chain .sort(), .limit() and .skip(), then .toArray() to get the results.
  • Update operators change parts of a document: $set sets fields, $inc adds to a number, $push adds to an array (creating it if missing), $unset removes a field.
SQLMongoDB driver
INSERT INTO students ...insertOne({...}), insertMany([...])
SELECT * FROM students WHERE city = 'Pune'find({ city: "Pune" }).toArray()
... WHERE maths >= 85 ORDER BY maths DESCfind({ maths: { $gte: 85 } }).sort({ maths: -1 })
UPDATE students SET city = 'Pune' WHERE ...updateOne(filter, { $set: { city: "Pune" } })
DELETE FROM students WHERE ...deleteOne(filter), deleteMany(filter)

IDs, indexes and duplicate errors

IDs from URLs are strings, but _id values are ObjectIds, so convert them, and check the string first:

JavaScript
await students.createIndex({ name: 1 }, { unique: true });
try {
  await students.insertOne({ name: "Priya", city: "Delhi" });
} catch (err) {
  console.log(err.code, err.name);
}

const idString = (await students.findOne({ name: "Meera" }))._id.toString();
console.log(ObjectId.isValid(idString), ObjectId.isValid("12345"));
console.log((await students.findOne({ _id: new ObjectId(idString) })).name);
Output
11000 MongoServerError
true false
Meera

An index makes searches on a field fast, and a unique index refuses duplicates with error code 11000, which your API can turn into 409 Conflict. Create indexes for the fields you filter and sort by most often.

Mongoose: schemas and models

MongoDB does not force documents in a collection to have the same shape. That freedom is convenient but risky: a typo such as nmae silently creates a new field. Mongoose adds a schema that describes each field's type and rules, and a model class for querying.

Terminal
npm install mongoose
JavaScript
// models.js
import mongoose from "mongoose";

const studentSchema = new mongoose.Schema(
  {
    name: { type: String, required: [true, "Name is required"], trim: true, maxlength: 60 },
    email: { type: String, required: true, unique: true, lowercase: true, trim: true },
    course: { type: String, enum: ["BCA", "BSc", "BTech"], default: "BCA" },
    fees: { type: Number, min: 0 },
    city: String,
  },
  { timestamps: true }, // adds createdAt and updatedAt
);

export const Student = mongoose.model("Student", studentSchema);

The model Student works with the collection students (Mongoose makes the name lowercase and plural).

JavaScript
import mongoose from "mongoose";
import { Student } from "./models.js";

await mongoose.connect(process.env.MONGODB_URI);
await Student.init(); // wait until the unique index on email exists

const rohan = await Student.create({ name: "  Rohan Verma ", email: "Rohan@Example.in", fees: 45000, city: "Lucknow" });
console.log(rohan.name, rohan.email, rohan.course, rohan.createdAt instanceof Date);

try {
  await Student.create({ email: "x@example.in", course: "MBA", fees: -10 });
} catch (err) {
  console.log(err.name);
  for (const [field, e] of Object.entries(err.errors)) console.log(`  ${field}: ${e.message}`);
}

await Student.create({ name: "Ananya", email: "ananya@example.in", course: "BTech", fees: 120000, city: "Kochi" });

const list = await Student.find({ fees: { $lt: 100000 } }).select("name course -_id").lean();
console.log(list);

const updated = await Student.findOneAndUpdate(
  { email: "rohan@example.in" },
  { $set: { city: "Pune" } },
  { returnDocument: "after", runValidators: true },
);
console.log(updated.city);

await mongoose.disconnect();
Output
Rohan Verma rohan@example.in BCA true
ValidationError
  name: Name is required
  course: `MBA` is not a valid enum value for path `course`.
  fees: Path `fees` (-10) is less than minimum allowed value (0).
[ { name: 'Rohan Verma', course: 'BCA' } ]
Pune

Mongoose trimmed the name, lowercased the email, filled in the default course and added timestamps. Invalid data was rejected with a ValidationError listing every problem. .lean() returns plain objects instead of full Mongoose documents, which is faster for read-only API responses. runValidators: true applies the schema rules to updates too, because Mongoose skips them on updates by default.

Mongoose in an Express API

JavaScript
import express from "express";
import mongoose from "mongoose";
import { Student } from "./models.js";

await mongoose.connect(process.env.MONGODB_URI);

const app = express();
app.use(express.json());

app.get("/api/students/:id", async (req, res) => {
  if (!mongoose.isValidObjectId(req.params.id)) return res.status(400).json({ error: "Invalid id" });
  const student = await Student.findById(req.params.id).lean();
  if (!student) return res.status(404).json({ error: "Student not found" });
  res.json(student);
});

app.post("/api/students", async (req, res) => {
  const { name, email, course, fees, city } = req.body; // pick fields, never pass req.body directly
  try {
    const student = await Student.create({ name, email, course, fees, city });
    res.status(201).json(student);
  } catch (err) {
    if (err.name === "ValidationError") return res.status(400).json({ error: err.message });
    if (err.code === 11000) return res.status(409).json({ error: "Email already registered" });
    throw err;
  }
});

app.listen(3000);
Output
POST /api/students -> 201 {"name":"Isha","email":"isha@example.in","course":"BSc","city":"Jaipur","_id":"6ae4722caebc3f8fcf685d1c","createdAt":"2026-10-30T10:32:12.943Z","updatedAt":"2026-10-30T10:32:12.943Z","__v":0}
POST /api/students -> 409 {"error":"Email already registered"}
GET /api/students/12345 -> 400 {"error":"Invalid id"}
GET /api/students/6ae4722daebc3f8fcf685d1f -> 404 {"error":"Student not found"}

__v is Mongoose's internal version key. Pick the fields you allow from req.body instead of passing the whole object, so nobody can set fields such as role or fees that they should not control.

Common mistakes

  • Comparing _id with a string. { _id: "6ae4..." } finds nothing with the driver; use new ObjectId(id) (Mongoose converts for you).
  • Passing req.body straight into a filter or create(). This allows NoSQL injection and mass assignment. Validate and pick fields.
  • Forgetting .toArray() with the driver's find(). You get a cursor, not documents.
  • Opening a new connection per request. Connect once at startup and reuse the client or the Mongoose connection.
  • No indexes. Queries scan every document; add indexes for fields you filter and sort on, and unique indexes for emails.

Key points

  • MongoDB stores JSON-like documents in collections; every document has a unique _id, usually an ObjectId.
  • The mongodb driver gives insertOne, find, findOne, updateOne with $set/$inc/$push, and deleteOne.
  • Filters use operators such as $gte and $in; projections choose fields; cursors support sort and limit.
  • Unique indexes reject duplicates with error code 11000.
  • Mongoose adds schemas, validation, defaults and timestamps; use .lean() for fast reads and runValidators on updates.
  • Validate input types before querying to prevent NoSQL injection.

Exercise

Start with the four students inserted in the driver example (Aarav, Priya, Kabir and Meera, before any updates or deletes). Write a query that returns the names and science marks of students with science marks of 80 or more, sorted from highest to lowest, without the _id field. Then write an update that adds the hobby "music" to every student from Jaipur.

Show solution
JavaScript
const goodInScience = await students
  .find({ "marks.science": { $gte: 80 } }, { projection: { _id: 0, name: 1, "marks.science": 1 } })
  .sort({ "marks.science": -1 })
  .toArray();
console.log(goodInScience);

const result = await students.updateMany({ city: "Jaipur" }, { $push: { hobbies: "music" } });
console.log(`Updated ${result.modifiedCount} student(s)`);
Output
[
  { name: 'Meera', marks: { science: 97 } },
  { name: 'Aarav', marks: { science: 92 } },
  { name: 'Priya', marks: { science: 89 } }
]
Updated 1 student(s)

Dot notation reaches the nested science field, -1 sorts in descending order, and updateMany() changes every matching document instead of only the first.

Quick Quiz

1. What is a collection in MongoDB?

2. Which filter finds students whose maths marks are 90 or more?

3. What does the update { $inc: { stock: -2 } } do?

4. An attacker sends { "email": { "$ne": null } } to a login route. What prevents this NoSQL injection?

Learn Node.js with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call