mrkeyoor.com_
Wed 05 Aug 05:01 UTC
npmDataupdated 05 Aug 2026

pg

pg (node-postgres) is the standard low-level PostgreSQL client for Node.js: a non-blocking driver that speaks the Postgres wire protocol in pure JavaScript, with optional native libpq bindings behind the same API. It gives you parameterized queries, connection pooling via the bundled pg-pool, LISTEN/NOTIFY, COPY support, and extensible type coercion between JS and Postgres. It is deliberately light on abstractions: you write SQL strings, it moves rows. Nearly every higher-level Postgres tool in the Node ecosystem, from Knex to many ORM drivers, sits on top of it, and the docs also work on Bun, Deno, and Cloudflare (via pg-cloudflare).

Verdict

The boring, correct choice for talking to Postgres from Node when you are comfortable writing SQL and reading the pooling docs. If you want types, migrations, or query building, put Drizzle or Knex on top rather than replacing it.

API stability5/5Major version 8 has been current since 2020 and the maintainer explicitly gates PRs on backwards compatibility; the query/pool API has barely changed in years.
Docs4/5node-postgres.com covers pooling, transactions, and types clearly and the wiki has an FAQ, but critical gotchas like the idle-client error event and type-parser defaults still surprise people because they live off the main path.
Maintenance4/5Actively pushed (Aug 2026) with steady releases (8.22.0), but it is substantially a one-lead sponsored project with 521 open issues and PRs, not a large team.
Ecosystem5/5The default Postgres driver of the Node world at 44M weekly downloads; pg-pool, pg-cursor, pg-query-stream, and pg-connection-string ship from the same monorepo, and most ORMs and builders document pg first.

Use it if

  • You want to write actual SQL against Postgres with parameterized queries and no ORM layer in the way
  • You need Postgres-specific features like LISTEN/NOTIFY for pub/sub or COPY FROM/TO for bulk loads, which most ORMs hide or skip
  • You are building a library or tool on Postgres; pg is the ecosystem's common denominator and what most tooling already depends on
  • You need connection pooling that you control directly, including checkout, transactions on a dedicated client, and pool sizing
Skip it if

Setup reality

npm install pg and it just works: pure JavaScript, no native compilation, Node 16+. Configuration is via standard PG* environment variables, a connection string, or an options object. The annoying parts are operational, not install-time: you must attach a pool.on('error') handler or a dropped idle connection can take down the process; transactions require checking out a client and releasing it in a finally block, and a missed release() quietly starves the pool; SSL against managed Postgres (RDS, Supabase, Neon) usually needs an ssl option that the connection string alone does not fully express; and streaming or cursors mean installing the sibling packages pg-query-stream or pg-cursor from the same monorepo.

Patterns

Query through a shared poolpool-query

import pg from 'pg'
const { Pool } = pg

const pool = new Pool() // reads PGHOST, PGUSER, PGPASSWORD, PGDATABASE

const { rows } = await pool.query('SELECT NOW()')
console.log(rows[0])

Create one Pool per process and share it; a new Pool per request destroys the point of pooling and exhausts Postgres connections.

Parameterized query (no SQL injection)parameterized-query

const { rows } = await pool.query(
  'SELECT * FROM users WHERE email = $1 AND active = $2',
  [email, true]
)

Placeholders are $1, $2 (not ?), and parameters only work for values, never for table or column names.

Transaction on a dedicated clienttransaction

const client = await pool.connect()
try {
  await client.query('BEGIN')
  await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [100, from])
  await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [100, to])
  await client.query('COMMIT')
} catch (e) {
  await client.query('ROLLBACK')
  throw e
} finally {
  client.release()
}

pool.query cannot do transactions because each call may use a different connection; forget release() and the pool eventually starves.

Handle errors on idle clientspool-error-handling

pool.on('error', (err) => {
  console.error('idle client error', err)
})

Without this handler, a backend disconnect on an idle pooled connection emits an unhandled 'error' event and crashes Node. Non-negotiable in production.

Connection string and SSL configconnection-config

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: { rejectUnauthorized: false }, // many managed PG hosts need this or a CA cert
  max: 10,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 5000
})

rejectUnauthorized: false accepts any cert; prefer ssl: { ca } with your provider's CA bundle when you can.

Pub/sub with LISTEN/NOTIFYlisten-notify

const client = new pg.Client()
await client.connect()

client.on('notification', (msg) => {
  console.log(msg.channel, msg.payload)
})
await client.query('LISTEN job_updates')

// elsewhere: await pool.query("NOTIFY job_updates, 'done'")

Use a dedicated long-lived Client, not the pool; a pooled connection can be recycled and silently stop listening.

Get bigint and numeric back as numberstype-parsing

import pg from 'pg'

// int8 (OID 20) and numeric (OID 1700) arrive as strings by default
pg.types.setTypeParser(20, (val) => parseInt(val, 10))
pg.types.setTypeParser(1700, (val) => parseFloat(val))

The string default exists because JS numbers lose precision past 2^53; only override if your values are safely in range.

Insert many rows in one statementbulk-insert

const values = users.flatMap(u => [u.name, u.email])
const placeholders = users
  .map((_, i) => `($${i * 2 + 1}, $${i * 2 + 2})`)
  .join(', ')
await pool.query(
  `INSERT INTO users (name, email) VALUES ${placeholders}`,
  values
)

pg has no built-in bulk helper; for very large loads use COPY FROM via the pg-copy-streams package instead of giant VALUES lists.

Named prepared statement with plan cachingprepared-statement

const result = await pool.query({
  name: 'fetch-user',
  text: 'SELECT * FROM users WHERE id = $1',
  values: [id]
})

The name caches the parsed plan per connection; keep names unique per SQL text or you get a prepared-statement conflict error.

Stream large result setsstreaming-rows

import QueryStream from 'pg-query-stream'

const client = await pool.connect()
const stream = client.query(new QueryStream('SELECT * FROM big_table'))
stream.on('end', () => client.release())
for await (const row of stream) {
  process(row)
}

pg-query-stream is a separate install from the same monorepo; without it a huge SELECT buffers every row in memory.

One-off script without a poolsingle-client-script

import pg from 'pg'
const client = new pg.Client({ connectionString: process.env.DATABASE_URL })
await client.connect()
const { rows } = await client.query('SELECT COUNT(*) FROM users')
await client.end()

Forgetting client.end() leaves the process hanging at exit; for anything serving traffic, use a Pool instead.

Alternatives

PackageRegistryPick it when
postgresnpmYou want a faster, dependency-free client with tagged-template SQL instead of $1 placeholders
drizzle-ormnpmYou want typed schema, migrations, and SQL-shaped queries while keeping pg underneath
knexnpmYou want a query builder and migrations without a full ORM
pg-promisenpmYou want raw SQL but with promise-centric helpers, named params, and multi-row insert generation