Головна / Статті / Більше, ніж CRUD: десять архітектурних звичок, які забезпечують підтримуваність додатків MERN

Більше, ніж CRUD: десять архітектурних звичок, які забезпечують підтримуваність додатків MERN

Дізнайтеся про звички на рівні системи, які підтримують здоров’я додатку MERN під час його розвитку: власність на дані, контракти API, похідний стан, код у шарах, оптимізовані навантаження та послідовні помилки.

1832 слів

Більшість навчальних посібників з MERN закінчуються працездатним додатком типу CRUD: MongoDB зберігає дані, Express надає кілька маршрутів, React відображає список, а можливо, є ще екран входу. Такий додаток працює, але рідко залишається незмінним під час розширення. У міру додавання функцій та співробітників API стає важко змінювати, стан системи втрачає синхронізацію, сторінки сповільнюються, а виправлення помилок займає більше часу, ніж їх створення. Причиною рідко є інструменти. Цей посібник охоплює десять архітектурних звичок, які допомагають вирішити ці проблеми, щоб ви могли розглядати додаток MERN як єдину систему, а не сукупність чотирьох бібліотек.

Розглядайте MERN як потік даних, а не список інструментів

Зазвичай стек описується лише як сукупність його компонентів:

MongoDB + Express + React + Node.js

Це точно, але не пояснює, як елементи взаємодіють між собою, що схоже на опис автомобіля як двигуна, чотирьох коліс та керма. Більш корисний образ показує, як дані надходять від користувача до бази даних та назад:

User
   │
   ▼
React
   │
HTTP
   │
   ▼
Express + Node
   │
Database Queries
   │
   ▼
MongoDB

Кожна стрілка позначає межу з власними правилами: що може надіслати браузер, що приймає сервер, що зберігає база даних. Більшість наведених нижче правил стосуються визначення того, що відбувається на цих межах.

1. Надайте кожній частині даних одного власника

Під час розробки функції спочатку потрібно з’ясувати, хто володіє пов’язаними даними. У багатьох молодих кодових базах відповідь на це питання залишається неясною. Деталі поточного користувача можуть зберігатися у стані компонента, у сховищі Redux, у localStorage, у результаті виклику API та в якомусь іншому кеші — усе це одночасно. Рано чи пізно одна з копій відстає від інших, і інтерфейс відображає дві суперечливі версії одного й того самого факту.

Чіткіше розподілення обов’язків:

  • MongoDB є джерелом істини для постійно зберіганих даних.
  • Бекенд відповідає за бізнес-правила, які визначають, як можуть змінюватися ці дані.
  • Фронтенд відображає дані та запитує зміни через API; будь-яка копія на стороні клієнта є лише кешем, а не авторитетним джерелом інформації.

Керівним принципом є те, що кожен елемент даних має лише одне достовірне джерело, а всі інші копії можуть бути застарілими. Бібліотеки для керування станом сервера існують переважно для прямого керування цим кешуванням; детальніше про цю сторону проблеми дивіться у публікації про переосмислення стану сервера за допомогою React Query та Redux.

2. Проектування API як контрактів

Типовий перший ендпоїнт виглядає так:

app.get("/users", async (req, res) => {
  const users = await User.find();
  res.json(users);
});

Цей підхід працює, але водночас непомітно обіцяє, що відповідь завжди буде масивом повних документів користувача, незалежно від того, які поля має модель. Як тільки мобільний додаток, панель керування, інтеграція з партнером чи інша команда починають використовувати саме таку структуру, її зміна може пошкодити їхню роботу.

Перш ніж додавати ендпоїнт, вирішіть:

  • саме які поля воно повертає, а не всю сирову структуру моделі (що також може призвести до витоку внутрішніх чи конфіденційних полів);
  • чи можна його змінити пізніше без пошкодження клієнтських частин, чи потрібна версіонування;
  • які інші системи, ймовірно, будуть використовувати цей API.

Ретельно спроєктований API може існувати довше, ніж кілька фронтендів; необережно створений API перетворюється на технічний борг вже протягом кількох місяців.

3. Зберігайте якомога менше стану React

React представлений як бібліотека для інтерфейсу, але у реальних додатках основною проблемою є стан. Поширеною помилкою є зберігання у стані значень, які можна обчислити:

