Express.js
Integración con PostgreSQL
Explica cómo conectar Express.js con PostgreSQL mediante pools, queries parametrizadas, repositorios, transacciones, timeouts y manejo seguro de errores.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Express.js
Explica cómo conectar Express.js con PostgreSQL mediante pools, queries parametrizadas, repositorios, transacciones, timeouts y manejo seguro de errores.
Express no administra PostgreSQL: coordina una request con un pool, consultas parametrizadas, transacciones y errores de persistencia sin filtrar detalles internos al cliente.
La integración correcta separa tres responsabilidades:
handler HTTP
→ traduce request y response
caso de uso
→ coordina reglas y transacción
repository o query module
→ ejecuta SQL y mapea datosLa base de datos protege integridad y concurrencia; Express expone un contrato HTTP sobre esas capacidades.
Crear una conexión por request es costoso. Un pool mantiene conexiones reutilizables:
import { Pool } from 'pg';
export const pool = new Pool({
connectionString: config.databaseUrl,
max: 10,
connectionTimeoutMillis: 2_000,
idleTimeoutMillis: 30_000,
});max debe calcularse considerando todas las instancias. Diez pods con pool 20 pueden abrir 200 conexiones.
const result = await pool.query(
`SELECT id, status, total
FROM orders
WHERE tenant_id = $1 AND id = $2`,
[tenantId, orderId],
);Los placeholders protegen valores. Identifiers dinámicos como columnas de sort requieren whitelist, no parámetros.
Un repository puede expresar operaciones del dominio:
interface OrderRepository {
findById(scope: OrderScope, id: string): Promise<Order | null>;
create(input: NewOrder): Promise<Order>;
}No necesita ocultar que existe SQL a cualquier coste. Para reporting o búsquedas complejas, un query module explícito puede ser más claro que forzar un repository genérico.
No devuelvas filas crudas al cliente:
function mapOrderRow(row: OrderRow): Order {
return {
id: row.id,
status: row.status,
total: Number(row.total),
createdAt: row.created_at,
};
}PostgreSQL puede devolver numeric como string para evitar pérdida de precisión. Define estrategia para dinero y tipos temporales.
Toda consulta debe incluir el tenant o branch derivado de identidad confiable:
WHERE tenant_id = $1 AND id = $2No cargues por ID global y autorices después si la consulta puede limitar desde el inicio. RLS puede añadir defensa adicional, pero requiere contexto seguro por conexión o transacción.
No reemplaces constraints con validación HTTP:
La validación mejora mensajes; la base protege contra carreras y otros escritores.
Mapea errores conocidos sin exponer SQL:
No conviertas cualquier error de DB en 400.
Configura límites en varias capas:
SET LOCAL statement_timeout = '2s';También existen timeout de adquisición del pool y deadline de request. El timeout interno debe dejar margen para mapear y responder antes de que el proxy cierre.
async function getOrderHandler(request, response) {
const order = await orders.findById(
{ tenantId: response.locals.actor.tenantId },
request.params.orderId,
);
if (!order) {
throw new OrderNotFoundError();
}
response.json({ data: toOrderResponse(order) });
}Flujo:
pool.query sirve para una consulta independiente. Una transacción necesita un client reservado:
const client = await pool.connect();
try {
await client.query('BEGIN');
// all transaction queries use client
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}Usar pool.query dentro de la transacción puede ejecutar en otra conexión.
Cargar una colección y luego hacer una query por elemento multiplica latencia:
1 query orders + 100 queries customersSoluciones: joins, consultas batch, loaders o una representación distinta. Mide antes de introducir abstracciones complejas.
El pool puede crearse sin conexión inmediata. Readiness debe comprobar si la aplicación puede cumplir su contrato, pero no hagas una query costosa en cada probe.
Al cerrar:
pool.end().No cierres el pool mientras handlers aún lo usan.
Mocks no detectan SQL inválido ni constraints.
SELECT * como contrato.pool.query en medio de una transacción de client?ORMs y query builders compara abstracciones de persistencia sin perder el modelo de PostgreSQL.