JS
#postgresql#node#pg#nodejs#database

PostgreSQL: Node.js (pg) — Pool, параметризованные запросы, JSONB

Подключение к PostgreSQL из Node.js через пакет pg: Pool, query с $1 $2, запросы по JSONB. Копируй — вставляй — работает. По документации node-postgres.

Как использовать

  1. Установи: npm i pg. Для TypeScript: npm i -D @types/pg.
  2. Создай Pool с host, database, user, password (лучше из process.env). Все запросы через pool.query(sql, [params]).
  3. Всегда используй плейсхолдеры $1, $2 — не подставляй значения в строку (защита от SQL-инъекций).

Подключение к PostgreSQL из Node.js делают через клиент node-postgres (pg). Для приложения нужен пул соединений (Pool) и параметризованные запросы. Проблема: подстановка значений в строку запроса ведёт к SQL-инъекциям; без пула каждое обращение создаёт новое соединение. Симптомы: уязвимость при пользовательском вводе, исчерпание соединений, ошибки «too many clients». Ниже — настройка Pool, запросы с $1, $2, работа с JSONB из Node по документации pg; проверка и типичные ошибки.

Решение

Подключение к PostgreSQL из Node.js через pg: Pool, параметризованные запросы, примеры по JSONB. По Pooling, Queries.

Установка и Pool

npm i pg
npm i -D @types/pg   # TypeScript
const { Pool } = require('pg');

const pool = new Pool({
  host: 'localhost',
  port: 5432,
  database: 'myapp',
  user: 'app_user',
  password: process.env.DB_PASSWORD,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

module.exports = { pool };

Параметризованные запросы

Параметры — вторым аргументом; в запросе плейсхолдеры $1, $2. Подстановка на стороне PostgreSQL — защита от инъекций.

const { pool } = require('./pool');

const res = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
const user = res.rows[0];

const insert = await pool.query(
  'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *',
  ['user@example.com', 'Alice']
);
const newUser = insert.rows[0];

await pool.query('UPDATE users SET name = $1 WHERE id = $2', ['Bob', userId]);

Запросы по JSONB

Оператор @> — передаём JSON как параметр:

const products = await pool.query(
  'SELECT * FROM products WHERE attrs @> $1',
  [JSON.stringify({ color: 'red' })]
);

const list = await pool.query(
  'SELECT * FROM products WHERE attrs @> $1',
  [JSON.stringify({ brand: 'Apple', color: 'black' })]
);

Проверка ключа и извлечение поля:

const byKey = await pool.query(
  "SELECT * FROM products WHERE attrs ? $1",
  ['brand']
);
const withBrand = await pool.query(
  "SELECT id, name, attrs->>'brand' AS brand FROM products"
);

Готовый пример

const { Pool } = require('pg');

const pool = new Pool({
  host: process.env.PGHOST || 'localhost',
  database: process.env.PGDATABASE || 'myapp',
  user: process.env.PGUSER || 'app_user',
  password: process.env.PGPASSWORD,
});

async function main() {
  const user = await pool.query('SELECT * FROM users WHERE id = $1', [1]);
  console.log(user.rows[0]);

  const products = await pool.query(
    'SELECT * FROM products WHERE attrs @> $1',
    [JSON.stringify({ color: 'red' })]
  );
  console.log(products.rows);
  await pool.end();
}

main().catch((err) => { console.error(err); process.exit(1); });

Проверка

  1. Подключение — запустите скрипт с корректными host, database, user, password. Ожидаем вывод без ошибки подключения. При неверных данных — сообщение об ошибке (ECONNREFUSED, authentication failed и т.д.).

  2. Результат запроса — res.rows — массив строк; res.rows[0] — первая строка. res.rowCount — количество затронутых строк (для INSERT/UPDATE/DELETE). Проверьте, что данные совпадают с тем, что в БД.

  3. Закрытие пула — в скриптах и тестах после всех запросов вызывайте await pool.end(), иначе процесс не завершится (пул держит соединения). В долгоживущем приложении (сервер) pool обычно не закрывают.

  4. JSONB-параметр — для @> передавайте сериализованный JSON: JSON.stringify({ key: value }). Не подставляйте строку в запрос вручную — только через $1.

Типичные ошибки

  • Подстановка в строку — никогда не делайте query('SELECT * FROM users WHERE id = ' + userId). Всегда используйте $1, $2 и массив параметров. Иначе SQL-инъекция.
  • Не закрывать pool в скриптах — в разовых скриптах и тестах без pool.end() процесс может висеть. В сервере (Express, Fastify и т.д.) пул обычно один на приложение и не закрывается до остановки.
  • Один Pool на приложение — создавайте один экземпляр Pool и переиспользуйте его. Не создавайте новый Pool на каждый запрос — это исчерпает лимит соединений PostgreSQL (max_connections).
  • Пароль и секреты — не храните пароль в коде. Используйте process.env.PGPASSWORD или переменные окружения (PGHOST, PGDATABASE, PGUSER, PGPASSWORD).

Где применять

  • Prod / dev: API, воркеры, скрипты миграций и админки на Node.js, подключающиеся к PostgreSQL. Для запросов по JSONB см. JSONB и GIN-индекс.
  • Совместно с EXPLAIN — при отладке медленных запросов выполняйте те же запросы из приложения и проверяйте план в psql (EXPLAIN ANALYZE).

Связанные сниппеты: JSONB и GIN-индекс, EXPLAIN ANALYZE.

Документация: node-postgres, Pooling, Queries.