iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

PostgreSQL (pg, drizzle)

`pg` (node-postgres) is the most-used Postgres driver in Node. Pair with a connection pool, parameterised queries, and a thin query layer (or Prisma / Drizzle / Kysely for typed queries). The shape that scales: pool at module scope, parameterised SQL everywhere, transactions wrapped in a helper.

Pool, parameterised SQL, transactions, type safety

EXAMPLE
// npm i pg
import { Pool, type PoolClient } from 'pg';

// 1) Pool — created once at module scope
export const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,                         // max connections (per process)
  idleTimeoutMillis: 30_000,
  connectionTimeoutMillis: 5_000,
  ssl: { rejectUnauthorized: true },   // production: verify the cert
});

pool.on('error', (err) => {
  console.error('pg pool error', err);
});

// 2) Parameterised queries — $1, $2, ... placeholders
export async function findCustomer(email: string) {
  const res = await pool.query<{ id: number; name: string }>(
    'SELECT id, name FROM customers WHERE email = $1',
    [email],
  );
  return res.rows[0] ?? null;
}

export async function listOrders(customerId: number, limit = 20) {
  const res = await pool.query(
    'SELECT * FROM orders WHERE customer_id = $1 ORDER BY created_at DESC LIMIT $2',
    [customerId, limit],
  );
  return res.rows;
}

// 3) Insert + RETURNING
export async function createOrder(customerId: number, totalCents: number) {
  const res = await pool.query<{ id: number }>(
    \`INSERT INTO orders (customer_id, total_cents, status)
     VALUES ($1, $2, 'new')
     RETURNING id\`,
    [customerId, totalCents],
  );
  return res.rows[0].id;
}

// 4) Transactions — wrap in a helper
export async function withTransaction<T>(fn: (c: PoolClient) => Promise<T>): Promise<T> {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    const result = await fn(client);
    await client.query('COMMIT');
    return result;
  } catch (e) {
    await client.query('ROLLBACK');
    throw e;
  } finally {
    client.release();
  }
}

export async function placeOrder(input: { email: string; name: string; totalCents: number }) {
  return withTransaction(async (c) => {
    const u = await c.query<{ id: number }>(
      \`INSERT INTO customers (email, name) VALUES ($1, $2)
       ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name
       RETURNING id\`,
      [input.email, input.name],
    );
    const o = await c.query<{ id: number }>(
      'INSERT INTO orders (customer_id, total_cents, status) VALUES ($1, $2, $3) RETURNING id',
      [u.rows[0].id, input.totalCents, 'new'],
    );
    return o.rows[0].id;
  });
}

// 5) Streaming large result sets
import QueryStream from 'pg-query-stream';
export async function streamOrders(onRow: (row: any) => void) {
  const client = await pool.connect();
  try {
    const stream = client.query(new QueryStream('SELECT * FROM orders ORDER BY id', [], { batchSize: 1000 }));
    await new Promise<void>((resolve, reject) => {
      stream.on('data', onRow);
      stream.on('end', resolve);
      stream.on('error', reject);
    });
  } finally {
    client.release();
  }
}

// 6) Type-safe queries with Drizzle / Kysely (recommended for non-trivial apps)
// npm i kysely pg
// import { Kysely, PostgresDialect } from 'kysely';
// interface Database { customers: { id: number; email: string; name: string }; orders: { ... } }
// const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool }) });
// const rows = await db.selectFrom('orders').select(['id','customer_id']).where('status', '=', 'new').execute();

// 7) Migrations — use a tool (node-pg-migrate, knex, drizzle-kit), not ad-hoc CREATE TABLE
// node-pg-migrate up
// drizzle-kit push

// 8) LISTEN / NOTIFY for cache invalidation
const subClient = await pool.connect();
await subClient.query('LISTEN orders_changed');
subClient.on('notification', (msg) => {
  console.log('cache invalidate', msg.payload);
});

// 9) Health check
export async function checkDb(): Promise<boolean> {
  try { await pool.query('SELECT 1'); return true; }
  catch { return false; }
}

// 10) Pitfalls
// - new Pool() inside a request handler -> connection storm
// - Forgetting client.release() -> pool starvation
// - String concat in SQL (NEVER)
// - SELECT * on huge tables in transactions
// - Returning a connection from a function -> leak (always release in finally)

Why it matters

Pool at module scope, parameterised SQL everywhere, transactions in a helper, migrations via a real tool. Those four habits cover 90% of "why is the database slow / leaking / vulnerable?" reports — and they cost less than half a day of refactoring on most existing Node + Postgres apps.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
import pg from 'pg';
const client = new pg.Client();
await client.connect();
const r = await client.query('SELECT NOW()');
Try it Yourself »

Discussion

Loading…