Главная / Статьи / Далее 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 может служить дольше нескольких фронтендов; небрежно созданный превращается в технический долг уже через несколько месяцев.

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 без нарушения работы клиентов, сведении состояния к минимуму и осознании того, как ранние решения влияют на последующие результаты.

  • Укажите одного ответственного за каждый фрагмент данных и рассматривайте все остальные копии как кэш.
  • Рассматривайте конечные точки как контракты и возвращайте четко определенные форматы данных.
  • Получайте состояние вместо того, чтобы дублировать его.
  • Проверяйте, авторизируйте и добавляйте поля в список разрешенных перед записью любых данных.
  • Сохраняйте небольшой размер полезной нагрузки на этапе формирования, прежде чем оптимизировать отображение.
  • Возвращайте последовательные, стандартизированные ошибки и группируйте код по функциям.
  • Когда появляется новая функция, самый полезный вопрос — не как ее создать, а кто отвечает за каждый аспект ее реализации.

    Связанные материалы