Skip to content

MongoDB with Express

MongoDB is a document-oriented NoSQL database, and one of the most common pairings with Express. This lesson connects an Express app directly using the official mongodb driver, before introducing Mongoose in the next lesson.

Connecting With the Native Driver

The official mongodb npm package provides MongoClient, which manages a connection pool to your MongoDB server (or Atlas cluster) and exposes db()/collection() methods for running queries with plain JavaScript objects, no schema required.

Connect once at startup, keep the client (or the resolved database handle) around, and reuse it across every request rather than reconnecting each time.

const { MongoClient } = require('mongodb');

let db;

async function connectDB() {
  const client = new MongoClient(process.env.MONGO_URI);
  await client.connect();
  db = client.db('myapp');
  console.log('MongoDB connected');
}

module.exports = { connectDB, getDb: () => db };

client.connect() establishes a connection pool internally; client.db('myapp') returns a handle to a specific database within your MongoDB server.

Basic CRUD With the Native Driver

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

await users.insertOne({ email: 'ada@example.com' });
await users.find({}).toArray();
await users.findOne({ _id: id });
await users.updateOne({ _id: id }, { $set: { name: 'Ada' } });
await users.deleteOne({ _id: id });
  • insertOne/insertMany create documents; find/findOne read them.
  • updateOne/updateMany modify documents, typically with the $set operator.
  • deleteOne/deleteMany remove documents matching a filter.
  • MongoDB's _id is an ObjectId, not a plain string, convert route params with new ObjectId(idString).

MongoDB Native Driver Cheatsheet

The core operations you'll use directly against a MongoDB collection.

Operation Method Example
Create insertOne users.insertOne({ email })
Read all find users.find({}).toArray()
Read one findOne users.findOne({ _id: id })
Update updateOne users.updateOne(filter, { $set: data })
Delete deleteOne users.deleteOne({ _id: id })
Count countDocuments users.countDocuments(filter)

A Full Express Route Example

Combining the connection setup with a real router shows how the native driver looks inside actual route handlers.

const { ObjectId } = require('mongodb');
const { getDb } = require('../db');

router.get('/:id', async (req, res, next) => {
  try {
    const db = getDb();
    const user = await db.collection('users').findOne({ _id: new ObjectId(req.params.id) });
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.json(user);
  } catch (err) {
    next(err);
  }
});

Native Driver vs Mongoose

The native driver gives you full, unopinionated control over queries with no schema enforcement, closer to raw MongoDB. Mongoose (covered next) adds schemas, validation, and a model-based API on top, trading a bit of flexibility for structure and convenience.

Common Mistakes

  • Creating a new MongoClient connection on every request instead of reusing one connected client.
  • Passing a raw string ID directly into a query expecting an ObjectId, causing a cast error or silent no-match.
  • Forgetting await on driver calls, leading to unresolved promises being treated as truthy values.
  • Not handling findOne returning null for a missing document.

Key Takeaways

  • The mongodb package is MongoDB's official native driver, schema-free and close to raw queries.
  • Connect once at startup and reuse the same client/db handle across requests.
  • MongoDB document IDs are ObjectId instances, not plain strings.
  • The native driver trades structure for flexibility compared to Mongoose.

Pro Tip

Wrap new ObjectId(req.params.id) in a try/catch (or validate the string format first), an invalid ID string throws synchronously and will crash an unguarded route.