Як правильно організувати базу даних в IoT-системі з адмін-панеллю та бекенд-сервісами
Вступ
Кожен розробник IoT-систем рано чи пізно стикається з дилемою: як організувати архітектуру проєкту, щоб адмін-панель та бекенд не конфліктували через базу даних? У цій статті розглянемо еволюцію підходів від найгіршого до найкращого на прикладі реального проєкту.
Контекст проєкту
Маємо типову IoT-систему:
- IoT Backend — Express.js сервер з MQTT брокером
- Admin Panel — Next.js додаток для управління пристроями
- PostgreSQL — база даних
- Prisma — ORM для роботи з БД
Завдання: організувати архітектуру так, щоб обидва сервіси працювали з однією базою даних без конфліктів.
❌ Підхід 1: “Бекенд керує всім” (Антипатерн)
Як виглядає:
services:
backend:
environment:
- SETUP_DATABASE=true # Backend налаштовує схему
admin-panel:
depends_on:
- backend # Чекає коли backend налаштує БД
Проблеми:
- 🚫 Бекенд виходить за межі своїх обов’язків
- 🚫 Адмін-панель залежить від бекенда
- 🚫 Незрозуміло, хто відповідає за схему БД
- 🚫 Складно масштабувати
Чому так не можна:
Бекенд повинен використовувати дані, а не керувати схемою. Це порушує принцип Single Responsibility.
🤔 Підхід 2: “Адмін-панель керує схемою” (Краще, але не ідеально)
Як виглядає:
services:
admin-panel:
environment:
- SETUP_DATABASE=true # Admin панель налаштовує схему
backend:
depends_on:
- admin-panel # Чекає готову схему
environment:
- WAIT_FOR_SCHEMA=true
Переваги:
- ✅ Логічний розподіл: адмін керує, бекенд використовує
- ✅ Бекенд не займається налаштуванням БД
- ✅ Чітка послідовність запуску
Недоліки:
- 🤔 Дублювання Prisma схеми в обох проєктах
- 🤔 Синхронізація змін вручну
- 🤔 Різні версії типів TypeScript
✅ Підхід 3: “Shared Database Package” (Ідеальний)
Архітектура:
monorepo/
├── packages/
│ └── database/ # 🎯 Централізована схема
│ ├── prisma/schema.prisma
│ ├── generated/prisma/
│ └── src/client.ts
├── admin-panel/ # Використовує database package
├── backend/ # Використовує database package
└── pnpm-workspace.yaml
Як працює:
1. Database Package
// packages/database/src/client.ts
import { PrismaClient } from '../generated/prisma'
export const prisma = new PrismaClient({
log: ['query', 'error', 'warn']
})
2. Admin Panel
// admin-panel/src/lib/prisma.ts
import { prisma } from 'database' // ✅ Імпорт з shared package
export { prisma }
3. Backend
// backend/src/database.js
const { prisma } = require('database') // ✅ Той самий клієнт
module.exports = { prisma }
Переваги:
- ✅ Single Source of Truth — одна схема для всіх
- ✅ Автоматична синхронізація типів
- ✅ Type Safety у всьому проєкті
- ✅ Легке тестування — одна база для всіх
- ✅ Простота розгортання — зрозуміла послідовність
🔧 Імплементація кроками
Крок 1: Створюємо Database Package
// packages/database/package.json
{
"name": "database",
"scripts": {
"db:generate": "prisma generate",
"db:deploy": "prisma migrate deploy",
"db:studio": "prisma studio"
},
"dependencies": {
"@prisma/client": "^5.16.0"
}
}
Крок 2: Налаштовуємо Workspace
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'admin-panel'
- 'backend'
Крок 3: Оновлюємо залежності
// admin-panel/package.json & backend/package.json
{
"dependencies": {
"database": "workspace:*"
}
}
Крок 4: Docker для Monorepo
# docker-compose.yml
services:
admin-panel:
build:
context: . # Build з root для доступу до packages/
volumes:
- ./packages/database:/app/packages/database
backend:
build:
context: . # Той самий контекст
depends_on:
- admin-panel # Чекає схему
📊 Порівняння підходів
| Критерій | Backend керує | Admin керує | Shared Package |
| Логічність | ❌ Погано | ✅ Добре | ✅ Відмінно |
| Синхронізація | ❌ Ручна | ❌ Ручна | ✅ Автоматична |
| Type Safety | ❌ Конфлікти | 🤔 Ручна | ✅ Автоматична |
| Масштабованість | ❌ Складно | 🤔 Середньо | ✅ Легко |
| Maintenance | ❌ Високий | 🤔 Середній | ✅ Низький |
🚀 Практичні переваги Monorepo
1. Розробка
# Одна команда для всього
pnpm dev
# Зміна схеми автоматично оновлює типи скрізь
vim packages/database/prisma/schema.prisma
pnpm db:generate # Оновлює всі проєкти
2. CI/CD
# .github/workflows/ci.yml
- name: Setup database
run: pnpm --filter database db:generate
- name: Test all projects
run: pnpm test # Тестує з консистентною схемою
3. Команда
- Новий розробник бачить всю архітектуру в одному репо
- Легко робити cross-service зміни
- Відсутність “mystery dependencies”
⚠️ Поширені помилки
1. “Скопіюю схему швидко”
Bash
# ❌ НЕ РОБІТЬ ТАК
cp admin-panel/prisma/schema.prisma backend/prisma/
Проблема: Через тиждень схеми стануть різними.
2. “Буду синхронізувати вручну”
Проблема: Людський фактор + складність проєкту = гарантовані помилки.
3. “Monorepo це складно”
Реальність: Сучасні інструменти (pnpm, Turborepo, Nx) роблять це простим.
🎯 Висновки
Коли використовувати Shared Database Package:
- ✅ Кілька сервісів працюють з однією БД
- ✅ Потрібна консистентність типів
- ✅ Команда більше 2 розробників
- ✅ Планується розвиток проєкту
Коли можна обійтися простішим:
- 🤔 Маленький проєкт (1 розробник)
- 🤔 Схема БД не змінюється
- 🤔 Різні бази для різних сервісів
🛠️ Інструменти для monorepo
| Інструмент | Призначення | Рекомендація |
| pnpm | Package manager | ⭐⭐⭐⭐⭐ |
| Turborepo | Build orchestration | ⭐⭐⭐⭐⭐ |
| Nx | Monorepo framework | ⭐⭐⭐⭐ |
| Lerna | Legacy tool | ⭐⭐ |
🔮 Майбутнє
Тренди в архітектурі IoT-проєктів:
- Micro-frontends для адмін-панелей
- Event-driven architecture для real-time даних
- Edge computing для обробки IoT-даних
- GraphQL Federation для великих систем
📚 Корисні ресурси
💡 Заключення
Правильна архітектура — це не “показуха”, а інвестиція в майбутнє проєкту. Shared Database Package підхід дозволяє:
- 🎯 Зосередитися на бізнес-логіці, а не на синхронізації
- 🚀 Швидше додавати нові фічі
- 🛡️ Уникати помилок через inconsistency
- 👥 Покращити DX (Developer Experience)
Почніть з простого, але плануйте масштабування. Ваша команда подякує вам через рік!
Якщо стаття була корисною — поділіться! Маєте питання або досвід з подібними проєктами? Пишіть в коментарях!