System Design

How We Chose Between Driver and Mongoose for MongoDB CRUD

We compared the MongoDB Node.js Driver and Mongoose ODM for CRUD operations — full code examples, real trade-offs, and the decision framework we used to choose.

M
Md Shayon
Sep 17, 2026
9 min read
Table of Contents
How We Chose Between Driver and Mongoose for MongoDB CRUD

When we started building a new Node.js service backed by MongoDB, we ran into a decision most teams hit early: use the official MongoDB Driver directly, or reach for Mongoose?

Both are valid. Both are widely used. Both handle the same four CRUD operations — create, read, update, and delete.

But they don't feel the same to work with. And the choice shapes how your codebase grows over months and years, not just how your first endpoint gets written.

This is how we evaluated the two options, what each one looks like in practice, and the reasoning that settled it for us.

The Problem We Were Trying to Solve

We were building a Node.js API that needed to:

  • Store user records and product data

  • Validate input before it ever hit the database

  • Evolve its schema over time as features shipped

  • Stay readable for developers who joined the project later

Nothing exotic. But those requirements pointed toward specific trade-offs between the raw driver and an ODM.

Before comparing them, a quick definition.

What CRUD Means in MongoDB

CRUD stands for Create, Read, Update, Delete. In MongoDB, each operation maps to a method:

Operation

Meaning

MongoDB Method

Create

Add new data

insertOne()

Read

Retrieve data

find()

Update

Change existing data

updateOne()

Delete

Remove data

deleteOne()

Say we have a users collection with documents like this:

{
  "name": "Shayon",
  "email": "shayon@example.com",
  "age": 25
}

We wanted to create a new user, find one, update their age, and eventually delete them. Both the Driver and Mongoose handle all four operations. The difference is how they handle them.

Option 1: The Official MongoDB Driver

The MongoDB Driver is the lower-level option. It talks straight to MongoDB with minimal abstraction between your code and the database.

npm install mongodb

Connecting to MongoDB

import { MongoClient } from "mongodb";
const client = new MongoClient("mongodb://localhost:27017");
await client.connect();
const db = client.db("shop");
const users = db.collection("users");

MongoClient manages the connection. From there, we pick the shop database and the users collection. That's it — no schemas, no models, just a handle on your data.

Creating a Document

const result = await users.insertOne({
  name: "Shayon",
  email: "shayon@example.com",
  age: 25,
});

console.log(result.insertedId);

MongoDB generates an _id automatically if you don't supply one.

Reading Documents

One document:

const user = await users.findOne({
  email: "shayon@example.com",
});

The empty filter {} matches every document in the collection.

Updating a Document

await users.updateOne(
  { email: "shayon@example.com" },
  { $set: { age: 26 } }
);

The first object selects the document. The $set operator defines the change.

Deleting a Document

await users.deleteOne({
  email: "shayon@example.com",
});

The Complete Driver CRUD Example

Here's everything in one runnable script:

import { MongoClient } from "mongodb";

const client = new MongoClient("mongodb://localhost:27017");

async function main() {
  await client.connect();

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

  // CREATE
  const result = await users.insertOne({
    name: "Shayon",
    email: "shayon@example.com",
    age: 25,
  });
  console.log("Created:", result.insertedId);

  // READ
  const user = await users.findOne({
    email: "shayon@example.com",
  });
  console.log("Found:", user);

  // UPDATE
  await users.updateOne(
    { email: "shayon@example.com" },
    { $set: { age: 26 } }
  );
  console.log("User updated");

  // DELETE
  await users.deleteOne({
    email: "shayon@example.com",
  });
  console.log("User deleted");

  await client.close();
}

main().catch(console.error);

What We Liked About the Driver

  • Direct control. Nothing happens that you didn't write.

  • Fewer dependencies. One package, no schema layer.

  • Full MongoDB feature access. Every operator, aggregation stage, and index option is available without translation.

  • Predictable performance. No middleware or schema validation running behind the scenes.

