Пошук уроків, статей та іншого контенту
Підключите production-базу даних, налаштуєте пул з’єднань і безпечне зберігання облікових даних.
У production база даних має бути:
доступна з інфраструктури, де запущено Next.js;
захищена автентифікацією та шифруванням з’єднання;
налаштована з урахуванням обмеження кількості з’єднань;
недоступна для клієнтського JavaScript;
підготовлена до повторних запусків і масштабування застосунку.
Для прикладів використаємо PostgreSQL і пакет pg.
Встановіть залежності:
npm install pg
npm install --save-dev @types/pgПідключення до бази даних потрібно виконувати лише на сервері Next.js. Не імпортуйте модуль із підключенням у клієнтські компоненти.
Рядок підключення зазвичай має такий формат:
postgresql://USER:PASSWORD@HOST:5432/DATABASEЛокально змінні можна зберігати у файлі .env.local:
DATABASE_URL=postgresql://app_user:strong_password@localhost:5432/app_db
DB_POOL_MAX=5Файл .env.local не можна додавати до репозиторію. Перевірте, що він є у .gitignore:
.env*
!.env.exampleДля команди можна створити .env.example без справжніх секретів:
DATABASE_URL=
DB_POOL_MAX=5У production змінні середовища додаються в панелі хостингу або в системі керування секретами. Не записуйте пароль у:
код;
package.json;
Dockerfile;
логи;
публічні змінні з префіксом NEXT_PUBLIC_.
DATABASE_URLне повинен мати префіксNEXT_PUBLIC_, інакше він може потрапити до клієнтського бандла.
Відкривати нове з’єднання для кожного HTTP-запиту неефективно:
// Поганий підхід: нове з'єднання створюється для кожного запиту
const client = new Client({
connectionString: process.env.DATABASE_URL,
});
await client.connect();
const result = await client.query('SELECT 1');
await client.end();Встановлення TCP-з’єднання та автентифікація мають свою вартість. Крім того, база даних може дозволяти лише обмежену кількість одночасних з’єднань.
Пул з’єднань:
повторно використовує вже відкриті з’єднання;
обмежує кількість одночасних з’єднань;
ставить зайві запити в чергу;
звільняє з’єднання після завершення запиту.
Створіть файл src/lib/db.ts:
import { Pool } from 'pg';
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error('Змінна середовища DATABASE_URL не налаштована');
}
const poolMax = Number(process.env.DB_POOL_MAX ?? 5);
if (!Number.isInteger(poolMax) || poolMax < 1) {
throw new Error('DB_POOL_MAX має бути додатним цілим числом');
}
declare global {
// Зберігаємо пул між перезавантаженнями під час локальної розробки.
// eslint-disable-next-line no-var
var postgresPool: Pool | undefined;
}
export const pool =
globalThis.postgresPool ??
new Pool({
connectionString: databaseUrl,
// Не створюємо більше цього числа з'єднань на один екземпляр застосунку.
max: poolMax,
// Закриваємо неактивне з'єднання після цього часу.
idleTimeoutMillis: 30_000,
// Не чекаємо нескінченно на вільне з'єднання.
connectionTimeoutMillis: 5_000,
});
if (process.env.NODE_ENV !== 'production') {
globalThis.postgresPool = pool;
}Під час локальної розробки Next.js може багаторазово перезавантажувати модулі. Глобальне посилання допомагає не створювати новий пул після кожного hot reload.
У production зазвичай запускається стабільний процес, тому глобальне збереження не потрібне.
Production-бази даних зазвичай вимагають TLS. Часто провайдер передає це налаштування безпосередньо в DATABASE_URL, наприклад через параметр sslmode=require.
DATABASE_URL=postgresql://app_user:password@db.example.com:5432/app_db?sslmode=requireЯкщо провайдер вимагає явного налаштування TLS, його можна передати в Pool:
const pool = new Pool({
connectionString: databaseUrl,
ssl: {
rejectUnauthorized: true,
// У змінній середовища зберігається сертифікат центру сертифікації.
ca: process.env.DATABASE_CA,
},
});Не використовуйте rejectUnauthorized: false без чіткого розуміння наслідків: це вимикає перевірку сертифіката сервера і послаблює захист від підміни з’єднання.
У App Router серверний код можна розмістити в Route Handler. Створіть файл src/app/api/health/route.ts:
import { pool } from '@/lib/db';
export const runtime = 'nodejs';
export async function GET() {
try {
const result = await pool.query<{ ok: number }>('SELECT 1 AS ok');
return Response.json({
status: 'ok',
database: result.rows[0]?.ok === 1,
});
} catch (error) {
console.error('Помилка перевірки бази даних:', error);
return Response.json(
{
status: 'error',
database: false,
},
{ status: 503 },
);
}
}Параметр runtime = 'nodejs' важливий, якщо драйвер бази даних використовує Node.js API. Пакет pg потрібно запускати в Node.js runtime, а не в Edge Runtime.
Після запуску застосунку запит:
curl http://localhost:3000/api/healthможе повернути:
{
"status": "ok",
"database": true
}У відповідь не потрібно додавати адресу бази, ім’я користувача чи текст помилки підключення. Такі дані можуть розкрити внутрішню конфігурацію системи.
Для значень, які надходять від користувача, використовуйте параметризовані запити:
import { pool } from '@/lib/db';
export async function findUserByEmail(email: string) {
const result = await pool.query(
`
SELECT id, email, name
FROM users
WHERE email = $1
LIMIT 1
`,
[email],
);
return result.rows[0] ?? null;
}Значення передається окремим масивом параметрів. Не вставляйте його безпосередньо в SQL-рядок:
// Небезпечно: значення користувача вставляється в SQL.
const query = `SELECT * FROM users WHERE email = '${email}'`;Параметризовані запити допомагають запобігати SQL-ін’єкціям.
max у конфігурації пулу — це максимальна кількість з’єднань на один процес або екземпляр застосунку.
Наприклад, якщо:
база дозволяє 100 з’єднань;
застосунок працює у 10 екземплярах;
max дорівнює 20;
то теоретично застосунок може спробувати використати до 200 з’єднань. Це перевищить ліміт бази.
На практиці залишайте частину з’єднань для:
адміністративних операцій;
міграцій;
інших сервісів;
внутрішніх процесів бази даних.
Тому починайте з невеликого значення, наприклад 5, і змінюйте його після вимірювань:
DB_POOL_MAX=5У serverless-середовищі кількість екземплярів може змінюватися автоматично. Кожен новий екземпляр може створити власний пул, тому великий max особливо небезпечний.
Якщо база має суворий ліміт з’єднань, використовуйте рекомендований провайдером connection pooler або проксі. Конкретне рішення залежить від вашого хостингу та бази даних.
Перед розгортанням перевірте:
DATABASE_URL налаштована саме в production-середовищі.
Пароль не потрапив у Git.
Підключення використовує TLS, якщо цього вимагає провайдер.
DB_POOL_MAX відповідає кількості екземплярів застосунку.
Серверний код не імпортується у клієнтські компоненти.
Для застосунку створений окремий користувач бази даних.
Користувач має лише необхідні дозволи.
Помилки підключення не повертаються клієнту повністю.
Production-збірка справді використовує Node.js runtime для pg.
Пароль у .env.local, коміті або конфігураційному файлі може стати доступним усім, хто має доступ до репозиторію.
Якщо секрет уже потрапив у Git, недостатньо просто видалити файл. Потрібно відкликати або змінити цей пароль у базі даних.
Якщо різні файли створюють власний new Pool(), застосунок відкриває зайві з’єднання. Створіть один модуль підключення та імпортуйте pool з нього.
maxЗначення max: 50 не означає, що застосунок стане швидшим. Під час масштабування кожен екземпляр може відкрити до 50 з’єднань і перевищити ліміт бази.
pg в Edge Runtimepg призначений для Node.js-середовища. Для коду, який використовує цей драйвер, явно задавайте:
export const runtime = 'nodejs';Налаштування rejectUnauthorized: false приховує проблему з сертифікатами, але зменшує безпеку. Спочатку налаштуйте правильний сертифікат або параметри підключення, рекомендовані провайдером.
Не повертайте користувачу error.message від драйвера бази даних. Повідомлення може містити адресу сервера, назву бази або фрагмент SQL. Повну помилку записуйте в захищені серверні логи, а клієнту повертайте загальне повідомлення.
Production-облікові дані зберігайте у змінних середовища або менеджері секретів.
Не використовуйте NEXT_PUBLIC_ для DATABASE_URL.
Створюйте один серверний пул з’єднань і повторно його використовуйте.
Обмежуйте max з урахуванням кількості production-екземплярів.
Для pg використовуйте Node.js runtime.
Увімкніть TLS і не вимикайте перевірку сертифіката без необхідності.
Використовуйте параметризовані SQL-запити.
Не розкривайте клієнту внутрішні помилки та облікові дані бази.