pg review
pg 8.23.0, the main node-postgres package, is a server-side PostgreSQL driver for Node. It opens clients and pools, sends parameterized SQL, exposes transactions and prepared statements, decodes result rows, and supports LISTEN/NOTIFY. Schema definitions and migrations remain your responsibility or belong to another tool. Version 8.23 adds opt-in query pipelining: independent statements can be sent on one client before earlier responses return, while each query keeps its own result or error. The connection still processes those statements in order, so pipelining is different from parallel work on several pooled connections.
pg 8.23.0 installed in 1.3 seconds and used 1 MB in our sandbox, so the runtime cost is small for Node teams that want raw PostgreSQL control. Skip it when you need bundled TypeScript types, browser execution, generated schemas, or an abstraction that owns migrations and identifier composition.
We installed it
| Install | ✓ · 1.3s | 14 packages on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package with exports map |
| Browser | n/a | could not be bundled for the browser (Node-only code, most likely) |
| Types | — | no TypeScript types found |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does pg install cleanly?
Yes. In a fresh container with an empty cache, npm install pg finished in 1 seconds, leaving 14 packages and 1 MB on disk. npm audit reported no known vulnerabilities.
Can pg run in a browser?
Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.
Does pg work with both ESM and CommonJS?
Yes. Both import 'pg' and require('pg') worked in Node 22 in our run. The package is published as CommonJS with an exports map.
Does pg include TypeScript types?
No type declarations were found in our install, so TypeScript users need their own declarations.
pg or postgres: which should you use?
Pick postgres when postgres 3.x fits when tagged-template SQL and built-in TypeScript declarations are preferred over pg's query config API. pg 8.23.0 installed in 1.3 seconds and used 1 MB in our sandbox, so the runtime cost is small for Node teams that want raw PostgreSQL control.
When should you not use pg?
A TypeScript project requires declarations from the runtime package itself. Our package check found no TypeScript types in pg 8.23.0.
Use it if
- pg 8.23.0 fits a Node service that wants direct PostgreSQL SQL and a small built-in connection pool.
- Queries need positional parameters, prepared statement names, transaction control, or LISTEN/NOTIFY without adopting an ORM.
- The team is prepared to own migration tooling, pool limits, transaction boundaries, identifier safety, and PostgreSQL-specific SQL.
- Independent statements on one connection may benefit from 8.23's explicit pipeline mode and do not depend on earlier results.
- A TypeScript project requires declarations from the runtime package itself. Our package check found no TypeScript types in pg 8.23.0.
- Developers expect generated row types, schema migrations, relation helpers, and composable query construction. pg deliberately stays close to SQL and the PostgreSQL protocol.
- The target is browser code. Our esbuild browser bundle failed, and database credentials plus a direct PostgreSQL connection belong in a trusted server.
- bigint and numeric columns must become JavaScript numbers automatically. pg returns values that could lose precision as strings unless you install a deliberate parser.
- One transaction needs concurrent independent execution. A PostgreSQL client handles one ordered protocol stream, and pipeline mode does not create several database sessions.
- SQL identifiers come from user input. Parameters protect values, not table or column names; dynamic identifiers require allowlists or a separate escaping utility.
Setup reality
We installed pg 8.23.0 in a fresh Node 22 Bookworm sandbox. npm took 1.3 seconds, left 14 packages, and used 1 MB. npm audit found 0 known vulnerabilities. The package has 5 direct dependencies, 1 peer dependency, and 164 KB unpacked. It declares Node >=16.0.0 and uses CommonJS with an exports map. Both require() and ESM import worked under Node 22.23.2. No TypeScript declarations were found.
Our esbuild browser bundle failed, which matches a database driver meant for trusted Node processes. Configure PGHOST, PGPORT, PGUSER, PGPASSWORD, and PGDATABASE or pass a connectionString. SSL is not inferred safely for every provider. Use the provider's certificate requirements, and be careful that sslmode parameters in a connection string can replace a separately supplied ssl object. Do not ship credentials to frontend code.
Pool.query is correct for one statement. A transaction must check out one client, issue BEGIN, all statements, and COMMIT on that same client, then release it in finally. Pool size should fit the database connection budget across every process and replica. Add a pool error handler because an idle client can still receive a network error. End the pool in scripts and graceful shutdown paths.
Parameters bind values only. Never interpolate untrusted identifiers into SQL. PostgreSQL int8 and numeric values return as strings by default to avoid silent precision loss; choose a column-specific conversion or a global type parser only when the value range is known. Version 8.23 pipeline mode is opt-in with pipeline: true. It reduces round trips for independent ordered queries, but a query cannot use a result that has not arrived.
Patterns
Parameterize a pooled query query-pool
import pg from 'pg';
const { Pool } = pg;
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const result = await pool.query('SELECT id, email FROM users WHERE id = $1', [userId]);$1 binds a value and blocks value-based SQL injection. It cannot stand for a table or column identifier.
Keep a transaction on one client run-transaction
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, from]);
await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, to]);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}Do not use pool.query inside this block. Each call could select a different database session.
Catch errors from idle pooled clients handle-pool-errors
pool.on('error', (error) => {
console.error('unexpected PostgreSQL client error', error);
});An idle checked-in connection can fail after a network break. Without this listener, the pool emits an unhandled error event.
Load the provider CA certificate configure-tls
import fs from 'node:fs';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { ca: fs.readFileSync(process.env.PG_CA_FILE, 'utf8') },
});Use the certificate and verification mode required by the provider. Connection-string SSL parameters can replace this ssl object.
Convert int8 only with a safe range parse-bigint
import pg from 'pg';
pg.types.setTypeParser(20, (value) => {
const parsed = BigInt(value);
if (parsed > BigInt(Number.MAX_SAFE_INTEGER)) return parsed;
return Number(parsed);
});OID 20 is int8. This global parser changes every matching result, so returning mixed number and bigint types needs caller agreement.
Name a frequently reused statement prepare-query
await client.query({
name: 'user-by-id',
text: 'SELECT id, email FROM users WHERE id = $1',
values: [userId],
});Prepared statement names are scoped to a connection. Reuse one name only with the same SQL text.
Receive PostgreSQL notifications listen-notify
const client = await pool.connect();
client.on('notification', (message) => {
console.log(message.channel, message.payload);
});
await client.query('LISTEN job_events');Keep this client checked out while listening. Reconnect and LISTEN again after connection loss.
Close a one-off process cleanly close-pool
try {
await runJob(pool);
} finally {
await pool.end();
}pool.end() rejects new work and closes idle clients after checked-out clients return. Servers usually call it during graceful shutdown.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| postgres | npm | postgres 3.x fits when tagged-template SQL and built-in TypeScript declarations are preferred over pg's query config API. |
| mysql2 | npm | Use mysql2 when the database is MySQL or MariaDB; it is not a PostgreSQL substitute on the same server. |
| better-sqlite3 | npm | Use better-sqlite3 for an embedded SQLite database where a network pool and PostgreSQL server are unnecessary. |
More data guides
numpy · fsspec · pandas · pyarrow · sqlalchemy · s3fs · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

