Express.js + MongoDB (Native Node.js Driver)
Table of Contents
- Module 1 — Intro to MongoDB and Atlas
- Module 2 — Connect MongoDB to Express
- Module 3 — Work with Documents: Query Language Basics
- Architecture and Diagrams
- Project Structure
- Reference Tables
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.
| Characteristic | SQL (Relational) | NoSQL (MongoDB) |
|---|---|---|
| Structure | Tables + fixed columns | Collections + flexible documents |
| Schema | Strict, predefined | Flexible, optional |
| Data | Rows / records | BSON documents (JSON-like) |
| Relationships | Foreign keys, JOINs | Nested documents or references |
| Scalability | Vertical | Horizontal |
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
_idof typeObjectId— 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
ObjectIdis 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:
- Create an account at mongodb.com → click Try Atlas Free
- Choose the free tier and deploy a cluster (e.g.,
Cluster0) - Atlas automatically generates an admin user with full access
- Create a database user with a password
- Configure authorized IP addresses
- 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
.envfile.
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:
| Method | Route | Operation |
|---|---|---|
| GET | /profiles | Retrieve all profiles |
| GET | /profiles/:id | Retrieve a profile by ID |
| POST | /profiles | Create a new profile |
| PATCH | /profiles/:id | Modify an existing profile |
| DELETE | /profiles/:id | Delete 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:
- A query object to locate the document
- 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);
}
});
$setonly 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 --watchautomatically restarts the server on file changes (equivalent tonodemon).
Reference Tables
MongoDB Collection Methods
| Method | Description | Returns |
|---|---|---|
find(query) | Retrieve multiple documents | Cursor → .toArray() |
findOne(query) | Retrieve the first matching document | Document 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 documents | number |
MongoDB Update Operators
| Operator | Description | Example |
|---|---|---|
$set | Set/modify specific fields | { $set: { name: "Bob" } } |
$unset | Remove a field | { $unset: { oldField: "" } } |
$inc | Increment a numeric value | { $inc: { connections: 1 } } |
$push | Add an element to an array | { $push: { tags: "node" } } |
$pull | Remove an element from an array | { $pull: { tags: "node" } } |
$rename | Rename a field | { $rename: { old: "new" } } |
Comparison Operators in Query Objects
| Operator | Meaning | Example |
|---|---|---|
$eq | Equal to (implicit default) | { age: { $eq: 30 } } |
$ne | Not equal to | { industry: { $ne: "Finance" } } |
$gt | Greater than | { connections: { $gt: 400 } } |
$gte | Greater than or equal to | { followers: { $gte: 1000 } } |
$lt | Less than | { posts_per_week: { $lt: 3 } } |
$lte | Less than or equal to | { connections: { $lte: 500 } } |
$in | Value in an array | { industry: { $in: ["Tech", "Media"] } } |
$nin | Value not in an array | { location: { $nin: ["London"] } } |
$exists | Field exists | { company: { $exists: true } } |
HTTP Methods and CRUD Operations
| HTTP Method | CRUD Operation | Express Method | MongoDB Method |
|---|---|---|---|
| GET | Read (all) | app.get('/profiles') | find({}).toArray() |
| GET | Read (one) | app.get('/profiles/:id') | findOne({ _id: objectId }) |
| POST | Create | app.post('/profiles') | insertOne(document) |
| PATCH | Update (partial) | app.patch('/profiles/:id') | updateOne(query, { $set: ... }) |
| DELETE | Delete | app.delete('/profiles/:id') | deleteOne({ _id: objectId }) |
Key Term Glossary
| Term | Definition |
|---|---|
| MongoClient | Main class of the MongoDB Node.js driver for interacting with the cluster |
| ObjectId | Unique data type generated by MongoDB for _id — must be converted from string |
createFromHexString | ObjectId method for converting a hexadecimal string to a valid ObjectId |
| Connection string | Atlas connection URL — must always be stored in .env |
| Persistent connection | A single MongoDB connection opened at startup, shared by all routes |
| Query object | JSON object passed to MongoDB methods to filter documents |
| Cursor | Result of find() — requires .toArray() to get a JavaScript array |
$set | Update 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