const [users, setUsers] = useState([]);
const [filteredUsers, setFilteredUsers] = useState([]);

Тут filteredUsers повністю визначається за допомогою users. Його окреме зберігання означає, що кожна зміна мусить підтримувати синхронність обох структур, а єдине забування призводить до використання застарілого списку. Краще обчислювати його під час відображення:

const filteredUsers = users.filter(user => user.active);

Правило полягає у тому, щоб зберігати лише те, що неможливо обчислити, а все інше отримувати шляхом похідних. Якщо процес отримання даних стає справді складним, useMemo може його зберігати у кеші, але це залишається похідними даними, а не другим джерелом істини.

4. Пам’ятайте, що CRUD — це проста частина

Багато проектів обмежуються чотирма основними операціями:

Create
Read
Update
Delete

Продакшн-бекенд охоплює ці операції ще багатьма функціями: перевірку даних, автентифікацію, авторизацію, бізнес-правила, обмеження частоти запитів, журнали аудиту, логування та сповіщення. Порівняйте примітивну операцію створення:

await User.create(req.body);

з версією, яка спочатку перевіряє вхідні дані

if (!isValid(req.body))
    throw new Error("Invalid input");

та після цього переконується, що користувач має право виконати дію, перш ніж записувати дані:

if (!canCreateUser(req.user))
    throw new Error("Unauthorized");await User.create(req.body);

Наївна версія також становить ризик масового присвоєння: передача req.body безпосередньо до функції create дозволяє клієнту встановлювати будь-які поля, які приймає схема, включаючи щось на кшталт прапорця role. Обов’язково перевіряйте та вибирайте дозволені поля явно. Також зазначимо, що невдалий перевірки дозволів концептуально є помилкою 403 (заборонено), а не 401, незалежно від тексту повідомлення про помилку. Запис даних — це просто; захист їх — ось де починається складність у розробці бекенду.

5. Розділення функцій на шари

Найочевидніша різниця між кодовою базою для хобі та професійною полягає у місці розташування логіки. Змішування функцій призводить до того, що обробники маршрутів наповнюються запитами до бази даних, компоненти React — правилами перевірки, а контролери — бізнес-логікою, причому кожен з цих файлів постійно зростає.

Бекенд, розділений на шари, призначає кожному шару певне завдання:

Routes
   │
Controllers
   │
Services
   │
Repositories
   │
Database

Мапування URL на обробники здійснюється за допомогою маршрутів, контролери перетворюють HTTP-запити на виклики функцій, сервіси містять бізнес-правила, а репозиторії взаємодіють з базою даних. Маленькі однозадачові одиниці легше тестувати та змінювати. Для більш детального ознайомлення дивіться принципи проектування API на Node.js з різними шарами.

6. Спочатку вирішуйте проблеми продуктивності на рівні джерела даних

Коли запитують, як прискорити React-додаток, більшість розробників вдаються до useMemo, React.memo та useCallback. Це допомагає, але багато проблем з продуктивністю виникають ще до того, як починає діяти React. Уявіть собі такий запит:

GET /users

що повертає

50,000 users

поки екран відображає лише

10 users

Жодна кількість мемоізації не компенсує надсилання та обробку десятків тисяч зайвих записів. Вирішуйте цю проблему на рівні джерела коду:

  • формуйте результати у пагінах;
  • фільтруйте дані на сервері;
  • передавайте лише ті поля, які потрібні клієнту;
  • стискуйте відповіді;
  • використовуйте кеш свідомо, маючи чіткий план анулювання даних у кеші.

Найшвидше для відображення є той компонент, який ніколи не отримує даних, які йому не потрібні.

7. Зробіть обробку помилок частиною дизайну

Код, призначений для розробки, часто приховує такі помилки:

try {
   ...
}
catch(error){
   console.log(error);
}

Журналування та подальша робота приховують проблему від клієнта та систем моніторингу. Продакшн-API потребують помилок, які є послідовними та зрозумілими для машин:

return res.status(400).json({
    message: "Invalid email address",
    code: "INVALID_EMAIL"
});

Стабільна структура з людиночитабельним message та машиночитабельним code дозволяє фронтенду відносити коди до конкретних повідомлень інтерфейсу, групувати журнали за кодами, активувати сповіщення при незвичайних темпах виникнення проблем, а також починати дебаггінг з відомої категорії замість стек-трейсу. Помилки є неминучими; метою є їх прогнозоване управління.

