Skip to content

Установка библиотек и подключение базы данных PostgreSQL к приложению

Введение

В рамках этого учебного курса мы будем разрабатывать веб-приложение с использованием Node.js и базы данных PostgreSQL. Для взаимодействия между приложением и базой данных мы будем использовать официальную библиотеку pg.

node-postgres (или просто pg) — это набор модулей для Node.js, предназначенных для работы с PostgreSQL. Она предоставляет удобные инструменты для выполнения SQL-запросов, управления подключениями и обработки результатов.

Библиотека поддерживает:

  • колбэки, промисы и async/await;
  • пул подключений (connection pooling);
  • подготовленные запросы (prepared statements);
  • курсоры;
  • потоковую обработку данных (streaming results);
  • интеграцию с C/C++;
  • расширенный парсинг типов данных PostgreSQL.

Как и сама PostgreSQL, pg предлагает широкий набор возможностей. Документация к библиотеке помогает быстро начать работу, а также содержит материалы по более сложным и специализированным темам. Благодаря этому мы сможем использовать всю мощь PostgreSQL прямо из кода на Node.js — просто, эффективно и без необходимости использовать ORM.

Основы API библиотеки pg

Библиотека pg предоставляет несколько ключевых интерфейсов для работы с PostgreSQL. В этом разделе мы рассмотрим три основных компонента:

  1. Client — прямое подключение к базе данных;
  2. Pool — пул подключений (наиболее часто используемый вариант);
  3. Result — структура данных, возвращаемая после выполнения SQL-запроса.

1. Client: подключение напрямую

Объект Client позволяет установить одно явное соединение с базой данных PostgreSQL. Это базовый интерфейс, полезный для выполнения небольших одноразовых операций или настройки транзакций вручную.

Пример:

js
import { Client } from "pg";

const client = new Client({
  host: "localhost",
  port: 5432,
  user: "postgres",
  password: "secret",
  database: "mydb",
});

await client.connect();

const res = await client.query("SELECT NOW()");
console.log(res.rows[0]);

await client.end();

Особенности:

  • Каждое соединение создаётся вручную через connect() и закрывается через end().

  • Используется в ситуациях, когда нужен полный контроль над соединением.

Объект Client создается с помощью конструктора new Client(config: Config), который принимает следующие параметры:

js
type Config = {
  user?: string, // по умолчанию: process.env.PGUSER или process.env.USER
  password?: string или function, // по умолчанию: process.env.PGPASSWORD
  host?: string, // по умолчанию: process.env.PGHOST
  port?: number, // по умолчанию: process.env.PGPORT
  database?: string, // по умолчанию: process.env.PGDATABASE или user
  connectionString?: string, // например: postgres://user:password@host:5432/database
  ssl?: any, // передаётся напрямую в node.TLSSocket, поддерживает все опции tls.connect
  types?: any, // пользовательские парсеры типов PostgreSQL
  statement_timeout?: number, // максимальное время выполнения SQL-оператора в миллисекундах (по умолчанию — без ограничения)
  query_timeout?: number, // максимальное время выполнения запроса (по умолчанию — без ограничения)
  lock_timeout?: number, // сколько миллисекунд запрос может находиться в блокировке перед отменой (по умолчанию — без ограничения)
  application_name?: string, // имя приложения, которое использует этот экземпляр Client
  connectionTimeoutMillis?: number, // максимальное время ожидания подключения (по умолчанию — без ограничения)
  idle_in_transaction_session_timeout?: number // время (в мс), после которого будет завершена сессия с открытой, но бездействующей транзакцией
}

2. Pool: пул подключений (рекомендуемый способ)

Объект Pool управляет множеством подключений к базе данных. Это наиболее эффективный и стабильный способ подключения в реальных веб-приложениях.

Пример:

js
import { Pool } from "pg";

const pool = new Pool({
  host: "localhost",
  port: 5432,
  user: "postgres",
  password: "secret",
  database: "mydb",
});

const result = await pool.query("SELECT * FROM users WHERE id = $1", [1]);
console.log(result.rows[0]);

Преимущества:

  • Пул автоматически управляет соединениями.

  • Повторно использует уже открытые подключения.

  • Подходит для приложений с высокой нагрузкой.

Пул соединений создается с помощью конструктора new Pool(config: Config), который принимает следующие параметры:

