Intermediate

Using Express with a NoSQL Database

Connect Express to MongoDB Atlas with the native driver and query documents.

Express.js + MongoDB (Native Node.js Driver)


Table of Contents


Module 1 — Intro to MongoDB and Atlas

SQL vs. NoSQL Databases

Relational databases (SQL) store data as tables with rigid columns and rows. NoSQL databases offer a more flexible and adaptable structure, particularly useful when data is too dynamic to fit in rows and columns.

CharacteristicSQL (Relational)NoSQL (MongoDB)
StructureTables + fixed columnsCollections + flexible documents
SchemaStrict, predefinedFlexible, optional
DataRows / recordsBSON documents (JSON-like)
RelationshipsForeign keys, JOINsNested documents or references
ScalabilityVerticalHorizontal

MongoDB Data Structure

MongoDB organizes data into collections (analogous to tables) composed of documents (analogous to rows). Documents are stored in BSON (Binary JSON) format, making them very similar to JSON objects.

Example MongoDB document for a social network application:

{
  "_id": { "$oid": "64f1a2b3c4d5e6f7a8b9c0d1" },
  "profile_id": 1,
  "first_name": "John",
  "last_name": "Doe",
  "company": "ABC Corp",
  "position": "Marketing Manager",
  "industry": "Technology",
  "location": "San Francisco, CA, United States",
  "education": "University of California, Berkeley",
  "connections": 500,
  "followers": 1000,
  "posts_per_week": 5
}

Key points:

  • Each document automatically receives an _id of type ObjectId — a guaranteed unique identifier.
  • There is no imposed schema: two documents in the same collection can have different fields.
  • Documents can contain nested documents or arrays.
  • The ObjectId is similar to a primary key in a relational database.

Configuring MongoDB Atlas

Atlas is MongoDB’s cloud platform. A cluster is a set of servers that hosts your databases for scalability and redundancy.

Setup steps:

  1. Create an account at mongodb.com → click Try Atlas Free
  2. Choose the free tier and deploy a cluster (e.g., Cluster0)
  3. Atlas automatically generates an admin user with full access
  4. Create a database user with a password
  5. Configure authorized IP addresses
  6. Retrieve the connection string via Connect → Drivers

The connection string looks like:

mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority

Module 2 — Connect MongoDB to Express

Installation and Connection String

Install the official MongoDB driver for Node.js:

npm install mongodb

The driver exposes the MongoClient class for interacting with the Atlas cluster. Initial connection code (provided by Atlas):

// connection.js (initial test version)
import { MongoClient, ServerApiVersion } from 'mongodb';

const uri = "mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/";

const client = new MongoClient(uri, {
  serverApi: {
    version: ServerApiVersion.v1,
    strict: true,
    deprecationErrors: true,
  }
});

async function run() {
  try {
    await client.connect();
    await client.db("admin").command({ ping: 1 });
    console.log("Successfully connected to MongoDB!");
  } finally {
    await client.close();
  }
}

run().catch(console.dir);

Securing the Connection String with dotenv

Storing credentials directly in code is a security bad practice. The solution is to use environment variables via the dotenv package.

npm install dotenv

.env file (at the project root, never committed to Git):

ATLAS_URI=mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority

Important: Do not wrap the connection string in quotes in the .env file.

Verify that .env is in .gitignore:

# .gitignore
.env
node_modules/

Inserting Documents with insertMany

After confirming the connection, create a database, collection, and insert data in bulk.

// connection.js — insertMany version (one-time use for seeding)
import { MongoClient } from 'mongodb';
import dotenv from 'dotenv';
dotenv.config();

import data from './data.js';

const uri = process.env.ATLAS_URI;
const client = new MongoClient(uri);

async function run() {
  try {
    const database = client.db('social_app');       // Creates DB if not exists
    const profiles = database.collection('profiles'); // Creates collection if not exists

    const result = await profiles.insertMany(data);
    console.log(`${result.insertedCount} documents were inserted`);
  } catch (error) {
    console.error(error);
  } finally {
    await client.close();
  }
}

