🐘 Setting up PostgreSQL
Your application's memory lives in a database, and PostgreSQL is the sturdy, open-source engine most professional Node.js teams reach for. Before you can query anything, you need Postgres installed, a database and user created, and Node talking to it — safely. This lesson gets all three done.
Week 8 · Day 2 (Tuesday: PostgreSQL with Node.js) · Lecture 1
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain what PostgreSQL is and why it pairs so well with a Node.js back end
- Install PostgreSQL on your platform (native installer, Homebrew, apt, or Docker)
- Create a database and a dedicated least-privilege user with
psql - Store credentials in environment variables instead of hardcoding them
- Connect a Node.js/Express app using the
pgPooland parameterized queries - Diagnose the most common "it won't connect" errors
Estimated Time: 70 minutes
Project: Stand up a local myapp_development database and hit it from a tiny Express endpoint.
In This Lesson
Why PostgreSQL?
PostgreSQL — everyone just says "Postgres" — is a free, open-source relational database. Relational means your data lives in tables of rows and columns with enforced relationships between them, and you talk to it in SQL. Postgres has been in active development for over three decades, and it has earned a reputation for correctness: it does exactly what the SQL standard says, and it does not lose your data.
Think of a database as a meticulous librarian. You hand it a precise request ("every order placed by customer 42, newest first") and it returns exactly those records, quickly, no matter how many millions of rows sit on the shelves. PostgreSQL is a particularly capable librarian — it understands advanced queries, complex data types, and even JSON documents.
your data stays correct] A --> C[Rich SQL
joins, window functions, CTEs] A --> D[JSON / JSONB
flexible documents too] A --> E[Extensible
custom types & extensions] A --> F[Open source
no license fees] A --> G[Huge ecosystem
tools, drivers, hosting]
What makes it a great default
| Strength | What it buys you |
|---|---|
| ACID compliance | Atomicity, Consistency, Isolation, Durability — a transfer that debits one account and credits another either fully completes or fully rolls back |
| Standards-compliant SQL | Skills transfer to other databases; fewer nasty surprises |
| Advanced features | Views, triggers, stored procedures, full-text search, window functions |
| Native JSON/JSONB | Store schemaless documents alongside structured rows, with indexing |
| Scalable | Runs a hobby project on a laptop and enterprise workloads on a cluster |
📖 Who runs on Postgres?
Apple (parts of iCloud), Instagram, Spotify, and Reddit all lean on PostgreSQL, as do countless startups. Managed hosts like Amazon RDS, Google Cloud SQL, Supabase, Neon, and Render make it a click to deploy — so the skills you build here are directly employable.
How Node.js & Postgres Fit Together
Node.js is exceptional at waiting well. Its event loop can juggle thousands of in-flight requests, and most of what a web server does is wait — for the network, the filesystem, and the database. A database query is the classic "go do this and tell me when you're done" operation, so it maps perfectly onto Node's asynchronous, async/await style.
Node handles other requests meanwhile PG-->>Node: rows Node-->>Browser: JSON response
Because the whole stack is JavaScript — React on the front end, Node on the back end, and query results arriving as plain JS objects — you stop context-switching between languages. That single-language flow is one of the biggest productivity wins of the full-stack JS approach.
Installing PostgreSQL
Pick one path below based on your machine. Docker is the most reproducible choice if you already have Docker installed, because it never pollutes your system and is trivial to reset.
Windows
- Download the interactive installer from the official Windows download page (EnterpriseDB build).
- Run it and follow the wizard. Write down the password you set for the built-in
postgressuperuser. - Keep the defaults: PostgreSQL Server, pgAdmin (a GUI), Command Line Tools, and port
5432. - When it finishes, search the Start menu for SQL Shell (psql) to open a terminal into your server.
macOS (Homebrew)
# Install PostgreSQL (version 16 shown; use the current major version)
brew install postgresql@16
# Start it now and on every login
brew services start postgresql@16
# Verify the server answers
psql --version
Prefer a GUI? Postgres.app is a drag-to-Applications alternative — open it and click Initialize.
Linux (Ubuntu / Debian)
sudo apt update
sudo apt install postgresql postgresql-contrib
# Confirm the service is running
sudo systemctl status postgresql
sudo systemctl start postgresql # if it wasn't already
Docker (reproducible, disposable)
# Run PostgreSQL 16 in a container, published on the standard port
docker run --name my-postgres \
-e POSTGRES_PASSWORD=change_me_in_real_life \
-p 5432:5432 \
-d postgres:16
# Open a psql session inside the container
docker exec -it my-postgres psql -U postgres
⚠️ That password is a placeholder
Never ship change_me_in_real_life or any password committed to source control. In this lesson we keep secrets in a .env file that is git-ignored — see the environment-variables section below.
Your First Database & User
The postgres superuser is your root key — powerful and dangerous. Best practice is to create an application-specific database and a dedicated user with only the privileges that app needs. That way a leaked app password can't drop every database on the server.
Open a psql shell (via SQL Shell on Windows, or the commands above), then run:
-- 1. Create a database for the app
CREATE DATABASE myapp_development;
-- 2. Create a dedicated, least-privilege user with a real password
CREATE USER myapp_user WITH ENCRYPTED PASSWORD 'supersecret_dev_pw';
-- 3. Let that user work inside the database
GRANT ALL PRIVILEGES ON DATABASE myapp_development TO myapp_user;
-- 4. Connect to the new database (psql meta-command)
\c myapp_development
-- 5. In Postgres 15+, also grant rights on the public schema
GRANT ALL ON SCHEMA public TO myapp_user;
💡 Meta-commands vs SQL
Lines starting with a backslash — \c, \dt, \q — are psql meta-commands, not SQL. \c dbname connects to a database, \dt lists tables, and \q quits. Everything else is standard SQL and would work from any client.
Now create a table and a couple of test rows so we have something to query from Node:
-- A tiny table with an auto-incrementing id and a timestamp
CREATE TABLE items (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
INSERT INTO items (name) VALUES ('First item'), ('Second item');
SELECT * FROM items;
-- id | name | created_at
-- ----+-------------+-------------------------------
-- 1 | First item | 2026-07-31 10:15:02.11+00
-- 2 | Second item | 2026-07-31 10:15:02.11+00
\q -- exit psql
If you see those two rows, your database is alive and reachable. Time to connect Node.
Credentials in Environment Variables
Hardcoding a database password into your source code is one of the most common — and most costly — security mistakes. Anyone who reads the repo (or your public GitHub) gets your credentials. The fix is to load configuration from the environment at runtime.
Set up a fresh Node project and install the driver plus dotenv, which loads a local .env file into process.env:
mkdir postgres-node-demo && cd postgres-node-demo
npm init -y
npm install pg dotenv express
Create a .env file in the project root:
# .env — never commit this file
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp_development
DB_USER=myapp_user
DB_PASSWORD=supersecret_dev_pw
And a .gitignore so it never reaches version control:
# .gitignore
node_modules/
.env
⚠️ Commit a template, not the secret
Teams usually commit a .env.example with the keys but blank values, so a new teammate knows what to fill in — while the real .env stays ignored. In production you don't use a file at all; the host (Render, Railway, AWS, etc.) injects the variables directly.
Connecting from Node.js
The pg package gives you two ways to connect: a single Client, and a Pool that manages many reusable connections. For a web server, always use a Pool — a fresh connection per request is slow and will exhaust the database. (We dig into pooling deeply two lessons from now.)
A reusable database module
Encapsulate the pool in one file and export a thin query helper. Everything else imports this module rather than creating its own connections.
// db.js — the single source of database connections
require('dotenv').config();
const { Pool } = require('pg');
// The Pool reads DB_* from the environment. Never inline the password.
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
});
// One-time smoke test so a bad config fails loudly at startup
pool.query('SELECT NOW()')
.then((res) => console.log('✅ Connected to PostgreSQL at', res.rows[0].now))
.catch((err) => console.error('❌ Database connection failed:', err.message));
module.exports = {
// Thin wrapper — callers pass SQL text and a params array
query: (text, params) => pool.query(text, params),
};
An Express endpoint that reads and writes
Notice every value coming from the outside world is passed as a parameter ($1), never glued into the SQL string. That single habit is your primary defense against SQL injection.
// app.js
const express = require('express');
const db = require('./db');
const app = express();
app.use(express.json());
// GET all items
app.get('/api/items', async (req, res) => {
try {
const result = await db.query('SELECT * FROM items ORDER BY id ASC');
res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
}
});
// POST a new item — user input flows in through $1, safely
app.post('/api/items', async (req, res) => {
const { name } = req.body;
if (!name) return res.status(400).json({ error: 'Name is required' });
try {
const result = await db.query(
'INSERT INTO items (name) VALUES ($1) RETURNING *',
[name] // ← values array, matched to $1
);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
⚠️ Never build SQL by string concatenation
// ❌ DANGEROUS — a value of "1; DROP TABLE items;" is catastrophic
const bad = `SELECT * FROM items WHERE id = ${req.params.id}`;
// ✅ SAFE — the driver sends the value separately from the SQL
const good = await db.query('SELECT * FROM items WHERE id = $1', [req.params.id]);
Parameterized queries send your SQL and your data on separate channels, so a malicious value can never change the shape of the query. This is non-negotiable.
Run it
node app.js
# In another terminal:
curl http://localhost:3000/api/items
curl -X POST http://localhost:3000/api/items \
-H "Content-Type: application/json" \
-d '{"name":"Bought from the API"}'
Output
✅ Connected to PostgreSQL at 2026-07-31T10:22:41.003Z
Server running on port 3000
[{"id":1,"name":"First item","created_at":"2026-07-31T10:15:02.110Z"}, ...]
When connecting won't work
| Error | Likely cause | Fix |
|---|---|---|
ECONNREFUSED | Server not running or wrong host/port | Start Postgres; confirm host localhost and port 5432 |
password authentication failed | Wrong user/password | Re-check .env; the user was created with that exact password |
database "..." does not exist | Typo or you never ran CREATE DATABASE | Create it, or fix DB_NAME |
permission denied for schema public | Postgres 15+ locks down public | GRANT ALL ON SCHEMA public TO myapp_user; |
Practice & Quiz
🏋️ Exercise 1: Stand up your own database
Goal: From an empty machine, create a bootcamp_dev database, a bootcamp_user with a password, and a notes table with columns id, body, and created_at. Insert one row and select it back.
💡 Hint
Use CREATE DATABASE, then CREATE USER ... WITH ENCRYPTED PASSWORD, then GRANT. Connect with \c bootcamp_dev before creating the table. Reach for SERIAL PRIMARY KEY and TIMESTAMPTZ DEFAULT NOW().
✅ Solution
CREATE DATABASE bootcamp_dev;
CREATE USER bootcamp_user WITH ENCRYPTED PASSWORD 'dev_pw_123';
GRANT ALL PRIVILEGES ON DATABASE bootcamp_dev TO bootcamp_user;
\c bootcamp_dev
GRANT ALL ON SCHEMA public TO bootcamp_user;
CREATE TABLE notes (
id SERIAL PRIMARY KEY,
body TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
INSERT INTO notes (body) VALUES ('My first note');
SELECT * FROM notes;
🏋️ Exercise 2: Add a safe lookup route
Goal: Add GET /api/items/:id to app.js. It must use a parameterized query and return 404 when nothing matches.
💡 Hint
Pass req.params.id as [id] to the query. Check result.rows.length === 0 before sending result.rows[0].
✅ Solution
app.get('/api/items/:id', async (req, res) => {
try {
const result = await db.query(
'SELECT * FROM items WHERE id = $1',
[req.params.id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Item not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
}
});
🎯 Quick Quiz
Question 1: In a web application, which should you use to connect to PostgreSQL?
Question 2: Why do we pass values as $1, $2 instead of building the SQL string?
Question 3: Where should your database password live?
Best Practices & Pitfalls
✅ Do
- Create a dedicated, least-privilege user per app instead of using
postgres - Keep credentials in environment variables; git-ignore your
.env - Use a
Poolfor web servers and a thin shareddb.jsmodule - Always use parameterized queries (
$1,$2) for any external input - Fail fast: run a
SELECT NOW()at startup so misconfiguration surfaces immediately
❌ Don't
- Hardcode passwords or commit a real
.env - Build SQL by concatenating user input into the string
- Ship your app running as the
postgressuperuser - Open a brand-new
Clientfor every request
✅ Production connections use SSL
Managed hosts require encrypted connections. When you deploy, add ssl: { rejectUnauthorized: false } (or a proper CA) to the pool config, or append ?sslmode=require to a connectionString. Locally over localhost you don't need it.
Summary
🎉 Key Takeaways
- PostgreSQL is an ACID-compliant, standards-based relational database that pairs naturally with async Node.js
- Install once (installer, Homebrew, apt, or Docker), then create a database and a least-privilege user with
psql - Keep credentials in environment variables, loaded via
dotenv, and git-ignore the.env - Connect with the
pgPoolfrom a single shareddb.jsmodule - Use parameterized queries everywhere to shut the door on SQL injection
📚 Additional Resources
- node-postgres — Connecting
- PostgreSQL docs — Creating a database
- PostgreSQL docs — CREATE ROLE / users & privileges
🚀 What's Next?
Your app can talk to Postgres — now let's make it fluent. The next lesson, Using the pg Library, dives into executing every kind of query (SELECT, INSERT, UPDATE, DELETE), reading the result object, handling transactions, and mapping PostgreSQL types to JavaScript.
🐘 Database online!
You installed PostgreSQL, created a secure database and user, and wired it into a Node app the right way. That foundation carries the rest of Week 8.