js
type Config = {
  // все параметры, допустимые для конфигурации клиента (Client), также допустимы здесь

  // дополнительные параметры, специфичные для пула подключений:

  // количество миллисекунд, в течение которых будет ожидаться подключение нового клиента
  // по умолчанию 0 — то есть ожидание не ограничено по времени
  connectionTimeoutMillis?: number

  // количество миллисекунд, которое клиент может находиться в состоянии простоя в пуле,
  // прежде чем он будет отключён от сервера и удалён из пула
  // по умолчанию 10000 (10 секунд); установите 0, чтобы отключить автоматическое удаление
  idleTimeoutMillis?: number

  // максимальное количество клиентов, которое может содержать пул
  // по умолчанию установлено значение 10
  max?: number

  // По умолчанию пул будет держать клиентов подключёнными к серверу PostgreSQL,
  // пока не истечёт idleTimeoutMillis, при этом Node.js будет поддерживать ссылку на сокет,
  // из-за чего event loop (цикл событий) не завершится, пока клиенты не будут закрыты вручную
  // или не отключатся сами по истечении времени простоя.

  // Если установить параметр `allowExitOnIdle: true`, то цикл событий Node.js завершится
  // сразу после того, как все клиенты в пуле перейдут в режим ожидания,
  // даже если сокеты всё ещё остаются открытыми.
  // Это удобно, например, в скриптах или тестах, где не хочется ждать,
  // пока все подключения станут неактивными.
  allowExitOnIdle?: boolean
}

Пул изначально создаётся пустым и будет лениво создавать новые подключения (клиенты) по мере необходимости. Каждое поле объекта конфигурации является полностью необязательным. Конфигурация, переданная пулу, также автоматически передаётся каждому клиенту, который создаётся внутри этого пула.

3. Result: результат выполнения SQL-запроса

Метод query(...) возвращает объект результата со следующей структурой:

js
{
  rows: Array<any>,         // массив строк результата
  rowCount: number,         // количество строк
  command: string,          // тип SQL-команды (например, SELECT, UPDATE)
  fields: Array<FieldInfo>  // информация о колонках
}

Пример запроса:

js
import pg from "pg";
const { Pool } = pg;

const pool = new Pool();

const client = await pool.connect();
const result = await client.query({
  rowMode: "array",
  text: "SELECT 1 as one, 2 as two;",
});
console.log(result.fields[0].name); // one
console.log(result.fields[1].name); // two
console.log(result.rows); // [ [ 1, 2 ] ]
await client.end();

Структура проекта

Создайте каталог gophertalk-backend-express. В нем создайте подкаталог src, файлы .env, package.json, README.md. В каталоге src создайте каталоги, указанные ниже, а также пустой файл app.js. Также создайте каталог __tests__ и подкаталоги в нем.

bash
gophertalk-backend-express/
├── src/
   ├── controllers/       # Обработка HTTP-запросов
   ├── services/          # Бизнес-логика
   ├── repositories/      # Работа с БД (SQL-запросы)
   ├── routes/            # Определение маршрутов
   ├── middlewares/       # Общие мидлвари
   ├── packages/          # скачанные пакеты с зависимостями
   ├── config/            # Конфигурация проекта
   ├── utils/             # Вспомогательные функции
   ├── validators/        # Валидаторы входных данных
   └── app.js             # Инициализация приложения
├── __tests__              # unit тесты
   ├── controllers/
   ├── services/
   └── repositories/
├── .env                   # Переменные окружения
├── package.json
└── README.md

Инициализация проекта и установка зависимостей

В файл package.json поместите следующее содержимое:

json
{
  "name": "gophertalk-backend-express",
  "version": "0.1.0",
  "type": "module",
  "main": "src/app.js",
  "scripts": {
    "dev": "nodemon src/app.js",
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
  },
  "dependencies": {
    "dotenv": "file:packages/dotenv-16.4.7.tgz",
    "express": "file:packages/express-4.21.2.tgz",
    "pg": "file:packages/pg-8.14.1.tgz",
    "jsonwebtoken": "file:packages/jsonwebtoken-9.0.2.tgz",
    "bcrypt": "file:packages/bcrypt-5.1.1.tgz",
    "zod": "file:packages/zod-3.24.3.tgz"
  },
  "devDependencies": {
    "nodemon": "file:packages/nodemon-3.1.9.tgz",
    "jest": "file:packages/jest-29.7.0.tgz"
  }
}