run();

insertMany returns an object containing the insertedCount property indicating how many documents were inserted.

Persistent Database Connection

In an Express application, you don’t want to open/close the connection on every request. Establish a persistent connection at application startup.

// connection.js — persistent connection (production)
import { MongoClient } from 'mongodb';
import dotenv from 'dotenv';
dotenv.config();

const uri = process.env.ATLAS_URI;
const client = new MongoClient(uri);

let connection;

try {
  connection = await client.connect();
} catch (error) {
  console.error(error);
}

const db = connection.db('social_app');

export default db;
// app.js — import the connection
import express from 'express';
const app = express();

import { ObjectId } from 'mongodb';
import db from './connection.js';

// ...routes...

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

The db variable represents the social_app database. It is exported and imported in app.js for use in all routes.


Module 3 — Work with Documents: Query Language Basics

HTTP Client for Testing Routes

To test Express routes (GET, POST, PATCH, DELETE), this course uses the RapidAPI Client VS Code extension. Popular alternatives include Postman, Insomnia, or the REST Client extension.

Routes defined in the application:

MethodRouteOperation
GET/profilesRetrieve all profiles
GET/profiles/:idRetrieve a profile by ID
POST/profilesCreate a new profile
PATCH/profiles/:idModify an existing profile
DELETE/profiles/:idDelete a profile

find — Retrieve All Documents

The find method returns a cursor (pointer to results). Call .toArray() to get a JavaScript array.

// GET /profiles — Retrieve all documents
app.get('/profiles', async (req, res, next) => {
  try {
    const collection = await db.collection('profiles');

    // find({}) = find all documents (empty object = no filter)
    const results = await collection.find({}).toArray();

    res.status(200).json(results);
  } catch (error) {
    next(error);
  }
});

Query objects — to filter results:

// Filter by specific field
const results = await collection.find({ industry: "Technology" }).toArray();

// Filter by numeric value
const results = await collection.find({ connections: { $gt: 400 } }).toArray();

// Combine multiple criteria (implicit AND)
const results = await collection.find({
  industry: "Technology",
  location: "San Francisco, CA, United States"
}).toArray();

// find without argument = returns all documents
const results = await collection.find().toArray();

// Limit number of results
const results = await collection.find({}).limit(10).toArray();

findOne — Retrieve a Document by ID

findOne returns the first matching document. The MongoDB ObjectId is not a simple string — it must be converted with ObjectId.createFromHexString().

// GET /profiles/:id — Retrieve a document by its ObjectId
app.get('/profiles/:id', async (req, res, next) => {
  try {
    const collection = await db.collection('profiles');

    // ID comes as string in req.params.id
    // Must be converted to MongoDB ObjectId
    const query = { _id: ObjectId.createFromHexString(req.params.id) };
    const result = await collection.findOne(query);

    res.status(200).json(result);
  } catch (error) {
    next(error);
  }
});

Advantage of createFromHexString: if the provided ID is invalid (wrong format), an explicit error is returned automatically — a form of built-in validation.

insertOne — Create a New Document

insertOne inserts a single document and returns an object containing insertedId. You can then use this ID to retrieve the newly created document and send it back to the client.

// POST /profiles — Create a new profile
app.post('/profiles', async (req, res, next) => {
  try {
    const collection = await db.collection('profiles');

    const results = await collection.insertOne({
      "profile_id": 101,
      "first_name": "Santiago",
      "last_name": "Gomez",
      "company": "Global Innovation",
      "position": "CTO",
      "industry": "Technology",
      "location": "Barcelona, Spain",
      "education": "University of Barcelona",
      "connections": 850,
      "followers": 1350,
      "posts_per_week": 4
    });

    // Retrieve the inserted document to return to the client
    const inserted = await collection.findOne({ _id: results.insertedId });
    res.status(201).json(inserted);
  } catch (error) {
    next(error);
  }
});

In production, document data would come from the HTTP request body (req.body) with express.json() middleware.

updateOne — Modify a Document

