PostgreSQL with Node.js — pg and Drizzle ORM

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

PostgreSQL is the most powerful open-source database. Connecting from Node.js is straightforward with the pg driver or higher-level ORMs.

The pg Library

npm install pg
npm install --save-dev @types/pg
import { Pool } from "pg";

const pool = new Pool({
  user: "postgres",
  password: "password",
  host: "localhost",
  port: 5432,
  database: "mydb",
});

// Test connection
pool.query("SELECT NOW()", (err, result) => {
  if (err) {
    console.error("Connection error:", err);
  } else {
    console.log("Connected at:", result.rows[0]);
  }
});

Connection Pooling

const pool = new Pool({
  user: "postgres",
  password: process.env.DB_PASSWORD,
  host: process.env.DB_HOST || "localhost",
  port: 5432,
  database: process.env.DB_NAME,
  max: 20, // Maximum connections
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

// Get connection from pool
const query = async (text: string, params?: any[]) => {
  const start = Date.now();
  try {
    const result = await pool.query(text, params);
    const duration = Date.now() - start;
    console.log(`Executed query (${duration}ms): ${text}`);
    return result;
  } catch (error) {
    console.error("Database error:", error);
    throw error;
  }
};

export default query;

CRUD Operations

interface User {
  id: number;
  name: string;
  email: string;
  created_at: Date;
}

const getUserById = async (id: number): Promise<User | null> => {
  const result = await pool.query(
    "SELECT * FROM users WHERE id = $1",
    [id]
  );
  return result.rows[0] || null;
};

const createUser = async (name: string, email: string): Promise<User> => {
  const result = await pool.query(
    "INSERT INTO users (name, email, created_at) VALUES ($1, $2, NOW()) RETURNING *",
    [name, email]
  );
  return result.rows[0];
};

const updateUser = async (
  id: number,
  name: string,
  email: string
): Promise<User | null> => {
  const result = await pool.query(
    "UPDATE users SET name = $1, email = $2 WHERE id = $3 RETURNING *",
    [name, email, id]
  );
  return result.rows[0] || null;
};

const deleteUser = async (id: number): Promise<boolean> => {
  const result = await pool.query(
    "DELETE FROM users WHERE id = $1",
    [id]
  );
  return result.rowCount! > 0;
};

const getAllUsers = async (): Promise<User[]> => {
  const result = await pool.query("SELECT * FROM users");
  return result.rows;
};

Transactions

const transferMoney = async (
  fromUserId: number,
  toUserId: number,
  amount: number
) => {
  const client = await pool.connect();

  try {
    await client.query("BEGIN");

    // Deduct from sender
    await client.query(
      "UPDATE accounts SET balance = balance - $1 WHERE user_id = $2",
      [amount, fromUserId]
    );

    // Add to receiver
    await client.query(
      "UPDATE accounts SET balance = balance + $1 WHERE user_id = $2",
      [amount, toUserId]
    );

    // Record transaction
    await client.query(
      "INSERT INTO transactions (from_user_id, to_user_id, amount) VALUES ($1, $2, $3)",
      [fromUserId, toUserId, amount]
    );

    await client.query("COMMIT");
    console.log("Transfer complete");
  } catch (error) {
    await client.query("ROLLBACK");
    console.error("Transfer failed:", error);
    throw error;
  } finally {
    client.release();
  }
};

Drizzle ORM

Drizzle is a modern, lightweight ORM:

npm install drizzle-orm pg
npm install -D drizzle-kit
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { pgTable, serial, varchar, text, timestamp } from "drizzle-orm/pg-core";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

export const db = drizzle(pool);

// Define schema
export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: varchar("name", { length: 256 }).notNull(),
  email: varchar("email", { length: 256 }).unique().notNull(),
  createdAt: timestamp("created_at").defaultNow(),
});

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: varchar("title", { length: 256 }).notNull(),
  content: text("content"),
  userId: serial("user_id").references(() => users.id),
  createdAt: timestamp("created_at").defaultNow(),
});

Drizzle Queries

import { db, users, posts } from "./schema";
import { eq, like } from "drizzle-orm";

// Select all
const allUsers = await db.select().from(users);

// Select with where
const user = await db
  .select()
  .from(users)
  .where(eq(users.id, 1));

// Insert
const newUser = await db.insert(users).values({
  name: "Alice",
  email: "alice@example.com",
}).returning();

// Update
await db
  .update(users)
  .set({ name: "Bob" })
  .where(eq(users.id, 1));

// Delete
await db.delete(users).where(eq(users.id, 1));

// Join
const userWithPosts = await db
  .select()
  .from(users)
  .leftJoin(posts, eq(users.id, posts.userId))
  .where(eq(users.id, 1));

// Aggregate
const count = await db
  .select({ count: sql`count(*)` })
  .from(users);

Migrations with Drizzle

# Generate migration
drizzle-kit generate:pg

# Apply migration
drizzle-kit migrate:pg
// drizzle.config.ts
import type { Config } from "drizzle-kit";

export default {
  schema: "./src/schema.ts",
  out: "./drizzle",
  driver: "pg",
  dbCredentials: {
    connectionString: process.env.DATABASE_URL!,
  },
} satisfies Config;

Connection Best Practices

  1. Use connection pooling - reuse connections
  2. Set query timeouts - prevent hanging queries
  3. Use parameterized queries - prevent SQL injection
  4. Implement retry logic - for transient failures
  5. Monitor connection usage - watch pool metrics
const pool = new Pool({
  max: 20,
  idleTimeoutMillis: 30000,
  statement_timeout: 30000, // 30 second query timeout
});

// Monitor pool
pool.on("error", (err) => {
  console.error("Unexpected error on idle client", err);
});

FAQ

Q: Should I use pg or an ORM? A: For simple queries, pg is fine. For complex applications, an ORM like Drizzle saves time and prevents errors.

Q: What's the performance difference? A: Minimal. ORMs add overhead but it's negligible compared to network latency.

Q: How many connections do I need in the pool? A: Typically 5-20 depending on concurrency. Start with 10 and monitor.


PostgreSQL with Node.js is rock-solid. Whether you use the raw pg driver or an ORM like Drizzle, you have a foundation for building scalable applications.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro