Перейти к содержанию

Providers и разделение HTTP, правил и SQL

Модуль задаёт граф, но обязанности классов должны оставаться понятными внутри него. В «Диспетчере» controller переводит HTTP-вход в вызов операции, TicketsService представляет доступные действия, TicketsRepository реализует чтение и запись PostgreSQL. Рассмотрим providers и constructor injection на этой цепочке. Результат — структура, в которой код запроса не растворяет предметную задачу, а транспорт не создаёт соединение вручную и не решает, когда выполнять COMMIT.

Provider — объект, который контейнер Nest создаёт и предоставляет другим классам. В простой форме ключом зависимости является сам класс. Injectable даёт Nest metadata для его создания и зависимостей. Это не означает, что любой такой класс автоматически доступен всем модулям: правила providers/imports/exports из предыдущей главы сохраняются. Декоратор и регистрация дополняют друг друга. Один отвечает за описание создаваемого класса, другой — за его место в конкретном графе приложения.

Сервис как предметная поверхность

Ниже полный src/tickets.service.ts. Он пока выглядит тонким: три метода делегируют repository, а patch передаёт actor.id. Это нормальная форма для небольшой лаборатории. Мы не добавляем искусственные бизнес-правила только ради увеличения service. Предметная поверхность уже полезна: controller не знает имя таблицы, mapping updated_at или транзакционный client. Если правила перехода статусов появятся позднее, именно здесь можно будет определить их взаимодействие с проверкой текущей версии.

import { Injectable } from '@nestjs/common';
import { TicketsRepository } from './tickets.repository.js';
import type { ListTicketsDto, PatchTicketDto } from './dto.js';
import type { Actor } from './model.js';
@Injectable()
export class TicketsService {
  constructor(private readonly repository: TicketsRepository) {}
  list(filter: ListTicketsDto) { return this.repository.list(filter); }
  get(id: string) { return this.repository.get(id); }
  patch(id: string, value: PatchTicketDto, actor: Actor) { return this.repository.patch(id,value,actor.id); }
}

Service получает repository через constructor. Nest использует metadata и регистрацию в TicketsModule, чтобы подставить зависимость. Поле private readonly запрещает обычную замену ссылки после создания объекта и не публикует repository в качестве метода service. Методы list/get/patch выражают нужные операции, а не универсальную SQL-функцию «выполни любой текст». Такой интерфейс легче читать со стороны controller и сложнее случайно использовать для обхода принятого предметного договора.

Actor приходит после guard и передаётся явно в patch. Service не должен принимать actorId из JSON body пользователя. Иначе клиент смог бы приписать запись другому диспетчеру, изменив обычное поле запроса. В лаборатории Actor создаётся из публичного токена и содержит фиксированное demo-id. Эта модель учебная, но направление доверия правильное: сведения об удостоверенной стороне берутся из серверного контекста, а содержание редактирования — из проверенного DTO.

Репозиторий и ресурс

Repository получает Database, который владеет pool. Методы repository знают SQL, строки результата и функцию ticket, переводящую их в wire-модель. Database не должен сам понимать title, status или конфликт rq-101. Его задача — ресурс соединения и завершение pool. Если смешать эти обязанности, любую новую предметную таблицу пришлось бы добавлять в общий инфраструктурный класс. Разделение сохраняет Database небольшим, а SQL-операцию привязывает к заявкам.

Конечный repository использует pg непосредственно. Мы не вводим ORM и не называем написанные SQL-файлы автоматическими миграциями. Такой выбор позволяет видеть конкретный SELECT FOR UPDATE и один client внутри транзакции. Для производственного проекта может быть выбрана другая библиотека, однако замена не должна менять внешний JSON случайным образом. Service и controller останутся связаны с договорными Ticket/DTO, а repository может адаптировать способ хранения при сохранении операции.

По умолчанию этот граф не хранит пользователя в mutable-поле service. Одни и те же provider-экземпляры обслуживают разные запросы, поэтому присваивание this.currentActor = actor перед await создало бы возможность смешения контекстов. Вместо этого actor передаётся параметром конкретного patch. Это обычное правило работы с разделяемыми объектами: временные данные операции живут в локальных переменных и аргументах. Singleton service полезен для методов и зависимостей, но не для случайного хранения текущего запроса.

Замена зависимости

Nest поддерживает custom providers: useValue, useFactory и useClass позволяют определить другой способ получения объекта по ключу. В нашей конечной версии они используются для APP_FILTER и APP_INTERCEPTOR, а repository предоставлен обычным классом. Если нужна отдельная in-memory серверная лаборатория, можно определить абстрактный ключ repository и подставить реализацию. Но такой режим не добавлен: самостоятельность Nest обеспечивается собственным seed и контрактом, а локальный клиентский fixture имеет другую цель.

Не создавайте new Database(config) внутри метода repository. Это обходит lifecycle контейнера и способен открыть новый pool на каждую операцию. При нескольких запросах число подключений окажется связано с частотой HTTP, а завершение объекта перестанет быть очевидным. Constructor dependency фиксирует владельца ресурса на уровне приложения. Max pool равен пяти как параметр лаборатории, но это не рассчитанная production-ёмкость: нагрузка, число процессов и лимиты PostgreSQL не измерялись.

Тонкий service не требует немедленного добавления ещё одного слоя interface-файлов. Когда конкретная задача имеет одну реализацию, прямой constructor понятен читателю. Абстракция становится полезной, если действительно нужны варианты или отдельный стабильный договор между пакетами. Важно не путать число классов с качеством архитектуры. Мы уже разделили обязанности, которые имеют разные причины изменения, и можем объяснить каждую на исходных rq-101…103 без вымышленных правил согласования.

Для мысленной проверки проследите patch rq-101: controller получает validated DTO и req.actor, service передаёт их repository, repository сравнивает version и пишет историю с actorId. Теперь представьте замену title сообщения пользователю. Она относится к filter/UI и не требует менять pool. Изменение SQL-имени updated_at относится к repository mapping, а не к форме запроса. Такой анализ показывает практический смысл выбранных границ лучше, чем самостоятельное перечисление decorators.

Следующий шаг оформит маршруты controller. Providers уже дают ему нужную зависимость и оставляют данные запроса вне общего mutable-состояния. Ни создание graph, ни SQL, ни параллельные обращения не выполнялись в сессии; речь идёт о написанной структуре и ожидаемом порядке операции. Читатель сможет проверить её после подготовки окружения, используя один завершённый архив и заранее определённый wire-контракт.

Статус примера — not_run. Зависимости, приложение, SQL, HTTP и тесты не запускались. Описанные ответы и изменения интерфейса ожидаемые; чтение документации не является проверкой выполнением. Полная конечная версия находится в архиве исходников, а её ограничения и действия читателя — в README и CONTRACT.md.

Перейдите к оглавлению серии или общему маршруту интерактивной веб-разработки. Механизм этой главы сверяется с официальным первичным источником; конкретные версии и непроверенные части перечислены в договоре курса.