8. Організація коду за функціоналом

При двадцяти файлах підійде будь-яка структура папок. При п’ятсотах це має велике значення. Структура, згрупована за технічним типом, розтягує одну функцію по всьому дереву:

routes/
controllers/
models/

Групування за функціоналом зберігає все, що стосується однієї сфери, в одному місці:

users/
    routes.js
    controller.js
    service.js
    validation.js
orders/
    routes.js
    controller.js
    service.js

Коли змінюється логіка обробки замовлень, ви відкриваєте лише папку orders. Папки функцій також значно полегшують визначення власників, перевірку коду та його подальше виймання у окремі сервіси.

9. Думайте про системи, а не про замовлення

Запит на додавання функції, наприклад „додати входження“, можна задовольнити обмежено — через форму та маршрут. Підхід, орієнтований на систему, змушує поставити додаткові запитання: як працює автентифікація від початку до кінця, де зберігаються токени, як застосовуються права доступу, що відбувається, коли токен закінчується термін дії, та як майбутній мобільний клієнт буде увіймати. Відповідь на ці запитання заздалегідь коштує трохи більше сьогодні, але дозволяє уникнути переписування коду пізніше.

10. Свідомо обирайте компроміси

Жодна архітектура не є найкращою у кожній ситуації. Кожен варіант має свої переваги та недоліки:

  • Проста архітектура: швидше до створення, складніше для масштабування.
  • Мікросервіси: незалежне масштабування, значно вища операційна складність.
  • Глобальний стан: легке обмін даними між компонентами, складніше виправлення помилок.
  • Нормалізована структура бази даних: менше дублювання, більше з’єднань чи пошуків.
  • Агресивне кешування: швидші відповіді, постійна проблема анулювання кешу.

Сильні інженери — це не ті, хто знає кожну схему, а ті, хто може пояснити, коли кожна схема варта своїх зусиль.

Як проекти MERN зазвичай деградують під час росту

Етап 1: все просто

Перше випускання охоплює основні елементи:

CRUD
Authentication
Dashboard
Deployment

Код є невеликим, і всі його розуміють.

Етап 2: ріст виявляє швидкі шляхи

Приєднується все більше користувачів, функцій та розробників. У всьому кодовому базисі з’являється дубльована логіка, несуворість кінцевих точок, повільні сторінки, заплутаний стан та складне виправлення помилок.

Етап 3: звинувачують інструменти

Команда робить висновок, що React не підтримує масштабування або що вибір MongoDB був помилкою. Зазвичай це не так. Архітектура просто ніколи не розвивалася разом із додатком.

Аналогія ресторану для шарів

Уявіть собі цю структуру як ресторан. MongoDB — це комора, де зберігаються всі інгредієнти. Express та Node — це кухня: вони вирішують, що буде готуватися, як це зробити та хто може замовляти страви. React — це обслуговування клієнтів, яке пропонує готові страви відвідувачам. Відвідувачам не потрібно знати, як працює кухня, а кухні не важливо, як розміщені тарілки на столі. Кожна частина виконує свою роботу належним чином, що саме і є необхідною сегрегацією у додатку MERN.

Ключові висновки

Розуміння технологій MERN полягає не стільки у написанні запитів, маршрутів та компонентів, скільки у розумінні шляху, яким проходять дані, розміщенні бізнес-правил у відповідному шарі, еволюції API без пошкодження клієнтських частин, збереженні мінімального обсягу стану та усвідомленні того, як ранні рішення впливають на подальший розвиток.

  • Визначте одного власника для кожної частини даних та розглядайте всі інші копії як кеш.
  • Ставіться до кінцевих точок як до контрактів та повертайте чітко визначені формати даних.
  • Отримуйте інформацію про стан замість того, щоб копіювати його.
  • Перевіряйте, авторизовуйте та додавайте поля до списку дозволених перед записом будь-чого.
  • Зберігайте об’єми даних мінімальними на етапі формування, перш ніж оптимізувати їх відображення.
  • Повертайте послідовні, стандартизовані помилки та групуйте код за функціями.
  • Коли з’являється нова функція, найкорисніше запитання — це не те, як її створити, а де саме належить кожна відповідальність.

    Пов’язана література