Lecture 22: Database Connectivity with PostgreSQL / MongoDB¶
Every real application needs to remember things after the server restarts — user accounts, blog posts, orders. That's what a database is for: software dedicated to storing, organizing, and retrieving data reliably. This lecture shows you how to connect a Node.js/Express application to a database, using MongoDB (a document database) as the primary example and PostgreSQL (a relational database) as a point of comparison.
In This Lecture¶
- Compare the relational (PostgreSQL) and document (MongoDB) data models
- Learn how to choose the right database for a given project
- Set up a database connection, understand connection strings, and connection pooling
- Perform CRUD operations from server-side code in both MongoDB and PostgreSQL
- Design collections/schemas and add basic validation
- Use environment variables safely and handle database errors properly
Relational vs. Document Data Models¶
A database stores data so it can be saved, searched, and updated efficiently, even after your server process stops running (this is called persistence — the data survives, or "persists," beyond the life of the program). There are many kinds of databases, but the two you will meet constantly are relational databases (like PostgreSQL, MySQL) and document databases (like MongoDB).
The Relational Model (PostgreSQL)¶
A relational database organizes data into tables, which look like spreadsheets: fixed columns (each with a defined data type) and any number of rows. Every row in a table has the same set of columns. Relationships between tables are expressed using foreign keys — a column in one table that refers to a row in another table.
-- users table
| id | name | email |
|----|---------|-------------------|
| 1 | Ayesha | ayesha@email.com |
| 2 | Bilal | bilal@email.com |
-- orders table (references users via user_id)
| id | user_id | total |
|----|---------|-------|
| 1 | 1 | 45.00 |
| 2 | 1 | 12.50 |
To combine data from both tables (say, "get Ayesha's orders"), you use a JOIN — a SQL operation that matches rows across tables using the foreign key.
The Document Model (MongoDB)¶
A document database stores data as documents — flexible, JSON-like objects — grouped into collections (roughly the document-database equivalent of a table). Each document can have its own set of fields, and related data is often embedded directly inside the parent document instead of living in a separate table.
// A document in the "users" collection
{
"_id": "64f1a2b3c4d5e6f7a8b9c0d1",
"name": "Ayesha",
"email": "ayesha@email.com",
"orders": [
{ "total": 45.00, "date": "2024-01-15" },
{ "total": 12.50, "date": "2024-02-02" }
]
}
Here, Ayesha's orders are stored inside her user document — no separate table and no JOIN needed to read them together.
flowchart LR
subgraph Relational["Relational (PostgreSQL)"]
direction TB
T1["users table\nid | name | email"]
T2["orders table\nid | user_id | total"]
T1 -- "foreign key\nJOIN" --> T2
end
subgraph Document["Document (MongoDB)"]
direction TB
D1["users collection\n{ name, email,\n orders: [ {...}, {...} ] }"]
end
Comparing the Two¶
| Relational (PostgreSQL) | Document (MongoDB) | |
|---|---|---|
| Structure | Tables with fixed columns | Collections of flexible JSON-like documents |
| Schema | Strict — defined up front, enforced by the database | Flexible — documents in the same collection can differ |
| Relationships | Foreign keys + JOINs | Embedding (nested data) or manual references |
| Best for | Data with clear structure and relationships (banking, inventory, anything needing strong consistency) | Data that's naturally nested, evolving, or read as a whole "unit" (user profiles, content, catalogs) |
| Query language | SQL (Structured Query Language) | MongoDB Query Language (JavaScript-like methods) |
| Scaling style | Traditionally scales up (a bigger server); modern versions also scale out | Designed from the start to scale out (spread across many servers) |
Terminology: SQL vs. NoSQL
You will often hear PostgreSQL/MySQL called SQL databases (after their query language) and MongoDB called a NoSQL database (meaning "not only SQL" — a broad category that includes document, key-value, and other non-relational databases).
Choosing the Right Database¶
There's no universal "best" database — the right choice depends on your data and your application's needs. Some practical guidelines:
- Choose PostgreSQL (relational) when your data has a clear, stable structure; when relationships between entities matter a lot (e.g., orders belonging to customers, belonging to a store); and when you need strong guarantees that data stays consistent (e.g., financial transactions, where you cannot afford a half-completed transfer).
- Choose MongoDB (document) when your data is naturally nested or hierarchical (e.g., a blog post with embedded comments); when your schema is likely to evolve quickly during early development; or when you're mostly reading/writing whole "documents" at a time (e.g., a user profile with all its settings).
Tip
Many real-world systems use both — a relational database for structured, transactional data (like payments) and a document database for flexible content (like product catalogs or activity logs). Choosing a database is a design decision per use case, not a one-time choice for the whole company.
Connection Setup, Connection Strings, and Pooling¶
To talk to a database, your Node.js server needs a driver — a library that knows how
to speak that database's network protocol. For MongoDB, that's the official mongodb
package (or mongoose, which we cover in the next lecture). For PostgreSQL, it's the
pg package.
A connection string (or URI) is a single string that packs together everything needed to reach the database: protocol, host, port, database name, and credentials.
mongodb://username:password@localhost:27017/myAppDB
postgresql://username:password@localhost:5432/myAppDB
Environment Variables and .env¶
You should never hard-code credentials (passwords, connection strings) directly in
your source code — anyone who sees the code (including everyone on GitHub, if the repo is
public) sees the password too. Instead, store them in environment variables: values
set outside your code, in the operating system or a .env file, and read into your
program at runtime.
# .env (never commit this file to Git)
MONGO_URI=mongodb://localhost:27017/myAppDB
PG_CONNECTION_STRING=postgresql://user:pass@localhost:5432/myAppDB
PORT=3000
// at the very top of your entry file
require("dotenv").config();
console.log(process.env.MONGO_URI); // reads the value from .env
Warning
Always add .env to your .gitignore file. Committing real credentials to a Git
repository — even a private one — is one of the most common causes of security
breaches in student and professional projects alike.
Connecting to MongoDB¶
const { MongoClient } = require("mongodb");
require("dotenv").config();
const client = new MongoClient(process.env.MONGO_URI);
async function main() {
await client.connect();
console.log("Connected to MongoDB");
const db = client.db("myAppDB");
return db;
}
main().catch(console.error);
Connecting to PostgreSQL and Connection Pooling¶
Opening a brand-new network connection for every single query is slow and wastes resources. Instead, both drivers give you a connection pool — a set of already-open connections that are reused across many queries, handed out to whichever part of your code needs one and returned to the pool when done.
const { Pool } = require("pg");
require("dotenv").config();
const pool = new Pool({
connectionString: process.env.PG_CONNECTION_STRING,
max: 10, // maximum number of connections kept in the pool
});
async function getUsers() {
const result = await pool.query("SELECT * FROM users");
return result.rows;
}
Note
MongoDB's driver also manages a connection pool internally (by default, up to 100
connections) even though you only call client.connect() once. You rarely need to
configure this yourself unless you're tuning a high-traffic production application.
sequenceDiagram
participant App as Express App
participant Pool as Connection Pool
participant DB as Database Server
App->>Pool: request a connection
Pool-->>App: hands out an existing connection
App->>DB: run query
DB-->>App: return results
App->>Pool: release connection back to pool
CRUD Operations¶
CRUD stands for Create, Read, Update, Delete — the four basic operations every data-driven application performs.
MongoDB Driver Example¶
async function crudDemo(db) {
const users = db.collection("users");
// CREATE
const insertResult = await users.insertOne({
name: "Ayesha",
email: "ayesha@email.com",
age: 21,
});
console.log("Inserted id:", insertResult.insertedId);
// READ
const oneUser = await users.findOne({ email: "ayesha@email.com" });
const allAdults = await users.find({ age: { $gte: 18 } }).toArray();
// UPDATE
await users.updateOne(
{ email: "ayesha@email.com" },
{ $set: { age: 22 } }
);
// DELETE
await users.deleteOne({ email: "ayesha@email.com" });
}
PostgreSQL (pg) Example¶
async function crudDemo(pool) {
// CREATE
const insertResult = await pool.query(
"INSERT INTO users (name, email, age) VALUES ($1, $2, $3) RETURNING id",
["Ayesha", "ayesha@email.com", 21]
);
console.log("Inserted id:", insertResult.rows[0].id);
// READ
const oneUser = await pool.query(
"SELECT * FROM users WHERE email = $1",
["ayesha@email.com"]
);
const allAdults = await pool.query("SELECT * FROM users WHERE age >= $1", [18]);
// UPDATE
await pool.query("UPDATE users SET age = $1 WHERE email = $2", [22, "ayesha@email.com"]);
// DELETE
await pool.query("DELETE FROM users WHERE email = $1", ["ayesha@email.com"]);
}
Always use parameterized queries
Notice the $1, $2 placeholders (PostgreSQL) instead of pasting variables
directly into the SQL string. Building queries by concatenating strings opens the
door to SQL injection, a serious security vulnerability where an attacker can
smuggle their own SQL commands through user input. The MongoDB driver's object-based
query syntax ({ email: someVariable }) is naturally safer in the same way, as long
as you don't build queries from raw, unvalidated strings yourself.
Schema / Collection Design and Validation¶
Even though MongoDB doesn't force a rigid schema, you should still plan your document structure deliberately — deciding what fields exist, their types, and whether related data should be embedded or referenced. MongoDB also supports optional schema validation at the database level:
await db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "email"],
properties: {
name: { bsonType: "string" },
email: { bsonType: "string", pattern: "^.+@.+$" },
age: { bsonType: "int", minimum: 0 },
},
},
},
});
In PostgreSQL, structure is enforced automatically through the table definition itself:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE NOT NULL,
age INTEGER CHECK (age >= 0)
);
Error Handling Around Database Calls¶
Database calls can fail for many reasons: the network drops, the connection string is
wrong, a query violates a constraint (like a duplicate email on a UNIQUE column), or the
database server is simply down. Never assume a database call will succeed — always wrap
it and respond sensibly.
app.post("/api/users", async (req, res) => {
try {
const result = await usersCollection.insertOne(req.body);
res.status(201).json({ id: result.insertedId });
} catch (err) {
console.error("Database error:", err.message);
res.status(500).json({ error: "Could not create user." });
}
});
Tip
Never send raw database error messages directly to the client in production — they can leak details about your schema or internal setup. Log the full error on the server, and send the client a short, generic message instead.
Try It Yourself¶
- Install MongoDB locally (or use a free MongoDB Atlas cluster) and connect to it from
a small Node.js script using the official
mongodbdriver. Insert three documents into aproductscollection, then write a query that finds only the products with apricegreater than 10. - Sketch (on paper or in a comment) how you would model a "blog post with comments" feature in both a relational schema (tables + foreign keys) and a MongoDB document (with embedded comments). Which approach would you pick, and why?
Key Takeaways¶
- Relational databases (PostgreSQL) use tables, fixed schemas, and JOINs; document databases (MongoDB) use flexible, JSON-like documents in collections, often embedding related data.
- Choose based on your data's shape and your app's needs — structured/relational data favors PostgreSQL, nested/evolving data favors MongoDB.
- A connection string tells your driver how to reach the database; a connection pool reuses connections instead of opening a new one per query.
- Keep credentials out of your source code — use environment variables and a
.envfile (never committed to Git). - CRUD (Create, Read, Update, Delete) operations look different in MongoDB (methods like
insertOne,find) versus PostgreSQL (SQL statements viapool.query), but the underlying goal is the same. - Always use parameterized queries to prevent SQL injection, and always wrap database
calls in
try/catchwith sensible error responses.