Пошук уроків, статей та іншого контенту
Налаштуємо пул з’єднань, змінні середовища та коректне відкриття й закриття з’єднань із PostgreSQL.
Пул з’єднань — це набір повторно використовуваних з’єднань із PostgreSQL, якими керує застосунок.
Замість того щоб створювати нове з’єднання для кожного запиту, застосунок:
бере вільне з’єднання з пулу;
виконує SQL-запит;
повертає з’єднання в пул;
повторно використовує його для наступних запитів.
Це зменшує витрати на створення з’єднань і дозволяє обмежити кількість одночасних підключень до бази даних.
Для NestJS використаємо пакет pg і сервіс DatabaseService, який:
читає налаштування змінних середовища;
створює пул;
перевіряє з’єднання під час запуску;
виконує SQL-запити;
коректно закриває пул під час завершення застосунку.
Встановіть пакети:
npm install @nestjs/config pg
npm install -D @types/pgПакет @nestjs/config надає ConfigService для роботи зі змінними середовища, а pg — клієнт PostgreSQL для Node.js.
Створіть файл .env у корені проєкту:
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
DATABASE_NAME=app_db
DATABASE_POOL_MAX=10
DATABASE_IDLE_TIMEOUT_MS=30000
DATABASE_CONNECTION_TIMEOUT_MS=5000Не додавайте .env до репозиторію. Додайте його до .gitignore:
.envDATABASE_HOST — адреса PostgreSQL;
DATABASE_PORT — порт PostgreSQL;
DATABASE_USER — користувач бази даних;
DATABASE_PASSWORD — пароль;
DATABASE_NAME — назва бази даних;
DATABASE_POOL_MAX — максимальна кількість одночасних з’єднань у пулі;
DATABASE_IDLE_TIMEOUT_MS — час очікування перед закриттям невикористовуваного з’єднання;
DATABASE_CONNECTION_TIMEOUT_MS — максимальний час очікування встановлення з’єднання.
Значення, які надходять зі змінних середовища, завжди є рядками. Тому числові параметри потрібно явно перетворити на числа.
ConfigModuleДодайте ConfigModule до головного модуля застосунку:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module';
import { AppController } from './app.controller';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
DatabaseModule,
],
controllers: [AppController],
})
export class AppModule {}Параметр isGlobal: true дозволяє використовувати ConfigService у модулях без повторного імпорту ConfigModule.
Створіть модуль для роботи з базою даних:
// src/database/database.module.ts
import { Global, Module } from '@nestjs/common';
import { DatabaseService } from './database.service';
@Global()
@Module({
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}@Global() робить модуль доступним для всього застосунку. Сервіс можна буде інжектити в інші модулі після одноразового підключення DatabaseModule.
Тепер створіть сам сервіс:
// src/database/database.service.ts
import {
Injectable,
Logger,
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Pool, QueryResult, QueryResultRow } from 'pg';
@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(DatabaseService.name);
private readonly pool: Pool;
constructor(private readonly configService: ConfigService) {
this.pool = new Pool({
host: this.configService.getOrThrow<string>('DATABASE_HOST'),
port: this.configService.getOrThrow<number>('DATABASE_PORT'),
user: this.configService.getOrThrow<string>('DATABASE_USER'),
password: this.configService.getOrThrow<string>('DATABASE_PASSWORD'),
database: this.configService.getOrThrow<string>('DATABASE_NAME'),
max: this.configService.get<number>('DATABASE_POOL_MAX', 10),
idleTimeoutMillis: this.configService.get<number>(
'DATABASE_IDLE_TIMEOUT_MS',
30_000,
),
connectionTimeoutMillis: this.configService.get<number>(
'DATABASE_CONNECTION_TIMEOUT_MS',
5_000,
),
});
this.pool.on('error', (error) => {
this.logger.error('Помилка неактивного з’єднання PostgreSQL', error.stack);
});
}
async onModuleInit(): Promise<void> {
await this.pool.query('SELECT 1');
this.logger.log('З’єднання з PostgreSQL успішно встановлено');
}
async onModuleDestroy(): Promise<void> {
await this.pool.end();
this.logger.log('Пул з’єднань PostgreSQL закрито');
}
async query<T extends QueryResultRow = QueryResultRow>(
text: string,
values: unknown[] = [],
): Promise<QueryResult<T>> {
return this.pool.query<T>(text, values);
}
}max: 10Максимальна кількість з’єднань, які пул може використовувати одночасно.
idleTimeoutMillis: 30_000Через скільки мілісекунд невикористане з’єднання може бути закрите.
connectionTimeoutMillis: 5_000Максимальний час очікування встановлення нового з’єднання. Якщо PostgreSQL недоступний, застосунок не чекатиме безкінечно.
onModuleInitМетод onModuleInit викликається NestJS після створення модулів і залежностей.
У ньому виконується:
SELECT 1Це проста перевірка, яка підтверджує, що:
конфігурація коректна;
PostgreSQL доступний;
облікові дані правильні;
застосунок може виконувати запити.
Без цієї перевірки помилка підключення може проявитися лише під час першого запиту користувача.
onModuleDestroyМетод onModuleDestroy викликається під час завершення життєвого циклу модуля.
Виклик:
await this.pool.end();дозволяє:
завершити активні з’єднання;
не залишити відкриті TCP-з’єднання;
коректно завершити процес Node.js;
не переривати запити посеред виконання.
Додамо контролер, який читає поточний час PostgreSQL:
// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { DatabaseService } from './database/database.service';
@Controller()
export class AppController {
constructor(private readonly databaseService: DatabaseService) {}
@Get('database-time')
async getDatabaseTime(): Promise<{ now: Date }> {
const result = await this.databaseService.query<{ now: Date }>(
'SELECT NOW() AS now',
);
return {
now: result.rows[0].now,
};
}
}Запустіть застосунок:
npm run start:devПісля запуску в логах має з’явитися повідомлення:
З’єднання з PostgreSQL успішно встановленоНадішліть запит:
curl http://localhost:3000/database-timeПриклад відповіді:
{
"now": "2026-09-01T12:00:00.000Z"
}Щоб NestJS обробляв сигнали завершення процесу, увімкніть hooks у main.ts:
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();
await app.listen(3000);
}
void bootstrap();Тепер під час обробки сигналів завершення NestJS викличе lifecycle-методи, зокрема onModuleDestroy.
Наприклад, під час зупинки застосунку:
Ctrl+Cсервіс виконає:
await this.pool.end();і в логах з’явиться:
Пул з’єднань PostgreSQL закритоmax і навантаженняЗначення DATABASE_POOL_MAX не потрібно встановлювати якомога більшим.
Якщо один екземпляр застосунку має пул на 10 з’єднань, а запущено 4 екземпляри, потенційно вони можуть використовувати до 40 з’єднань:
кількість екземплярів × maxЦе значення потрібно узгодити з лімітом підключень PostgreSQL та іншими клієнтами бази даних.
Занадто малий пул може створювати чергу запитів. Занадто великий пул може перевантажити PostgreSQL.
Для невеликого застосунку значення від 5 до 10 часто є достатньою початковою конфігурацією. Остаточне значення потрібно визначати за навантаженням і вимірюваннями.
Значення не слід вставляти безпосередньо в SQL-рядок. Використовуйте параметри:
const result = await this.databaseService.query<{ id: number; name: string }>(
'SELECT id, name FROM users WHERE email = $1',
[email],
);PostgreSQL замінить $1 значенням із масиву параметрів. Це допомагає уникати SQL-ін’єкцій і правильно обробляти спеціальні символи.
Для одного SQL-запиту достатньо:
await this.pool.query('SELECT NOW()');Якщо потрібно виконати кілька запитів у межах однієї транзакції, необхідно отримати клієнт із пулу та обов’язково повернути його назад:
import { Injectable } from '@nestjs/common';
import { Pool } from 'pg';
@Injectable()
export class TransferService {
constructor(private readonly pool: Pool) {}
async transfer(fromId: number, toId: number, amount: number) {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[amount, fromId],
);
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, toId],
);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
}Ключовим є блок finally:
finally {
client.release();
}Він виконається і після успішного завершення, і після помилки. Якщо не викликати release(), клієнт залишиться зайнятим, а пул поступово може вичерпати всі доступні з’єднання.
У прикладі транзакції
TransferServiceпотрібно інжектити саме той екземпляр пулу, який використовує застосунок. Для звичайних запитів краще приховувати пул заDatabaseServiceі використовувати його методquery.
Невдалий підхід:
// Не робіть так для кожного HTTP-запиту
const client = new Client({
host: 'localhost',
database: 'app_db',
});
await client.connect();
await client.query('SELECT 1');
await client.end();Створення та закриття підключення для кожного запиту створює зайве навантаження. Застосунок має використовувати один пул, створений під час запуску.
release()Якщо клієнт отримано через pool.connect(), його потрібно звільнити:
const client = await pool.connect();
try {
await client.query('SELECT 1');
} finally {
client.release();
}Інакше доступні клієнти пулу поступово закінчаться.
Не слід викликати pool.end() після кожного SQL-запиту. Цей метод призначений для остаточного завершення роботи застосунку.
Не зберігайте пароль і адресу бази даних безпосередньо в коді:
// Невдалий підхід
password: 'postgres',Використовуйте змінні середовища та ConfigService.
Якщо не виконати тестовий запит у onModuleInit, застосунок може запуститися, навіть коли PostgreSQL недоступний. Краще виявити проблему під час запуску, а не після першого користувацького запиту.
Значення max: 100 не гарантує кращу продуктивність. Велика кількість з’єднань може перевищити можливості PostgreSQL. Розмір пулу потрібно підбирати з урахуванням кількості екземплярів застосунку та ліміту бази даних.
Пул з’єднань повторно використовує з’єднання PostgreSQL.
Налаштування підключення потрібно зберігати у змінних середовища.
max обмежує кількість одночасних з’єднань.
connectionTimeoutMillis обмежує час встановлення підключення.
idleTimeoutMillis визначає час життя неактивного з’єднання.
onModuleInit можна використати для перевірки доступності PostgreSQL.
onModuleDestroy має закривати пул через pool.end().
app.enableShutdownHooks() дозволяє NestJS коректно обробляти завершення процесу.
Для запитів через pool.connect() клієнт завжди потрібно повертати в пул через release().
Для значень SQL-запитів потрібно використовувати параметри $1, $2 тощо.