Databases with Node.js
Almost every real application needs to persist data — survive restarts, share state across instances, query and relate records. So far this tutorial used in-memory arrays; now we connect to real databases. Node's async, non-blocking model is a natural fit: database calls are I/O, so they await without blocking the event loop. This page maps the landscape — database families, the driver/ORM spectrum, and the connection and security fundamentals that apply no matter which you choose.
The database families
Family | Examples | Shape |
|---|---|---|
Relational (SQL) | PostgreSQL, MySQL, SQLite | Tables, rows, schemas, joins |
Document (NoSQL) | MongoDB, CouchDB | Flexible JSON-like documents |
Key–value | Redis, DynamoDB | Fast lookups by key, often in-memory |
Graph | Neo4j | Nodes & relationships |
Search | Elasticsearch | Full-text, analytics |
The access-layer spectrum: driver → query builder → ORM
Layer | You write | Examples |
|---|---|---|
Raw driver | SQL / commands by hand |
|
Query builder | Chained JS that builds SQL | Knex |
ORM / ODM | Objects & models; it generates queries | Prisma, Sequelize, TypeORM, Mongoose |
Everything is async
// Database calls are I/O — always await them (or .then):
app.get('/users/:id', async (req, res, next) => {
try {
const user = await db.users.findById(req.params.id) // non-blocking
if (!user) return res.status(404).json({ error: 'Not found' })
res.json(user)
} catch (err) {
next(err) // forward DB errors to the error handler
}
})Connections aren't free — use a pool
// DON'T open a new connection per request:
app.get('/x', async (req, res) => {
const conn = await createConnection() // 🚩 slow, leaks, exhausts the DB
// ...
})
// DO create ONE pool at startup and reuse it everywhere:
import { Pool } from 'pg'
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 })
// each query borrows a connection from the pool and returns itNever build queries by string concatenation
// SQL INJECTION — never interpolate user input into a query string:
const bad = `SELECT * FROM users WHERE email = '${req.body.email}'` // 🚩
// SAFE — parameterized query; the driver escapes the value:
const good = await pool.query(
'SELECT * FROM users WHERE email = $1',
[req.body.email],
)Keep credentials in the environment
// Read the connection string from the environment, never hard-code it:
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
// .env (git-ignored): DATABASE_URL=postgres://user:pass@host:5432/dbWhat the rest of the section covers
SQL vs NoSQL — choosing a data model.
SQL: PostgreSQL, MySQL, and ORMs (Sequelize, Prisma, TypeORM).