What Gave Us Pause

  • No schema enforcement. Nothing stops bad data from being written.

  • Validation is manual. You write and maintain every check yourself.

  • More boilerplate. Every operation is explicit — good for clarity, slower for development.

  • Repeated patterns. Field names and document structures get duplicated across the codebase.

Option 2: Mongoose, the ODM Approach

Mongoose is an ODM — an Object Document Mapper. It sits between your application and the MongoDB Driver, adding schemas, models, validation, and middleware.

bash

npm install mongoose

Defining a Schema

First, declare the structure of your documents:

import mongoose from "mongoose";

const userSchema = new mongoose.Schema({
  name: { type: String, required: true },
  email: { type: String, required: true },
  age: { type: Number, required: true },
});

Creating a Model

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

The User model exposes methods for working with the collection.

Connecting to MongoDB

await mongoose.connect("mongodb://localhost:27017/shop");

CRUD with the User Model

Create:

const user = await User.create({
  name: "Shayon",
  email: "shayon@example.com",
  age: 25,
});

Read:

const user = await User.findOne({
  email: "shayon@example.com",
});

const users = await User.find({});

Update:

await User.updateOne(
  { email: "shayon@example.com" },
  { $set: { age: 26 } }
);

Delete:

await User.deleteOne({
  email: "shayon@example.com",
});

The Complete Mongoose CRUD Example

import mongoose from "mongoose";

const userSchema = new mongoose.Schema({
  name: { type: String, required: true },
  email: { type: String, required: true },
  age: { type: Number, required: true },
});

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

async function main() {
  await mongoose.connect("mongodb://localhost:27017/shop");

  // CREATE
  const user = await User.create({
    name: "Shayon",
    email: "shayon@example.com",
    age: 25,
  });
  console.log("Created:", user);

  // READ
  const foundUser = await User.findOne({
    email: "shayon@example.com",
  });
  console.log("Found:", foundUser);

  // UPDATE
  await User.updateOne(
    { email: "shayon@example.com" },
    { $set: { age: 26 } }
  );
  console.log("User updated");

  // DELETE
  await User.deleteOne({
    email: "shayon@example.com",
  });
  console.log("User deleted");

  await mongoose.connection.close();
}

main().catch(console.error);

What We Liked About Mongoose

  • Schema definition. The structure of every document is declared once.

  • Built-in validation. required: true alone eliminates a whole class of bugs.

  • Readable models. User.create() and User.findOne() read like domain logic, not database calls.

  • Middleware hooks. Pre-save and post-save logic lives in one place.

  • Documentation and community. Well-established, with answers to most problems a search away.

What Gave Us Pause

  • Extra abstraction. Debugging sometimes means tracing through Mongoose internals.

  • Opinionated defaults. Behavior you didn't write can surprise you.

  • Performance overhead. Schema validation and middleware add work on every operation.

  • Not required by MongoDB. Mongoose is optional — MongoDB itself doesn't need it.

Driver vs Mongoose: The Same Operations, Different Surroundings

Put the methods side by side and something jumps out:

Operation

MongoDB Driver

Mongoose

Create

insertOne()

create()

Read one

findOne()

findOne()

Read many

find()

find()

Update

updateOne()

updateOne()

Delete

deleteOne()

deleteOne()

The method names are nearly identical. The difference isn't the operations — it's what surrounds them.

Architecturally, the Driver gives you a direct path:

text

Node.js Application

MongoDB Driver

MongoDB

Mongoose inserts one more layer:

text

Node.js Application

Mongoose

MongoDB Driver

MongoDB

One terminology note while we're here: because MongoDB is a document database, ODM is the accurate term — not ORM. Mongoose is an Object Document Mapper, not an Object Relational Mapper. The distinction matters when you're reading documentation and evaluating other libraries.

How We Actually Made the Decision