Файл package.json содержит json-объект со следующими полями:

  1. "name": "gophertalk-backend-express" - Название проекта. Обычно совпадает с названием каталога. Используется при публикации пакета (если проект публикуется в npm).

  2. "version": "0.1.0", - Версия проекта в формате SemVer (семантическое версионирование): major.minor.patch

  3. "type": "module" - Указывает, что проект использует ECMAScript-модули (ESM), а не CommonJS. Благодаря этому можно использовать import / export вместо require.

  4. "main": "src/app.js" - Главный файл приложения.

  5. "scripts" - раздел скриптов

    • "dev": "nodemon src/app.js" - запускает приложение в режиме разработки с автообновлением при изменениях (с помощью nodemon);
    • "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" - запускает Jest для выполнения тестов. Флаг --experimental-vm-modules нужен для поддержки ESM.
  6. "dependencies" - Основные зависимости проекта. Все пакеты установлены локально через файл (file:packages/...), а не из интернета. Это бывает нужно в офлайн-среде или при использовании локального репозитория.

  7. "devDependencies" - Зависимости только для разработки. Эти зависимости не попадают в продакшен-сборку.

Поместите в папку src/packages пакеты:

После этого в корне проекта в каталоге gophertalk-backend-express выполнить команду:

bash
npm install

Установка переменных окружения

Использование переменных окружения в проекте позволяет отделить конфиденциальные и изменяемые настройки (например, параметры подключения к базе данных) от основного кода приложения. Это важно по нескольким причинам.

Во-первых, безопасность: данные вроде логина, пароля, адреса сервера и имени базы данных не должны попадать в систему контроля версий (например, Git), чтобы избежать утечек при публикации кода. Переменные окружения можно хранить в .env файле, который добавляется в .gitignore, или задавать напрямую в среде запуска (например, на сервере или в CI/CD).

Во-вторых, гибкость и удобство настройки: приложение можно разворачивать в разных средах — локально, на тестовом сервере, в продакшене — без изменения исходного кода. Достаточно задать переменные окружения для каждой среды.

В-третьих, читаемость и масштабируемость: конфигурационные значения собраны в одном месте, их проще менять и документировать. Это особенно важно в командной разработке и при работе с множеством сервисов и баз данных.

Для удобства разработки мы используем пакет dotenv, который умеет считывать переменные из файла .env. Пример заполнения этого файла представлен ниже.

bash
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=postgres
ACCESS_TOKEN_EXPIRES=1h
REFRESH_TOKEN_EXPIRES=24h
ACCESS_TOKEN_SECRET=super_secret_access_token_key
REFRESH_TOKEN_SECRET=super_secret_refresh_token_key

Значения переменных установите сами. Вам нужен сервер PostgreSQL, база данных в нем и учетная запись с правами в этой БД.

Значения переменных ACCESS_TOKEN_EXPIRES, REFRESH_TOKEN_EXPIRES, ACCESS_TOKEN_SECRET и REFRESH_TOKEN_SECRET не изменяйте, они понадобятся в дальнейшем.

Настройка подключения к PostgreSQL

Создайте файл db.js в каталоге src/config. Поместите в него следующее содержимое:

js
import pg from "pg";
import dotenv from "dotenv";

dotenv.config();

const { Pool } = pg;

export const pool = new Pool({
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
});

Разберем, что происходит в этом коде.

  1. Импорт библиотек

    js
    import pg from "pg";
    import dotenv from "dotenv";
    • pg — библиотека для работы с PostgreSQL в Node.js.

    • dotenv — библиотека для загрузки переменных окружения из .env файла в process.env.

  2. Загрузка переменных окружения

    js
    dotenv.config();
    • Загружает переменные из .env файла в глобальный объект process.env.

    • После этого можно использовать, например, process.env.DB_HOST.

  3. Создание пула подключений и его экспорт

    js
    const { Pool } = pg;
    
    export const pool = new Pool({
      host: process.env.DB_HOST,
      port: process.env.DB_PORT,
      user: process.env.DB_USER,
      password: process.env.DB_PASSWORD,
      database: process.env.DB_NAME,
    });
    • Создаётся и экспортируется объект pool, который управляет множеством подключений к базе данных.

    • Все параметры берутся из переменных окружения

Создание главного файла приложения, запуск приложения и проверка подключения к БД

Поместите следующее содержимое в src/app.js

js
import dotenv from "dotenv";
import express from "express";
import { pool } from "./db/index.js";

dotenv.config();

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

app.get("/api/health-check", async (req, res) => {
  try {
    await pool.query("SELECT 1");
    res.status(200).send("OK");
  } catch (err) {
    res.status(500).send("DB connection failed");
  }
});

