open atlas
↑ К треку
Архитектура бэкенда BE · 02 · 01

Middleware и DI: два паттерна, формирующие любой backend

Под каждым фреймворком лежат два паттерна: middleware перехватывает каждый запрос по порядку, а внедрение зависимостей связывает сервисы один раз на старте. Один управляет тем, что выполняется вокруг вашего кода; другой — тем, до чего код может дотянуться и как вы его тестируете.

BE Junior ◷ 11 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Два инженера описывают один и тот же backend. Один говорит: «каждый запрос проходит через auth, логирование и валидацию до обработчика». Другой говорит: «обработчик получает клиент базы, кеш и mailer, которые он сам не создавал». Они описывают один код с двух ракурсов: запрос, текущий сквозь слои, и зависимости, текущие внутрь объектов. Эти два ракурса — middleware и внедрение зависимостей, и почти каждое решение по фреймворку, которое вы примете, относится к одному из них.

Две ортогональные оси

Прежде чем чинить архитектурную проблему, нужно понять, на какой оси она живёт — иначе тянешься не за тем инструментом. Middleware и внедрение зависимостей отвечают на разные вопросы, и их путаница — частый источник мутной архитектуры:

  • Middleware — про ось запроса: что выполняется вокруг обработчика на каждом запросе и в каком порядке. Это динамично, на каждый запрос, упорядочено: auth, логирование, парсинг тела, rate limiting. Прошлый юнит показал, почему этот порядок — граница безопасности.
  • Внедрение зависимостей — про ось связывания: какие объекты куску кода дают, вместо того чтобы он конструировал их сам. Это в основном статично, решается на старте и про связанность и тестируемость: обработчик получает UserRepository, а не вызывает new UserRepository() внутри себя.

Одно — конвейер; другое — граф. Запрос путешествует по конвейеру; граф собирается один раз до того, как придёт хоть один запрос.

Зачем нужен middleware: сквозные заботы

Auth, логирование, трейсинг, сжатие и обработка ошибок нужны почти каждому эндпоинту, но не принадлежат ни одному из них. Без middleware каждый обработчик переизобретает их — несогласованно. Middleware выносит эти сквозные заботы в упорядоченные слои, оборачивающие обработчик, чтобы они были написаны один раз и применялись единообразно.

Зачем нужен DI: инверсия управления

Когда класс вызывает new EmailService() внутри себя, он приварен к этой конкретной реализации: нельзя подменить её фейком в тесте, нацелить на другой конфиг или переиспользовать класс в другом месте. Инверсия управления переворачивает это — класс объявляет, что ему нужно (отправитель писем), а что-то внешнее решает, какую конкретную реализацию он получит. Это внешнее место — корень композиции, единственная точка рядом со стартом, которая знает реальные реализации и связывает граф объектов.

MiddlewareВнедрение зависимостей
ОсьЗапрос (что выполняется вокруг кода)Связывание (что коду дают)
КогдаНа каждый запрос, динамическиОдин раз на старте, статически
ФормаУпорядоченный конвейерГраф объектов
РешаетСквозные заботыСвязанность, тестируемость
ЛомаетсяПорядок → баги безопасности/производительностиСкрытый new → нетестируемый код

Они встречаются во фреймворке

Реальные фреймворки сплавляют оба. Контроллеры NestJS разрешаются из DI-контейнера и сидят за конвейером middleware/guard/interceptor. Express держит DI неявным (вы связываете руками или цепляете к req), но модель middleware явная. Распознавать, на какой оси живёт проблема, — рефлекс сеньора: «этот эндпоинт медленный на каждом запросе» — обычно middleware; «этот класс невозможно протестировать» — обычно DI.

У двух осей очень разные профили стоимости, и именно поэтому их стоит разделять. Стоимость middleware — на каждый запрос и повторяющаяся: каждый слой в цепочке Express обычно добавляет единицы микросекунд накладных расходов, но раздутая цепочка из 12–15 слоёв (auth, логирование, парсинг тела, сжатие, CORS, helmet, rate limit и т. д.) может добавлять порядка 0,3–0,5мс на p50 и 1–3мс на p99, когда в игру вступают GC и медленный асинхронный слой вроде аудит-лога — умноженное на каждый запрос, это уже реальная пропускная способность. Стоимость DI в основном платится один раз на старте: приложение NestJS с несколькими сотнями провайдеров тратит порядка десятков-сотен миллисекунд на сборку графа при загрузке, а затем обслуживает запросы синглтонами с почти нулевой стоимостью связывания на запрос. Так что ошибка в middleware облагает налогом каждый запрос навсегда; ошибка в DI обычно облагает только загрузку (с одним резким исключением — request-scoped провайдеры из урока 04, которые затягивают стоимость связывания обратно в горячий путь каждого запроса).

Викторина

Класс-сервис вызывает `new StripeClient()` прямо в конструкторе, и команда понимает, что его невозможно юнит-тестировать без обращения к реальному Stripe API. На какой оси проблема?

Викторина

Что лучше всего описывает разницу в том, *когда* действуют middleware и DI?

Расставь шаги по порядку

Упорядочьте события от старта процесса до обслуженного запроса:

  1. 1 Корень композиции связывает граф объектов (репозитории, клиенты, сервисы)
  2. 2 Фреймворк регистрирует конвейер middleware по порядку
  3. 3 Сервер начинает слушать соединения
  4. 4 Приходит запрос и течёт через конвейер middleware
  5. 5 Обработчик выполняется, используя внедрённые в него зависимости
  6. 6 Ответ формируется и возвращается
Вспомните перед уходом
  1. 01
    В чём разница между осью запроса и осью связывания, и зачем держать их раздельно?
  2. 02
    Зачем нужен middleware и какую проблему создают сквозные заботы без него?
  3. 03
    Что такое инверсия управления и какую роль играет корень композиции?
Итог

Под каждым backend-фреймворком лежат два ортогональных паттерна. Middleware — ось запроса: упорядоченные слои, оборачивающие обработчик и выносящие сквозные заботы — auth, логирование, парсинг тела, rate limiting — чтобы они были написаны один раз и применялись единообразно, а их порядок служил границей безопасности и производительности. Внедрение зависимостей — ось связывания: вместо того чтобы класс конструировал своих коллабораторов через new, он объявляет, что ему нужно, а корень композиции собирает граф объектов один раз на старте, и именно это делает код подменяемым и тестируемым. Одно — конвейер на каждый запрос; другое — граф времени старта. Фреймворки их сплавляют — контроллер NestJS разрешается через DI и сидит за конвейером guard/interceptor — но рефлекс сеньора в том, чтобы спросить, на какой оси живёт проблема. Следующие уроки уходят вглубь каждой: сперва анатомия написания корректного middleware, затем механика инверсии управления и DI-скоупов. Теперь, когда встретишь нестабильный эндпоинт или нетестируемый класс, первый вопрос — ось запроса или ось связывания?

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено
Связанные уроки
встречается в188

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.