Express remains the most common way to build a Node.js API, and pairing it with PostgreSQL is a default for good reason. The gap between a working local app and a robust production deployment is mostly about how you manage the database connection. This guide focuses on that, plus the deployment mechanics.
Pool, don't connect per request
The single most important production rule: use a connection pool. Opening a new PostgreSQL connection per request is slow and quickly exhausts the server's connection limit. With pg:
import pg from 'pg'
export const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 5_000,
ssl: process.env.PGSSL ? { rejectUnauthorized: false } : undefined
})Size max carefully. If you run 4 instances with max: 10, that's 40 connections. Stay under your database's limit (the PandaStack Free tier allows 50 connections; Pro allows 300).
A clean query layer
import { pool } from './db.js'
export async function getUser(id) {
const { rows } = await pool.query(
'SELECT id, email FROM users WHERE id = $1',
[id]
)
return rows[0]
}Always use parameterized queries ($1, $2) — never string interpolation — to prevent SQL injection.
Migrations
Keep schema changes in versioned migration files. A lightweight tool like node-pg-migrate works well:
npx node-pg-migrate upRun migrations as a separate deploy step before the new app version takes traffic. Make changes backward-compatible across one release (add nullable columns first, enforce constraints later) so rolling deploys don't break in-flight instances.
Health checks and graceful shutdown
app.get('/healthz', (req, res) => res.json({ status: 'ok' }))
app.get('/readyz', async (req, res) => {
try {
await pool.query('SELECT 1')
res.json({ ready: true })
} catch {
res.status(503).json({ ready: false })
}
})
const server = app.listen(process.env.PORT || 3000, '0.0.0.0')
process.on('SIGTERM', async () => {
server.close(async () => {
await pool.end()
process.exit(0)
})
})Closing the pool on SIGTERM lets in-flight queries finish and prevents "connection terminated unexpectedly" errors in your logs during deploys.
Dockerfile
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app .
USER node
EXPOSE 3000
CMD ["node", "index.js"]Deploying on PandaStack
This is where the managed database pays off:
- 1Provision a managed PostgreSQL instance (14.x or 16.x) from the [dashboard](https://dashboard.pandastack.io).
- 2Connect your Express repo as a container app. PandaStack auto-detects Node — you may not even need the Dockerfile above.
- 3Attaching the database injects
DATABASE_URLautomatically, so thepg.Poolconfig above works with zero changes. - 4Add a release step to run your migrations.
- 5Point the readiness probe at
/readyz.
Images build with rootless BuildKit in ephemeral Kubernetes Jobs and deploy via Helm. You get automatic SSL, live logs (self-hosted Elasticsearch), and server-side metrics without instrumenting your code.
Connection budgeting
| Plan | Max DB connections | Suggested pool max × instances |
|---|---|---|
| Free | 50 | e.g. 10 × 4 = 40 |
| Pro | 300 | e.g. 20 × 10 = 200 |
| Premium | 1000 | scale accordingly |
If you scale horizontally, lower per-instance max or add a pooler. Leave headroom for migrations and admin tools that also consume connections.
Backups
Managed PostgreSQL includes scheduled and manual backups, with 7-day retention on Free and longer on paid plans. Take a manual backup before running a risky migration so you have a clean restore point.
References
- [node-postgres pooling guide](https://node-postgres.com/features/pooling)
- [PostgreSQL connection settings](https://www.postgresql.org/docs/current/runtime-config-connection.html)
- [Express production best practices](https://expressjs.com/en/advanced/best-practice-performance.html)
- [OWASP SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)
Want your Express API and PostgreSQL auto-wired together? PandaStack's free tier includes a managed database with automatic DATABASE_URL injection — start at [dashboard.pandastack.io](https://dashboard.pandastack.io).