updateOne takes two arguments:

  1. A query object to locate the document
  2. An update object with modifications (using operators like $set)
// PATCH /profiles/:id — Modify specific fields
app.patch('/profiles/:id', async (req, res, next) => {
  try {
    const collection = await db.collection('profiles');

    // 1st argument: find the document by ID
    const query = { _id: ObjectId.createFromHexString(req.params.id) };

    // 2nd argument: specify fields to modify with $set
    const updates = {
      $set: {
        first_name: "Victoria",
        company: "New Company Inc."
      }
    };

    const result = await collection.updateOne(query, updates);

    // Retrieve the updated document to return
    const updated = await collection.findOne(query);
    res.status(200).json(updated);
  } catch (error) {
    next(error);
  }
});

$set only replaces the specified fields. Other document fields remain intact.

deleteOne — Remove a Document

deleteOne deletes the first document matching the query object. It returns an object with the deletedCount property.

// DELETE /profiles/:id — Delete a profile
app.delete('/profiles/:id', async (req, res, next) => {
  try {
    const collection = await db.collection('profiles');

    const result = await collection.deleteOne({
      _id: ObjectId.createFromHexString(req.params.id)
    });

    res.status(200).json({
      message: `${result.deletedCount} document(s) have been deleted.`
    });
  } catch (error) {
    next(error);
  }
});

Global Error Handler

// Global error handling middleware (always last)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: {
      message: err.message || 'Internal Server Error'
    }
  });
});

The next(error) pattern in routes passes the caught error to this global handler. This centralizes error handling and avoids duplication.


Architecture and Diagrams

Express + MongoDB Architecture

graph TB
    Client["Client\n(Browser / Postman / REST Client)"]
    Express["Express Application\napp.js\n(Port 3000)"]
    Connection["connection.js\n(MongoClient)"]
    DotEnv[".env\nATLAS_URI"]
    Atlas["MongoDB Atlas\nCluster (Cloud)"]
    DB["Database: social_app"]
    Collection["Collection: profiles"]

    Client -->|"HTTP Request\nGET / POST / PATCH / DELETE"| Express
    Express -->|"import db"| Connection
    Connection -->|"process.env.ATLAS_URI"| DotEnv
    Connection -->|"client.connect()"| Atlas
    Atlas --> DB
    DB --> Collection
    Express -->|"HTTP Response\nJSON"| Client

CRUD Flow with the Native MongoDB Driver

sequenceDiagram
    participant C as Client
    participant E as Express Route
    participant M as MongoDB Driver
    participant A as Atlas Cluster

    C->>E: GET /profiles/:id
    E->>E: ObjectId.createFromHexString(req.params.id)
    E->>M: collection.findOne({ _id: objectId })
    M->>A: Query on social_app.profiles
    A-->>M: JSON Document
    M-->>E: result
    E-->>C: res.status(200).json(result)

    Note over E,A: Same pattern for find, insertOne, updateOne, deleteOne

Data Schema — Profile Document

erDiagram
    PROFILES {
        ObjectId _id PK "Automatically generated by MongoDB"
        int profile_id "Application identifier"
        string first_name "First name"
        string last_name "Last name"
        string company "Company"
        string position "Job title"
        string industry "Industry sector"
        string location "Geographic location"
        string education "Academic background"
        int connections "Number of connections"
        int followers "Number of followers"
        int posts_per_week "Posts per week"
    }

Express Middleware Pipeline

flowchart LR
    Req["Incoming\nHTTP Request"]
    JSON["express.json()\nParse the body"]
    Route["Route Handler\nasync (req, res, next)"]
    DB["MongoDB\nOperation"]
    Resp["Response\nres.json()"]
    Err["Global Error Handler\napp.use(err, req, res, next)"]

    Req --> JSON
    JSON --> Route
    Route -->|"Success"| DB
    DB --> Resp
    Route -->|"next(error)"| Err
    DB -->|"Error catch"| Err
    Err --> Resp

Project Structure

