Пошук уроків, статей та іншого контенту
Розглянете роль баз даних у Node.js-застосунках, взаємодію компонентів і життєвий цикл запиту до даних.
Node.js-застосунок зазвичай відповідає за:
приймання HTTP-запитів;
перевірку вхідних даних;
виконання бізнес-логіки;
звернення до бази даних;
формування HTTP-відповіді.
База даних відповідає за довготривале зберігання інформації. Наприклад:
облікові записи користувачів;
товари та замовлення;
повідомлення;
налаштування застосунку.
Дані, що зберігаються лише в пам’яті Node.js, зникнуть після перезапуску процесу. База даних зберігає їх незалежно від життєвого циклу Node.js-застосунку.
Типова архітектура має такий вигляд:
Клієнт
↓ HTTP-запит
Node.js-сервер
↓
Маршрутизація
↓
Бізнес-логіка
↓
Репозиторій або сервіс доступу до даних
↓
Драйвер бази даних
↓
База данихСервер приймає запит від клієнта та визначає, що з ним робити.
Наприклад:
GET /users/42Сервер має зрозуміти:
що клієнт хоче отримати користувача;
який ідентифікатор користувача потрібен;
які дані треба знайти в базі;
яку відповідь повернути.
Node.js не має універсального вбудованого інтерфейсу для всіх баз даних. Для взаємодії використовують драйвер або бібліотеку, що розуміє протокол конкретної бази даних.
Наприклад, пакет pg використовується для PostgreSQL:
npm install pgДрайвер:
відкриває мережеве з’єднання з базою;
передає SQL-запити;
отримує результати;
перетворює їх на об’єкти JavaScript;
повідомляє про помилки.
База даних виконує SQL-запит і повертає результат. Вона також відповідає за:
збереження даних;
пошук і фільтрацію;
обмеження цілісності даних;
паралельну роботу багатьох клієнтів.
Node.js не повинен самостійно зберігати важливі дані у змінних або файлах, якщо ці дані мають бути доступними після перезапуску застосунку.
Взаємодія з базою даних відбувається через з’єднання. Створення нового з’єднання для кожного HTTP-запиту є неефективним:
з’єднання потрібно створити;
виконати один запит;
закрити з’єднання.
Замість цього застосунок використовує пул з’єднань. Пул заздалегідь керує набором з’єднань і повторно використовує їх.
HTTP-запит 1 ─┐
HTTP-запит 2 ─┼─ Пул з’єднань ─ База даних
HTTP-запит 3 ─┘Коли Node.js потрібно виконати запит:
пул надає вільне з’єднання;
драйвер відправляє SQL до бази;
результат повертається в застосунок;
з’єднання стає доступним для наступного запиту.
Пул не означає, що кожен HTTP-запит обов’язково створює нове з’єднання.
Розглянемо запит:
GET /usersЙого типовий життєвий цикл складається з таких етапів.
Браузер, мобільний застосунок або інший сервер надсилає запит Node.js-серверу.
Node.js перевіряє HTTP-метод і шлях:
метод: GET;
шлях: /users.
Після цього викликається відповідний обробник.
Обробник може перевірити:
наявність потрібних параметрів;
формат параметрів;
права доступу.
У простому прикладі додаткових параметрів може не бути.
Код маршруту не повинен містити всю логіку роботи з базою. Краще передати цю відповідальність окремому рівню, наприклад функції репозиторію.
Репозиторій знає:
яку таблицю використовувати;
який SQL-запит виконати;
які параметри передати;
як повернути результат застосунку.
Драйвер бере SQL-запит і передає його базі даних через з’єднання з пулу.
База знаходить потрібні записи та повертає результат.
Застосунок:
отримує рядки з бази;
за потреби перетворює їх;
формує JSON;
встановлює HTTP-статус.
Клієнт отримує, наприклад:
[
{
"id": 1,
"name": "Олена"
}
]Якщо база даних недоступна або запит завершився помилкою, сервер має повернути відповідний статус, наприклад 500 Internal Server Error.
Навіть у невеликому застосунку корисно розділяти відповідальність компонентів.
Маршрут визначає, який код виконується для певного HTTP-запиту.
GET /users → отримати список користувачівОбробник координує роботу:
читає параметри;
викликає потрібну функцію;
повертає результат клієнту.
Репозиторій працює з базою даних. Він приховує SQL від решти застосунку.
Наприклад, замість SQL у маршруті можна викликати:
const users = await getUsers();Так код маршруту не залежить від деталей конкретної таблиці.
База даних зберігає та обробляє дані. Вона не повинна знати про HTTP-маршрути або формат відповіді клієнту.
У цьому прикладі:
Node.js створює HTTP-сервер;
pg керує підключенням до PostgreSQL;
репозиторій виконує SQL-запит;
маршрут повертає список користувачів.
Спочатку створимо таблицю в PostgreSQL:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
INSERT INTO users (name, email)
VALUES
('Олена', 'olena@example.com'),
('Андрій', 'andrii@example.com');Створіть файл server.js:
const http = require('node:http');
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST || 'localhost',
port: Number(process.env.DB_PORT || 5432),
database: process.env.DB_NAME || 'example_db',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
});
async function getUsers() {
const result = await pool.query(
'SELECT id, name, email FROM users ORDER BY id'
);
return result.rows;
}
function sendJson(response, statusCode, data) {
response.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
});
response.end(JSON.stringify(data));
}
const server = http.createServer(async (request, response) => {
if (request.method === 'GET' && request.url === '/users') {
try {
const users = await getUsers();
sendJson(response, 200, users);
} catch (error) {
// Не показуємо технічні деталі помилки клієнту
console.error('Помилка запиту до бази даних:', error);
sendJson(response, 500, {
error: 'Не вдалося отримати користувачів',
});
}
return;
}
sendJson(response, 404, {
error: 'Маршрут не знайдено',
});
});
server.listen(3000, () => {
console.log('Сервер запущено на http://localhost:3000');
});
async function shutdown() {
// Коректно закриваємо пул перед завершенням процесу
await pool.end();
server.close(() => {
process.exit(0);
});
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);Встановіть пакет і запустіть сервер:
npm install pg
node server.jsПісля цього запит до:
GET http://localhost:3000/usersповерне записи з таблиці users.
У прикладі:
Pool керує з’єднаннями;
getUsers належить до рівня доступу до даних;
SQL-запит виконується через pool.query;
result.rows містить рядки, отримані з PostgreSQL;
HTTP-обробник перетворює результат на JSON.
Параметри не слід вставляти в SQL за допомогою конкатенації рядків.
Небезпечний варіант:
const email = request.query.email;
const sql = `SELECT * FROM users WHERE email = '${email}'`;Якщо значення надходить від користувача, така побудова запиту може призвести до SQL-ін’єкції.
Для параметрів потрібно використовувати параметризовані запити:
const result = await pool.query(
'SELECT id, name, email FROM users WHERE email = $1',
[email]
);У цьому випадку PostgreSQL окремо обробляє SQL і значення параметра.
Порядкові номери параметрів у PostgreSQL починаються з $1:
await pool.query(
'SELECT id, name FROM users WHERE id = $1',
[userId]
);Помилки можуть виникнути на різних етапах:
база даних недоступна;
неправильні облікові дані;
таблиця не існує;
порушено унікальність значення;
SQL-запит має помилку;
пул не має вільного з’єднання.
Операції з базою даних є асинхронними, тому їх потрібно очікувати за допомогою await і обробляти через try...catch.
try {
const result = await pool.query('SELECT id FROM users');
console.log(result.rows);
} catch (error) {
console.error('Помилка роботи з базою даних:', error);
}Клієнту зазвичай не варто повертати повний текст внутрішньої помилки. Він може містити:
назви таблиць;
структуру бази даних;
фрагменти SQL;
налаштування сервера.
Технічні деталі краще записати в журнал сервера, а клієнту надіслати зрозуміле загальне повідомлення.
Параметри підключення не варто жорстко записувати в коді застосунку. Замість цього використовують змінні середовища:
DB_HOST=localhost
DB_PORT=5432
DB_NAME=example_db
DB_USER=postgres
DB_PASSWORD=секретний_парольУ коді вони читаються через process.env:
const databaseName = process.env.DB_NAME;Це дає змогу використовувати той самий код у різних середовищах:
локальна розробка;
тестове середовище;
продакшен.
Паролі та інші секрети не слід додавати до репозиторію з кодом.
Для операції отримання користувачів потік виглядає так:
1. Клієнт надсилає GET /users
2. Node.js знаходить обробник маршруту
3. Обробник викликає getUsers()
4. Репозиторій викликає pool.query(...)
5. Пул надає з’єднання
6. PostgreSQL виконує SELECT
7. PostgreSQL повертає рядки
8. getUsers() повертає result.rows
9. Node.js формує JSON-відповідь
10. Клієнт отримує список користувачівТаке розділення допомагає зрозуміти, де саме сталася помилка. Наприклад:
якщо маршрут не викликався — проблема на рівні HTTP;
якщо getUsers не викликається — проблема в коді застосунку;
якщо SQL не виконується — проблема взаємодії з базою;
якщо рядки повернулися, але відповідь неправильна — проблема формування відповіді.
Це створює зайве навантаження та може швидко вичерпати доступні з’єднання.
Краще створити один пул під час запуску застосунку та використовувати його повторно.
Якщо помилку запиту не обробити, клієнт може отримати некоректну відповідь, а застосунок — непередбачувану поведінку.
Для асинхронних операцій із базою використовуйте try...catch.
Не вставляйте значення користувача безпосередньо в SQL. Використовуйте параметризовані запити.
Коли маршрут одночасно:
читає HTTP-запит;
будує SQL;
обробляє результат;
формує відповідь,
код стає складнішим для перевірки та повторного використання. Роботу з базою краще винести в окремі функції або репозиторії.
Повний текст помилки бази даних призначений для журналу сервера, а не для користувача. Клієнту слід повертати безпечне та зрозуміле повідомлення.
Під час коректного завершення застосунку відкритий пул потрібно закрити. Для цього використовується pool.end().
Node.js обробляє HTTP-запити, а база даних зберігає довготривалі дані.
Для взаємодії Node.js із базою використовується драйвер.
Пул з’єднань дає змогу повторно використовувати з’єднання.
Життєвий цикл запиту проходить шлях від клієнта через маршрут і рівень даних до бази, а потім назад.
Репозиторій або інший рівень доступу до даних допомагає відокремити SQL від HTTP-логіки.
Асинхронні запити до бази потрібно обробляти через await і try...catch.
Для значень, що надходять від користувача, потрібно використовувати параметризовані SQL-запити.
Налаштування підключення та секрети слід передавати через змінні середовища.