node-postgres (pg)
Patrones de producción para node-postgres (pg): pooling, consultas seguras y transacciones en TypeScript en Node 24.
Busca en todas las páginas de la documentación
pg)Patrones de producción para node-postgres (pg): pooling, consultas seguras y transacciones en TypeScript en Node 24.
Tarjeta de referencia rápida - lista para copiar y pegar.
import pg from "pg";
export const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
connectionTimeoutMillis: 5_000,
idleTimeoutMillis: 30_000,
});
export async function findOrder(id: string) {
const { rows } = await pool.query(
"SELECT id, total_cents FROM orders WHERE id = $1",
[id]
);
return rows[0] ?? null;
}Cuándo usarlo:
// src/db/pool.ts
import pg from "pg";
export const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: Number(process.env.PG_POOL_MAX ?? 10),
ssl: process.env.NODE_ENV === "production" ? { rejectUnauthorized: true } : undefined,
});
// src/db/orders.ts
import type { Pool, PoolClient } from "pg";
import { pool } from "./pool";
export type Order = { id: string; userId: string; totalCents: number };
export async function createOrderWithItems(
userId: string,
items: { sku: string; qty: number; priceCents: number }[]
): Promise<Order> {
const client = await pool.connect();
try {
await client.query("BEGIN");
const orderRes = await client.query<{ id: string }>(
"INSERT INTO orders (user_id, total_cents) VALUES ($1, $2) RETURNING id",
[userId, items.reduce((s, i) => s + i.qty * i.priceCents, 0)]
);
const orderId = orderRes.rows[0].id;
for (const item of items) {
await client.query(
"INSERT INTO order_items (order_id, sku, qty, price_cents) VALUES ($1, $2, $3, $4)",
[orderId, item.sku, item.qty, item.priceCents]
);
}
await client.query("COMMIT");
return { id: orderId, userId, totalCents: items.reduce((s, i) => s + i.qty * i.priceCents, 0) };
} catch (err) {
await client.query("ROLLBACK");
throw err;
} finally {
client.release();
}
}
// src/routes/orders.ts (Express 5)
import express from "express";
import { createOrderWithItems } from "../db/orders";
const router = express.Router();
router.post("/", async (req, res, next) => {
try {
const { userId, items } = req.body;
const order = await createOrderWithItems(userId, items);
res.status(201).json(order);
} catch (err) {
next(err);
}
});
export default router;Lo que esto demuestra:
BEGIN/COMMIT/ROLLBACK explícitosdb/ o adaptadores de repositorio| API | Usar cuándo |
|---|---|
pool.query(sql, params) | SELECT/INSERT/UPDATE de una sola vez |
pool.connect() + client.query | Transacción de múltiples consultas en la misma conexión |
pool.end() | Apagado del proceso |
pool.query extrae un cliente internamente y lo libera// Seguro
await pool.query("SELECT * FROM users WHERE email = $1", [email]);
// Inseguro - nunca hagas esto
await pool.query(`SELECT * FROM users WHERE email = '${email}'`);pg envía los parámetros por separado del texto SQLIN dinámicas, usa = ANY($1::uuid[]) con un parámetro de arrayconst res = await pool.query<{ id: string; email: string }>(
"SELECT id, email FROM users WHERE id = $1",
[id]
);query<T>() tipa solo las rows; valida en los límites con Zod cuando sea necesariopg soporta sentencias preparadas sin nombre por consulta automáticamentepool.on("error", (err) => {
console.error({ msg: "error de cliente inactivo", err: err.message });
});pool.waitingCount se mantiene alto (agotamiento del pool)max_connections de Postgres. Solución: pool singleton a nivel de módulo.client.release() olvidado - inanición del pool. Solución: try/finally alrededor de cada connect().ssl: { rejectUnauthorized: true } con CA si es necesario.| Alternativa | Usar cuándo | No usar cuándo |
|---|---|---|
| Prisma | Migraciones de esquema y prioridad DX | SQL personalizado pesado en cada endpoint |
| Drizzle | SQL-first con inferencia de TypeScript | El equipo no quiere visibilidad de SQL |
| Knex | Código base heredado ya en Knex | Greenfield con opciones Drizzle/Prisma |
@vercel/postgres / Neon serverless driver | Edge/serverless sin pooler | Worker de larga duración con pool local |
Comienza con 10 por proceso de Node. Ajusta según max_connections de Postgres y el recuento de réplicas. Consulta Ajuste del pool de conexiones.
Sí. import pg from "pg" con "type": "module" en package.json.
Usa dbmate, flyway, goose, o Drizzle/Prisma migrate. Nunca apliques DDL ad hoc en shells de producción.
Proporciona Pool como un proveedor personalizado o usa adaptadores de TypeORM/Prisma que usan pg internamente.
Postgres devuelve bigint como string en JS. Usa el cast ::text o mapea a string en los tipos de TypeScript.
Client dedicado de larga duración, no el pool. Raro en APIs sin estado; prefiere colas para eventos.
Testcontainers Postgres para pruebas de integración; simula Pool con jest.mock solo para casos triviales.
Pools separados para URLs de lectura y escritura. Enruta SELECTs al pool de réplicas en los métodos del repositorio explícitamente.
Prefiere la opción ssl explícita en el código sobre ?sslmode=require solo cuando la verificación de CA es importante.
Usa INSERT ... VALUES ($1,$2), ($3,$4) de varias filas o COPY para cargas masivas.
pool.end()Versiones de la pila: Esta página fue escrita para Node.js 24.18.0 (LTS activa), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 y NestJS 11.
Revisado por Chris St. John·Última actualización: 16 jul 2026