nosql-express-mongodb/
├── .env                    # Environment variables (ATLAS_URI) — not committed
├── .gitignore              # Includes .env and node_modules/
├── package.json            # Dependencies: express, mongodb, dotenv
├── connection.js           # Persistent MongoClient connection → exports db
├── app.js                  # Express CRUD routes + global error handler
└── data.js                 # Initial data (seed) — array of 100 profiles

package.json — project configuration:

{
  "name": "express_mongo_example",
  "version": "1.0.0",
  "main": "app.js",
  "type": "module",
  "scripts": {
    "dev": "node --watch app.js"
  },
  "dependencies": {
    "dotenv": "^16.4.5",
    "express": "^4.19.2",
    "mongodb": "^6.8.0"
  }
}

"type": "module" enables ES Modules syntax (import/export) instead of CommonJS (require/module.exports).
node --watch automatically restarts the server on file changes (equivalent to nodemon).


Reference Tables

MongoDB Collection Methods

MethodDescriptionReturns
find(query)Retrieve multiple documentsCursor → .toArray()
findOne(query)Retrieve the first matching documentDocument or null
insertOne(doc)Insert a document{ insertedId, acknowledged }
insertMany(docs)Insert multiple documents{ insertedCount, insertedIds }
updateOne(query, update)Modify the first matching document{ matchedCount, modifiedCount }
updateMany(query, update)Modify all matching documents{ matchedCount, modifiedCount }
deleteOne(query)Delete the first matching document{ deletedCount }
deleteMany(query)Delete all matching documents{ deletedCount }
countDocuments(query)Count matching documentsnumber

MongoDB Update Operators

OperatorDescriptionExample
$setSet/modify specific fields{ $set: { name: "Bob" } }
$unsetRemove a field{ $unset: { oldField: "" } }
$incIncrement a numeric value{ $inc: { connections: 1 } }
$pushAdd an element to an array{ $push: { tags: "node" } }
$pullRemove an element from an array{ $pull: { tags: "node" } }
$renameRename a field{ $rename: { old: "new" } }

Comparison Operators in Query Objects

OperatorMeaningExample
$eqEqual to (implicit default){ age: { $eq: 30 } }
$neNot equal to{ industry: { $ne: "Finance" } }
$gtGreater than{ connections: { $gt: 400 } }
$gteGreater than or equal to{ followers: { $gte: 1000 } }
$ltLess than{ posts_per_week: { $lt: 3 } }
$lteLess than or equal to{ connections: { $lte: 500 } }
$inValue in an array{ industry: { $in: ["Tech", "Media"] } }
$ninValue not in an array{ location: { $nin: ["London"] } }
$existsField exists{ company: { $exists: true } }

HTTP Methods and CRUD Operations

HTTP MethodCRUD OperationExpress MethodMongoDB Method
GETRead (all)app.get('/profiles')find({}).toArray()
GETRead (one)app.get('/profiles/:id')findOne({ _id: objectId })
POSTCreateapp.post('/profiles')insertOne(document)
PATCHUpdate (partial)app.patch('/profiles/:id')updateOne(query, { $set: ... })
DELETEDeleteapp.delete('/profiles/:id')deleteOne({ _id: objectId })

Key Term Glossary

TermDefinition
MongoClientMain class of the MongoDB Node.js driver for interacting with the cluster
ObjectIdUnique data type generated by MongoDB for _id — must be converted from string
createFromHexStringObjectId method for converting a hexadecimal string to a valid ObjectId
Connection stringAtlas connection URL — must always be stored in .env
Persistent connectionA single MongoDB connection opened at startup, shared by all routes
Query objectJSON object passed to MongoDB methods to filter documents
CursorResult of find() — requires .toArray() to get a JavaScript array
$setUpdate operator that only modifies the specified fields
next(error)Express pattern to pass an error to the global error handler
ES Modules"type": "module" in package.json enables import/export in Node.js

Search Terms

express · nosql · database · node.js · apis · microservices · backend · full-stack · web · mongodb · document · connection · documents · architecture · atlas · crud · data · http · methods · operators · query · retrieve · string

Interested in this course?

Contact us to book it or get a custom training plan for your team.