Back to Blog
Tutorial10 min read2026-06-26

How to Connect Node.js to a Managed PostgreSQL Database

Connecting Node.js to managed Postgres correctly means pooling, SSL, and a clean migration path. Here's a production-ready setup using pg, with notes for Prisma and Drizzle.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Getting Postgres + Node right the first time

Connecting Node to Postgres looks like a one-liner, then production teaches you about connection pools, SSL, connection limits, and migrations. This tutorial gets you to a setup that survives real traffic, using the standard pg driver and notes for Prisma and Drizzle.

The assumption: you have a managed Postgres and a connection string in DATABASE_URL. On PandaStack, provisioning a database auto-injects DATABASE_URL into your app's environment, so you skip the copy-paste step.

The connection string

A Postgres URL looks like:

postgresql://USER:PASSWORD@HOST:5432/DBNAME?sslmode=require

Never hardcode this. Read it from the environment so the same code runs in dev, staging, and prod with different values.

Use a pool, not a client per request

The single biggest mistake is opening a new connection on every request. Postgres connections are expensive and managed databases cap them (PandaStack free tier allows 50 connections; Pro 300). Use a pool:

// db.js
import pg from "pg";

export const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,                       // keep below your DB connection limit
  idleTimeoutMillis: 30_000,
  connectionTimeoutMillis: 5_000,
  ssl: process.env.DATABASE_URL?.includes("localhost")
    ? false
    : { rejectUnauthorized: false },
});

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

Note the max setting. If you run 4 app instances each with max: 10, that's 40 connections — keep the total under your database's limit. Size the pool to limit / instances with headroom.

Querying safely

Always use parameterized queries. Never string-concatenate user input — that's SQL injection.

import { pool } from "./db.js";

export async function getUserByEmail(email) {
  // $1 is a bound parameter — safe
  const { rows } = await pool.query(
    "select id, email, name from users where email = $1",
    [email]
  );
  return rows[0] ?? null;
}

For multi-statement transactions, check out a single client so all statements run on the same connection:

export async function transferCredits(fromId, toId, amount) {
  const client = await pool.connect();
  try {
    await client.query("begin");
    await client.query("update accounts set credits = credits - $1 where id = $2", [amount, fromId]);
    await client.query("update accounts set credits = credits + $1 where id = $2", [amount, toId]);
    await client.query("commit");
  } catch (e) {
    await client.query("rollback");
    throw e;
  } finally {
    client.release(); // always release back to the pool
  }
}

The finally { client.release() } is non-negotiable — a leaked client exhausts the pool and your app hangs.

SSL notes

Managed Postgres almost always requires TLS. With pg, ssl: { rejectUnauthorized: false } works against providers that use their own CA without you shipping the CA cert. If your provider gives you a CA certificate and you want strict verification:

import fs from "node:fs";
ssl: {
  rejectUnauthorized: true,
  ca: fs.readFileSync(process.env.PG_CA_CERT_PATH).toString(),
}

Start permissive to confirm connectivity, then tighten verification once it works.

With Prisma

Prisma reads DATABASE_URL from the env automatically:

// schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
npx prisma migrate deploy   # apply migrations in production (not 'migrate dev')
npx prisma generate

Instantiate the client once (singleton) — a new PrismaClient per request will exhaust connections fast:

import { PrismaClient } from "@prisma/client";
export const prisma = globalThis.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") globalThis.prisma = prisma;

With Drizzle

import { drizzle } from "drizzle-orm/node-postgres";
import pg from "pg";

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10 });
export const db = drizzle(pool);

Drizzle is a thin layer over the same pg pool, so the pooling advice above applies directly.

Migrations as a release step

Don't run migrations in your app's start command if you scale to multiple instances — they'll race. Run them as a one-off:

# build step or release command, run once per deploy
npx prisma migrate deploy
# or with raw SQL / node-pg-migrate
npm run migrate

Deploying on PandaStack

  1. 1Create a managed PostgreSQL (14.x or 16.x) in the dashboard.
  2. 2Attach it to your Node service — DATABASE_URL is injected automatically.
  3. 3Set your migration command as a release/build step.
  4. 4Deploy; watch live logs for connection success.
  5. 5Confirm pool size stays under the tier's connection limit.

Production checklist

  • Pool sized to connection_limit / instances with headroom.
  • Parameterized queries everywhere.
  • client.release() in every finally.
  • SSL enabled (sslmode=require).
  • Migrations run once per deploy, not on every instance start.
  • Backups configured (PandaStack does scheduled backups; verify retention fits your needs).

References

  • node-postgres (pg) docs: https://node-postgres.com/
  • node-postgres pooling: https://node-postgres.com/features/pooling
  • Prisma deployment / migrate deploy: https://www.prisma.io/docs/orm/prisma-migrate/workflows/development-and-production
  • Drizzle node-postgres: https://orm.drizzle.team/docs/get-started-postgresql#node-postgres
  • PostgreSQL connection strings: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING

---

Want a managed Postgres with the connection string auto-wired into your Node app? PandaStack injects DATABASE_URL for you. Provision one free at https://dashboard.pandastack.io

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also