NodeJSDatabases with Node.js

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

Most apps start with one relational or one document database
You rarely need exotic stores early. The two mainstream choices are a **relational** database (PostgreSQL is the common default) or a **document** database (MongoDB). [SQL vs NoSQL](/nodejs/sql-vs-nosql) covers the trade-off; the short version is that relational databases fit structured, related data with strong guarantees, while document stores fit flexible, denormalized data. Redis usually joins later as a [cache](/nodejs/redis), not the primary store.
The access-layer spectrum: driver → query builder → ORM

Layer

You write

Examples

Raw driver

SQL / commands by hand

pg, mysql2, mongodb

Query builder

Chained JS that builds SQL

Knex

ORM / ODM

Objects & models; it generates queries

Prisma, Sequelize, TypeORM, Mongoose

Higher abstraction trades control for convenience
A **driver** gives you full control and minimal magic but you write every query. An **ORM** (Object-Relational Mapper) / **ODM** (Object-Document Mapper) lets you work with model objects and generates queries for you — faster to build, but with overhead and occasional awkward queries. There's no universally right level: learn the driver to understand what's happening, then choose an ORM when its productivity pays off. Later pages cover both.
Everything is async

JS
// 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
  }
})
DB calls return promises — `await` and handle rejection
Every modern Node database client returns promises, so queries are `await`ed inside `async` handlers and never block the [event loop](/nodejs/event-loop) — the server keeps serving other requests while the database works. Always handle rejections: `try/catch` with `next(err)`, or rely on [Express 5's](/nodejs/error-middleware) auto-forwarding. An unhandled DB rejection can crash the process.
Connections aren't free — use a pool

JS
// 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 it
Open the connection/pool ONCE at startup — never per request
Establishing a database connection involves a TCP handshake and authentication — expensive to do per request, and doing so quickly exhausts the database's connection limit and leaks sockets. Create a **single connection pool** when the app boots and reuse it for every query; the pool hands out and recycles a small set of connections. This is so important it gets its [own page](/nodejs/connection-pooling).
Never build queries by string concatenation

JS
// 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],
)
Always use parameterized queries — injection is the #1 risk
Concatenating user input into a query lets an attacker inject `' OR 1=1 --` and read or destroy your entire database. **Parameterized queries** (placeholders like `$1`/`?` with a separate values array) send data apart from the command so it can never be interpreted as SQL. ORMs parameterize by default. NoSQL has its own [injection variant](/nodejs/mongodb-driver) too. Treat every value from a client as hostile — see [validation](/nodejs/validation-intro).
Keep credentials in the environment

JS
// 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/db
Never commit database credentials to source control
Connection strings contain passwords. Hard-coding them — or committing a `.env` with real values — leaks them to anyone with repo access and into git history forever. Load credentials from environment variables (git-ignore `.env`), and [validate them at startup](/nodejs/validation-intro) so a missing `DATABASE_URL` fails loudly. Rotate any secret that's ever been committed.
What the rest of the section covers
Next
Choose the right data model for your app: [SQL vs NoSQL](/nodejs/sql-vs-nosql).