app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

Этот код — минимальный сервер на express, подключённый к базе данных PostgreSQL через pg. Разберем его.

1. Импорт библиотек

```js
import dotenv from "dotenv";
import express from "express";
import { pool } from "./config/db.js";
```

- `dotenv` — загружает переменные из `.env` файла.

- `express` — фреймворк для создания веб-сервера и REST API.

- `pool` — импортированный пул подключений к PostgreSQL.

2. Загрузка переменных окружения

```js
dotenv.config();
```

Загружает переменные окружения из `.env` в `process.env`.

3. Создание приложения и определение порта

```js
const app = express();
const PORT = process.env.PORT || 3000;
```

- `app` — экземпляр сервера Express.

- `PORT` — порт, на котором будет работать сервер (берётся из `.env` или по умолчанию 3000).

4. Подключение middleware

```js
app.use(express.json());
```

Позволяет Express автоматически парсить тело входящих JSON-запросов (`req.body`).

::: details Что такое middleware
Middleware — это функция, которая выполняется при обработке HTTP-запроса, между моментом его получения и отправкой ответа.

Middleware-функции могут:

- изменять объект `req` (запроса) или `res` (ответа),

- завершать обработку запроса (`res.send()` и т.п.),

- передавать управление следующей middleware-функции с помощью `next()`.

Они широко используются для:

- логирования,

- аутентификации,

- валидации данных,

- обработки ошибок,

- парсинга JSON и форм (`express.json()`, `express.urlencoded()`),

- и многого другого.

Рассмотрим пример.

```mermaid
flowchart TD
A[Входящий HTTP запрос] --> B[Middleware - проверка авторизации]
B -->|Авторизован| C[Контроллер обработки запроса]
B -->|Не Авторизован| D[401 Unauthorized]
C --> E[200 ОК]
```

Сначала запрос от клиента попадает в middleware, который проверяет, авторизован ли пользователь. Если авторизация прошла успешно (например, токен валиден), middleware передаёт управление дальше — в контроллер, который обрабатывает запрос и формирует ответ. В этом случае клиент получает успешный ответ `200 OK`.

Если же пользователь не авторизован (например, отсутствует токен или он недействителен), middleware не передаёт управление контроллеру, а сразу возвращает ответ с ошибкой `401 Unauthorized`, сообщая клиенту, что доступ запрещён.

:::

5. Маршрут /api/health-check

```js
app.get("/api/health-check", async (req, res) => {
  try {
    await pool.query("SELECT 1");
    res.status(200).send("OK");
  } catch (err) {
    res.status(500).send("DB connection failed");
  }
});
```

- Это технический `GET` маршрут для проверки состояния сервера и базы данных.

- Он делает простой запрос `SELECT 1` к базе.

- Если БД отвечает — возвращает `200 OK`, иначе `500 DB connection failed`.

::: details HTTP коды
HTTP-статус-коды делятся на 5 категорий, каждая из которых имеет своё назначение. Вот некоторые из них:

### 🔵 1xx — Информационные (Informational)

| Код | Назначение                                                            |
| --- | --------------------------------------------------------------------- |
| 100 | Продолжайте (Continue) — сервер получил заголовки и ждёт тело запроса |
| 101 | Переключение протоколов (Switching Protocols) — например, WebSocket   |

***

### 🟢 2xx — Успешные (Success)

| Код | Назначение                                                                 |
| --- | -------------------------------------------------------------------------- |
| 200 | OK — успешный запрос                                                       |
| 201 | Created — успешно создан ресурс (чаще при POST)                            |
| 204 | No Content — запрос успешен, но тело ответа отсутствует (например, DELETE) |

***

### 🟡 3xx — Перенаправление (Redirection)

| Код | Назначение                                                |
| --- | --------------------------------------------------------- |
| 301 | Moved Permanently — постоянное перенаправление            |
| 302 | Found — временное перенаправление                         |
| 304 | Not Modified — использовать закешированную версию ресурса |

***

### 🔴 4xx — Ошибки клиента (Client Errors)

| Код | Назначение                                                                        |
| --- | --------------------------------------------------------------------------------- |
| 400 | Bad Request — некорректный запрос                                                 |
| 401 | Unauthorized — требуется авторизация                                              |
| 403 | Forbidden — доступ запрещён, даже при наличии авторизации                         |
| 404 | Not Found — запрашиваемый ресурс не найден                                        |
| 409 | Conflict — конфликт запроса (например, при создании дубликата)                    |
| 422 | Unprocessable Entity — ошибка обработки данных (например, ошибка валидации формы) |

