Проблемы с middlewares в Express.js
Express.js это низкоуровневый фреймворк. Он полностью построен на концепции middleware. По сути приложение на Express это серия вызовов middleware-функций. Express не даёт никаких абстракций поверх этого и не даёт никаких рекомендаций по их использованию.
В небольших приложениях обычно с этим проблем нет, но как только приложение становится больше, то повсеместное использование middleware приводит к ряду проблем.
Сильная связанность между middlewares
Так как middlewares выполняются по цепочке, то велик соблазн делать их очень маленькими выполняющими только одну функцию и передавать результат дальше. Например, представим что мы хотим сделать валидацию данных в теле запроса и возвращать ошибки если валидация не прошла. Можно сделать две middleware validateBody
и sendValidationErrors
. Использоваться будут так:
app.post( '/comments' validateBody, sendValidationErrors, addCommentHandler );
Проблема в том что они не имеют смысла друг без друга и всегда должны добавляться вместе. Легко ошибиться и добавить только одну или добавить в неправильном порядке и т.д. Решение тут либо объединить их в одну middleware, либо вообще не использовать для этого middleware (об этом будет дальше).
Использование middlewares для общих функций
Часто возникает потребность выполнять один и тот же код в нескольких роутов и такой код выносят в middleware и подключают для нужных роутов. Основная проблема здесь, что при взгляде на основной хендлер у вас нет информации о том какие middlewares выполняются перед ним, так как их подключение происходит где-то снаружи этого хендлера и даже возможно в другом файле. Поэтому становится сложнее понять весь процесс работы данного роута и увеличивается вероятность ошибиться при подключении нужно middleware (забыть её подключить, подключить не в том порядке и т.д.).
Решение здесь - выносить общий код не в middleware, а в обычные функции.
Пример до:
app.get( '/route', middleware1, middleware2, middleware3, myHandler );
Пример после:
function myHandler() { func1(); func2(); func3(); // ... }
Использование middlewares для разделения кода хендлера
Представим, что в примере с добавлением комментария нам сначала нужно проверить что это не спам, а для этого нам нужно отправить запрос на другой сервис. Кажется что эта проверка не связана напрямую с кодом добавления комментария и можно вынести это в отдельную middleware и в будущем можно будет легко её переиспользовать в другом роуте где это понадобится.
async function checkSpam(req, res, next) { const isSpam = await spamChecker.check(req.body); if (isSpam) { throw new Error('spam!'); } next(); } function addCommentHandler() { // ... } app.post( '/comments' checkSpam, addCommentHandler );
Кроме всех проблем из пункта "Использование middlewares для общих функций" сюда добавляются новые: при взгляде на файл с роутами для комментариев уже невозможно сразу сказать что из этого обработчики роутов, а что middleware. Даже наличие параметра next
ничего не гарантирует, так как он может быть и в обычном обработчике. Придется вчитываться либо в комментарии к функциям (если они написаны и актуальны) либо в их код. Дополнительно к этому цепочки middlewares очень хрупкие и их легко сломать если забыть в какой-то ситуации вызвать next
. При рефакторинге кода разделенного на middlewares надо быть особенно внимательным.
Свалка в объектах req и res
Каждая middleware имеет доступ к объектам реквеста и респонса и в любой момент может там что-то добавить, изменить или удалить. Через какое-то время уже совершенно невозможно отследить какой обработчик куда и что добавил. Отладка становится сложной, количество случайных багов растёт.
Goto программирование
Любая middleware может вызвать next
и передать ему объект ошибки. В результате все последующие обработчики будут пропущены и выполняться начнут обработчики ошибок. Есть соблазн написать много кастомных обработчиков ошибок чтобы там ловить разные типы ошибок и как-то их обрабатывать.
app.get( '/route', middleware1, middleware2, middleware3, logError, handleAuthError, handleCommonError, myHandler );
В итоге получаем классическое goto программирование. В таком коде легко делать баги и его тяжело отлаживать.
Слишком сильная связанность кода приложения с кодом фреймворка
Задача http слоя в приложении это сопоставить запрос с нужным обработчиком (по урлу, http методу и т.д.), достать и подготовить данные из запроса (например, распарсить json или достать параметр из урла), передать их в обработчик с бизнес логикой, получить от него результат и превратить его в http ответ понятный клиенту (сериализовать в json, выставить правильный код ответа и т.д.).
Если вы начинаете использовать абстракции которые предлагает http-фреймворк для бизнес кода, то он становится слишком от него зависимым. Middlewares поощряют использовать экспрессовские объекты реквеста и респонса. Когнитивная модель приложения построенная на middlewares более сложная, с ней сложнее работать и в конечном итоге это приводит к более медленной разработке и большему количеству багов.