Express / 37 минут

Слои и зависимости API

Отделите HTTP, сценарий и хранилище; передавайте зависимости явно.

В процессе Теория: впереди Практика: впереди Тест: впереди
10 / 12
После этой главы
  • Отделите HTTP, сценарий и хранилище; передавайте зависимости явно.
  • Применить тему в задаче «Слои и зависимости API» и объяснить граничные случаи.

Около 7 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Причины разделять обязанности

Небольшой handler легко написать в одном файле. Со временем он начинает читать параметры, проверять пользователя, обращаться к БД, отправлять письмо и формировать ответ. Изменение одной детали тогда затрагивает несвязанные правила. Слои нужны не ради количества директорий, а чтобы причины изменения и зависимости оставались понятными.

У HTTP-адаптера своя ответственность: принять проверенный ввод, вызвать сценарий и представить результат как статус и публичный DTO. Сервис описывает сценарий приложения, а репозиторий или другой адаптер занимается конкретным способом хранения. Это не требует микросервисов: все части могут жить в одном процессе и одном небольшом проекте.

Тонкий HTTP-слой

Контроллер не должен знать внутренние детали SQL или создавать новое подключение к БД на каждый запрос. Он также не должен возвращать сырую запись пользователя только потому, что она уже есть в памяти. Явный публичный ответ удерживает контракт стабильным и предотвращает утечку приватных полей.

JavaScript
function createUsersRouter(service) {
  const router = express.Router();
  router.get('/:id', async (req, res) => {
    const user = await service.findPublic(req.params.id);
    if (!user) return res.status(404).json({ error: 'missing' });
    res.json(user);
  });
  return router;
}

Здесь HTTP-слой знает, как из результата сценария сделать ответ, но не знает, где именно живут записи. Сам service передаётся фабрике извне. Это делает настройку приложения явной и позволяет тесту предоставить контролируемую зависимость без переписывания production-кода.

Сценарий и репозиторий

Сервис принимает данные и зависимости предметного действия. Он может проверить доменное правило, запросить репозиторий и построить публичную проекцию. Репозиторий предоставляет узкий набор операций хранения. Не обязательно создавать универсальный generic CRUD на все таблицы: несколько понятных методов часто лучше отражают реальную задачу.

JavaScript
function createUsersService(repository) {
  return {
    async findPublic(id) {
      const user = await repository.find(id);
      return user ? { id: user.id, name: user.name } : null;
    },
  };
}

Сервис не получает res и не выбирает CSS-состояние кнопки. Поэтому один и тот же сценарий можно проверить без HTTP или вызвать из другого доверенного адаптера. При этом не стоит выносить каждое простое выражение в отдельный сервис: граница должна иметь понятную ответственность и полезный контракт.

Точка сборки и время жизни

Зависимости связывают в одном понятном месте: фабрике приложения или bootstrap-модуле. Там создают конфигурацию, клиент БД, репозитории, сервисы и routers. Общий пул БД может жить долго, а контекст подтверждённого пользователя относится к запросу. Не смешивайте эти сроки жизни в глобальной переменной «currentUser».

JavaScript
function createApp(repository) {
  const app = express();
  const service = createUsersService(repository);
  app.use('/users', createUsersRouter(service));
  return app;
}

Такой dependency injection не требует большого контейнера. Это обычная передача зависимостей, которая позже станет основой понимания NestJS. Контейнер автоматизирует создание и разрешение графа, но не решает за разработчика, как правильно разделить бизнес-возможности.

Ошибки и транзакционные границы

Ожидаемое отсутствие записи может быть результатом сценария, а неожиданный отказ адаптера — исключением. Не позволяйте HTTP-статусам просачиваться во все низкоуровневые утилиты без необходимости. При нескольких связанных записях именно сценарий должен понимать их атомарность и вызывать подходящую транзакционную операцию адаптера.

Тестовый in-memory репозиторий не доказывает все свойства настоящей БД: уникальность, блокировки и особенности транзакций проверяются отдельно на реальной инфраструктуре. Подмена полезна для логики сценария, но не должна создавать ложную уверенность в поведении хранилища. Разные тесты покрывают разные границы.

Практика внедрения поиска

В упражнении создаётся сервис над переданным массивом и маршрут GET /users/:id. Ответ содержит только id и name. Дополнительный secret во входных данных не должен появиться в JSON. Пустой список или неизвестный id дают согласованный 404. Это маленькая проверка одновременно архитектурной границы и публичного представления.

После решения замените массив на другой репозиторий, не меняя код router. Затем протестируйте сервис напрямую и отдельно проверьте HTTP-ответ. Для итогового API заметок предложите структуру по возможностям: notes, auth и общая инфраструктура. Она должна помогать найти сценарий, а не требовать обхода десятка абстрактных слоёв для одного понятного действия.

Загружаем редактор…

Музыка: Reverie — Scott Buckley · CC BY 4.0