***

### 🔴 5xx — Ошибки сервера (Server Errors)

| Код | Назначение                                                              |
| --- | ----------------------------------------------------------------------- |
| 500 | Internal Server Error — внутренняя ошибка сервера                       |
| 502 | Bad Gateway — неверный ответ от стороннего сервера                      |
| 503 | Service Unavailable — сервер временно недоступен (например, перегружен) |
| 504 | Gateway Timeout — истекло время ожидания ответа от другого сервиса      |

:::

::: details HTTP методы
HTTP-методы определяют тип действия, которое клиент (например, браузер или frontend-приложение) хочет выполнить на сервере по заданному URL. Они являются основой для построения REST API и позволяют реализовывать операции чтения, создания, обновления и удаления ресурсов.

Каждый метод имеет своё назначение и семантику, и его правильное использование помогает сделать API логичным, безопасным и удобным для использования.

| Метод   | Назначение                                                                        | Идёмпотентность | Безопасность | Используется в REST для  |
| ------- | --------------------------------------------------------------------------------- | --------------- | ------------ | ------------------------ |
| GET     | Получение данных с сервера                                                        | ✅ Да           | ✅ Да        | Чтение                   |
| POST    | Отправка новых данных на сервер (создание ресурса)                                | ❌ Нет          | ❌ Нет       | Создание                 |
| PUT     | Полное обновление ресурса (замена)                                                | ✅ Да           | ❌ Нет       | Обновление               |
| PATCH   | Частичное обновление ресурса                                                      | ❌ Нет          | ❌ Нет       | Частичное обновление     |
| DELETE  | Удаление ресурса                                                                  | ✅ Да           | ❌ Нет       | Удаление                 |
| HEAD    | Как `GET`, но без тела ответа (используется для проверки заголовков, кеша и т.д.) | ✅ Да           | ✅ Да        | Проверка доступности     |
| OPTIONS | Возвращает допустимые методы для указанного ресурса (применяется для CORS)        | ✅ Да           | ✅ Да        | Обнаружение возможностей |

Если метод идемпотентен, это значит, что повторный вызов этого метода не изменит результат. Например:

- `GET /users` вернёт один и тот же список при каждом вызове;

- `DELETE /user/5` удалит пользователя, и повторный вызов уже ничего не изменит (если пользователь был удалён в первый раз);

- `POST /users` не идемпотентен — при каждом вызове может создаваться новый пользователь.

Безопасный HTTP-метод — это метод, который не изменяет состояние сервера. Он используется только для получения информации, и его выполнение не должно иметь побочных эффектов (например, создавать, изменять или удалять данные). Например:

- `GET` — безопасен, потому что просто читает данные;

- `POST` — не безопасен, потому что может создавать ресурсы или выполнять действия.

:::

6. Запуск сервера

```js
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});
```

Запускает сервер на указанном порту и выводит сообщение в консоль.

Чтобы запустить наше приложение, выполните команду

bash
npm run dev

Если все сделано правильно, то в консоли будет вывод:

bash
> gophertalk-backend-express@0.1.0 dev
> nodemon src/app.js

[nodemon] 3.1.9
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node src/app.js`
Server is running on port 3000

Чтобы проверить, что подключение к БД выполнено успешно, необходимо сделать GET запрос по адресу http://localhost:3000/api/health-check. Сделать это можно несколькими способами.

  1. С помощью утилиты curl

    В другой консоли (так как в первой у нас запущено приложение) выполните команду:

    bash
    curl http://localhost:3000/api/health-check

    В случае успеха вы увидите ответ OK. Иначе в консоли с приложением будет ошибка.

  2. С помощью браузера.

    Откройте любой браузер и перейдите по адресу http://localhost:3000/api/health-check. При таком действии браузер отправляет GET запрос. Если все нормально, то вы также увидите текст Ok.

  3. С помощью программного обеспечения Postman, об этом далее.

Итог

Мы создали минимальный, но уже работоспособный каркас веб-приложения на основе Express.
Вы узнали, как:

  • подключать необходимые зависимости (express, pg, dotenv);
  • настраивать переменные окружения через .env;
  • использовать пул подключений к базе PostgreSQL;
  • реализовать базовую маршрутизацию (/api/health-check).

Этот фундаментальный каркас послужит основой для дальнейшей разработки полноценного REST API.