We scored each option against the four requirements we started with:

Requirement

Driver

Mongoose

Store user and product data

Validate input before write

⚠️

Evolve schema over time

⚠️

Stay readable for new developers

⚠️

The Driver handled storage perfectly. But validation, schema evolution, and readability all required us to build our own layers on top.

That's the turning point, and it's the honest answer to the Driver-vs-Mongoose question: the Driver doesn't remove work — it defers it.

Choose the Driver, and eventually you write your own validation functions, field-name constants, input sanitization, and type checks. You can do all of it well. But it's code you now own and maintain for the life of the project.

Mongoose packages those same concerns into a layer we didn't have to build.

Readability was the requirement that tipped it for us. Martin Fowler put it plainly years ago: "Any fool can write code that a computer can understand. Good programmers write code that humans can understand." User.findOne({ email }) reads like domain logic. A raw collection query paired with a hand-rolled validation helper reads like plumbing. When we pictured a new developer joining the team six months in, the choice got easier.


If you are thinking to learn node.js, read this article before taking any dicision

When We'd Still Choose the Raw Driver

Mongoose isn't always the answer. We identified five scenarios where we'd reach for the Driver instead.

1. High-throughput, low-abstraction workloads. Logs, metrics, or events where speed matters more than validation — the Driver wins.

2. Unstructured or rapidly changing data. When documents genuinely don't have a fixed shape, a schema becomes friction instead of a safety net.

3. Heavy use of advanced MongoDB features. Aggregation pipelines, change streams, and bulk operations are more direct without a translation layer.

4. Minimal dependencies. Some teams prefer fewer packages in the dependency tree. That's a legitimate engineering position.

5. Full control over queries. When performance tuning requires knowing exactly what hits the wire, the Driver is transparent.

When Mongoose Is Worth the Abstraction

And the flip side — where npm install mongoose earns its place:

1. Structured, predictable data. Users, orders, products — anything with a known shape.

2. Validation matters. Schema-level validation prevents bad data at the source instead of at three in the morning.

3. Team scalability. New developers read User.findOne() faster than they read a raw query plus custom guards.

4. Schema evolution. Adding fields, defaults, and migrations is cleaner with a schema definition.

5. Application-level logic. Middleware hooks for password hashing, timestamps, and audit trails live in one predictable place.

Common Mistakes Worth Avoiding

Whichever option you pick, a few traps come up again and again.

Confusing an ODM with an ORM. MongoDB is a document database. Tools like Mongoose are ODMs, not ORMs. Getting this right matters when reading docs or comparing libraries.

Forgetting to close connections in scripts. With the Driver, that's await client.close(). With Mongoose, await mongoose.connection.close(). In production, you typically reuse connections rather than opening and closing one per request — but for one-off scripts, closing is a good habit.

Skipping update operators. MongoDB updates use operators like $set. Forget them, and you can accidentally replace an entire document instead of changing one field.

Assuming Mongoose is required. It isn't. MongoDB works perfectly well with the raw Driver. Mongoose is a convenience layer, not a dependency.

Learn more about mongodb here

The Takeaway

Both approaches perform the same CRUD operations: Create → Read → Update → Delete.

The MongoDB Driver gives you direct access and full control. Mongoose gives you structure, validation, and readability on top of that access.

For our project, we chose Mongoose — because validation, schema clarity, and team readability outweighed the extra abstraction. Your requirements table will probably look different from ours, and that's fine. Let your data, your team, and the amount of structure your application actually needs make the call.

Once the four operations feel comfortable in either approach, you're ready for what comes next: query operators, indexes, aggregation pipelines, transactions, pagination, and performance tuning.

Tags

# mongodb# nodejs# mongoose# mongodb-driver# crud-operations# odm# nosql# javascript# backend-development# database# web-development# nodejs-tutorial# mongoose-odm# api-development# database-design
Keep Reading

Related Articles

Continue your learning journey