Files
backend/.ai/architecture.md
ksv741 e3ecb8c15e
Backend verify / commits (pull_request) Successful in 1m29s
Backend verify / security (pull_request) Successful in 1m5s
Backend verify / verify (pull_request) Successful in 6m13s
Backend verify / commits (push) Skipped
Backend verify / security (push) Successful in 1m11s
Backend verify / verify (push) Failing after 7m34s
refactor: enforce repository boundary in application (WealthLedger/backend#171)
2026-11-04 22:29:01 +03:00

22 KiB
Raw Permalink Blame History

Backend architecture

Устройство и data flow

NestJS + TypeScript modular monolith. Composition root AppModule подключает бизнес-модули, validated configuration, Prisma и Pino. Общая configureApp задаёт /api/v1, CORS/cookies, validation, request logging и error filter. Точные versions/scripts — в package.json; source code определяет фактическую реализацию.

HTTP controller → application service/use case → pure domain rules
                            ↓
                repository/data access → Prisma → SQLite
                            ↓
                integration client → T-Bank gRPC / MOEX / NCR HTTP

modules/auth, modules/banking/bank-cards и области modules/investments владеют пользовательскими возможностями. Сложные модули разделены на presentation/application/domain/infrastructure; компактные остаются плоскими. *.module.ts — Nest DI, *.api.ts — используемые cross-module exports. Application использует конкретные providers; обязательных ports на каждый класс нет.

Stocks/bonds — slices одного instruments module: catalog владеет identity/cache, market — котировками, income — выплатами/личными позициями. Общая orchestration в instruments/application использует DTO/provider records и не становится pure domain из-за extraction в функцию. Boundary mappers преобразуют внешние records; чистые расчёты portfolio/banking остаются у domain owner.

PrismaService владеет DB lifecycle. Application services/use cases получают данные через concrete repositories; запросы и CAS живут в infrastructure по data convention. Money — currency + decimal string; расчёты используют точные общие helpers.

Auth управляет identity/session; TbankAccessService проверяет доступ к счёту и connection generation. Integration clients владеют protocol/deadline/retry; provider records преобразуются перед чистыми domain calculations. Пользовательский tbank-connection не владеет низкоуровневым gRPC client.

Nest DTO/metadata → npm run openapi:generate → игнорируемый Git openapi.json для публичных consumers. Problem Details фильтруются общей HTTP boundary; native Pino добавляет request context, StructuredLogger ограничивает поля событий. Jest unit tests, изолированные SQLite/HTTP suites и OpenAPI parity доказывают разные части поведения. Локальные источники выбираются через Источники.

Module-first organization

Сначала выбери business owner в modules/, затем ответственность внутри него.

Progressive structure

Три уровня организации: business module → responsibility layer → technical grouping при реальной необходимости. Компактный module с понятными controller/DTO/service может оставаться плоским. Ориентир 5–7 файлов не является порогом: учитывай число ответственностей, наличие domain/repository/use cases и удобство просмотра каталога.

Когда в module смешаны несколько services, transport, расчёты и persistence, группируй файлы по ответственности:

feature/
├── feature.module.ts
├── feature.api.ts          # только при существующих внешних потребителях
├── presentation/           # HTTP, DTO, guards
├── application/            # сценарии, orchestration, application input
├── domain/                 # чистые бизнес-правила и расчёты
└── infrastructure/         # persistence, framework и boundary mapping

Создавай только нужные слои. В корне сложного module оставляй composition и публичную boundary; production code и colocated tests размещай в слое владельца. Внутри business slice действует то же правило, без отдельного Nest module ради папок. controllers/, dto/, services/, use-cases/, persistence/, mappers/ допустимы только внутри слоя, когда множество однотипных файлов мешает навигации. Для одного controller или пары DTO дополнительная поддиректория не нужна. Пустые папки, новые barrels, CQRS и interfaces не требуются для этой организации.

Поиск и создание файла

При работе над feature проходи владельцев в порядке:

  1. Найди business module.
  2. Прочитай его *.module.ts (для slice — composition родительского module).
  3. Проверь presentation/: controller и transport contract.
  4. Проследи вызовы в application/: сценарии и services.
  5. Проверь используемые правила в domain/.
  6. Проверь infrastructure/: repository и технические реализации.
  7. По обнаруженным зависимостям переходи в integrations/ и common/.

В плоском module классифицируй файлы по suffix и следуй их ответственности. Отсутствующий слой пропускай. Если business owner известен, начинай локально, а не с repository-wide search.

Rule of locality: code that belongs to one business capability stays inside that capability unless it is cross-cutting. Например, расчёты карт принадлежат bank-cards/domain, даже если реализованы обычными функциями.

Перед созданием файла определи:

  1. Какой business module владеет изменением? Сначала owner, затем common при подтверждённой общей потребности.
  2. Какова ответственность: HTTP/DTO/guard → presentation; сценарий/orchestration → application; чистое business rule/calculation → domain; persistence или module-specific technical implementation → infrastructure; внешний протокол → integrations.
  3. Какой approved suffix из таблицы соответствует этой ответственности?
  4. Какой слой владеет файлом? В компактном плоском module сохрани размещение рядом с owner; в слоистом используй соответствующий слой.
  5. Достаточно ли colocate файл рядом с существующими? Level 3 grouping создавай только при заметном улучшении навигации по множеству однотипных файлов.

Если owner или роль не определены, сначала установи ответственность. Generic utils.ts, helpers.ts, manager.ts, handler.ts и arbitrary directory её не заменяют.

Naming & File Suffix Convention

Имена файлов — kebab-case. File suffix определяет responsibility, directory определяет architectural layer. Размещение в таблице предпочтительно для слоистого module; компактный плоский module сохраняет файлы в корне. Cross-module consumers во всех строках ограничены публичной границей ниже.

Suffix Когда использовать Где Кто зависит Когда не использовать
*.controller.ts HTTP endpoint / transport presentation Nest wiring DB, расчёты, SDK, сложная оркестрация
*.dto.ts Request/response, validation, OpenAPI presentation Controller; application boundary при совпадении shape Универсальная domain model
*.use-case.ts Один самостоятельный application scenario application Controller, другой orchestrator Prisma queries, Request/Response, SDK implementation
*.service.ts Конкретная cohesive reusable capability рядом с owner; application/domain/integration по роли Сценарии и services; domain — только от чистого domain service Свалка несвязанных сценариев
*.repository.ts Persistence queries и согласованные DB reads/writes infrastructure Application service/use case Domain calculations, HTTP request, external transport
*.client.ts Низкоуровневый внешний HTTP/gRPC/SDK integrations или infrastructure Application orchestration, boundary mapper Пользовательский lifecycle и domain rules
*.mapper.ts Meaningful conversion между моделями обычно infrastructure у преобразуемой boundary Код соответствующей boundary Формальное копирование нескольких одинаковых полей
*.policy.ts Cohesive decision rule domain/application/integration по входным данным Владелец правила; domain — от чистой policy Persistence, transport, generic strategy
*.guard.ts Nest guard presentation Nest/controller Основная account authorization и финансовые правила
*.strategy.ts Framework strategy, например JWT infrastructure Framework wiring Обычный service без strategy contract
*.module.ts Nest imports/providers/controllers/exports module root Composition root, importing module Бизнес-логика
*.types.ts Малая группа связанных TS types рядом с owner Потребители модели на разрешённых слоях Глобальные types всего module, runtime validation
*.api.ts Явный список публичных exports root конкретного module/integration Другие modules DTO/client/service implementation или generic API logic
*-input.ts Application input: нормализация и проверка формы, отличающейся от wire DTO application Application code Копия DTO под новым именем; вариант *.input.ts
*.calculations.ts, *.rules.ts или предметное имя Чистые бизнес-расчёты, value semantics domain Application и domain Nest/Prisma/HTTP/SDK зависимости
*.spec.ts Существующая unit/component проверка поведения рядом с owner/slice Test runner Архитектурные проверки папок и trivial wiring

bank-card-input.ts использует выбранный *-input.ts: переводит Money в копейки, нормализует значения и проверяет application input. DTO не дублируется отдельным Input type. rub-money.ts — domain-правила RUB; bond-money.mapper.ts — преобразование ответа клиента. Эти роли не определяются словом money в имени.

Технические framework suffixes (*.filter.ts, *.middleware.ts, *.decorator.ts, *.config.ts, *.validation.ts) сохраняют соответствующие существующие роли в common/config. tbank.crypto.ts — локальная реализация шифрования токена; имя не является разрешением создавать generic crypto layer. HTTP/e2e naming и расположение — в Testing.

Без зафиксированной необходимости не вводи *.helper.ts, *.utils.ts, *.manager.ts, *.processor.ts, *.handler.ts, *.provider.ts, *.factory.ts. Новая naming convention сначала получает конкретную роль и обоснование здесь. Для data access выбран только *.repository.ts, не persistence/store/dao.

Публичные границы

*.module.ts задаёт DI boundary; *.api.ts перечисляет используемые другими модулями providers, функции и типы. Внешний production import идёт через этот API или файл Nest module. Чистые integrations/tbank/decimal.ts, integrations/tbank/tbank-protocol.ts, integrations/tbank/tbank-error-code.ts и modules/investments/operations/domain/operations-sync-plan.ts — отдельные публичные входы для protobuf conversion, protocol/error vocabulary и interval rules: импорт этих значений и расчётов не загружает DI/Prisma. Внутри модуля используй прямые локальные импорты. Tests могут импортировать внутренний owner для проверки поведения и mocks. Новый export требует текущего потребителя; controller и persistence остаются приватными. Не добавляй index.ts на каждом уровне.

Circular dependencies устраняй изменением ownership/composition. forwardRef() не является стандартным исправлением boundaries; исключение требует конкретной неизбежной связи и проверки DI initialization.

Каждый provider регистрируется у одного владельца. Потребители импортируют его module, а не повторяют provider: это сохраняет единственный кэш и лимитер. AuthModule экспортирует guard; наличие JWT не заменяет account access check.

  • income читает операции через operations.api.ts.
  • instruments получает личные позиции через portfolio.api.ts.
  • tbank-connection использует T-Bank integration и публичный сброс портфеля.
  • portfolio и другие инвестиционные модули используют TbankModule, который не зависит от business modules. Так управление подключением не образует цикл.
  • investment-calendar.ts и account-reference.dto.ts разделяются только инвестиционными областями; это не глобальная библиотека бизнес-правил.

Ответственность и направление зависимостей

Владелец Ответственность
Controller / presentation Route, verified identity, transport DTO, validation, status/cookie; вызов сценария
Use case / application service Оркестрация, business validation, доступ и согласованность результата
Domain Чистые правила и расчёты; без NestJS, Prisma, HTTP/Swagger и внешних SDK
Persistence / infrastructure Запросы, транзакции, технические реализации границ
src/integrations/{tbank,moex,ncr} Внешние протоколы, декодирование, retry/deadline/cache; без зависимостей от HTTP DTO business modules
src/prisma Общий DB provider и lifecycle; не generic repository

Основной вызов: presentation → application → domain; application использует конкретные infrastructure providers, зарегистрированные module. Infrastructure может использовать application types/input и domain rules; новые interfaces ради инверсии зависимостей не обязательны. Domain не импортирует presentation или infrastructure. Application не строит Prisma queries: use case и связный CRUD service обращаются к репозиторию по Data. Entity/interface на каждую таблицу не требуется. DTO остаются транспортными runtime classes; существующее использование DTO на application boundary допустимо по HTTP API.

Сохраняй существующий error contract: HTTP exceptions остаются на application/ transport boundary. Чистому расчёту не нужен новый framework ошибок. Mapping и stable codes — по Errors. Точные ограничения — HTTP, Data, Security, Integrations.

Сценарии и размер сервисов

Разделяй независимые операции, а не строки. Пример: BankCardsService владеет картами, BankOperationsService — журналом/переводами, GetBankSummaryUseCase — сводкой через repository и чистый расчёт. Не разрывай денежную transaction, общий cache/limiter или pending promises при extraction.

Расширение abstractions — по change scope.

Common

common содержит общий error filter, logging, API decorators, тип Money, его HTTP MoneyDto и точную decimal-арифметику, используемые несколькими областями. Код одного модуля принадлежит этому модулю: например, RUB/kopecks rules — банковским картам, protobuf units/nano conversion — T-Bank integration. Domain использует money.types.ts, а не Swagger DTO; это общая форма денег для карт и портфеля. Давай функциям предметные имена; не создавай глобальные utils.ts/helpers.ts.

Domain literals and magic values

Общий выбор symbolic values и числовых policies — Domain values. Сохраняй strict TS и explicit boundary types. Для недоверенного input выбирай unknown + runtime narrowing; ESLint запрещает explicit any в domain/application model, тестах и конфигурации TypeScript. declare допустим для заполняемых извне DTO fields, не для сокрытия неинициализированного owned state.

OpenAPI enum metadata и runtime validation должны читать тот же vocabulary, что application/domain. Например, BankOperationKind принадлежит bank-cards/domain, а DTO получает допустимые значения через Object.values(BankOperationKind). Публичные enum values входят в OpenAPI artifact. Внешние/generated enums сохраняй; generated Prisma/protocol code вручную не меняй.

@typescript-eslint/no-magic-numbers включён как error; базовое ESLint rule отключено в пользу TS-версии. Общего списка ignore для чисел и bigint нет: значения, проверяемые правилом, получают предметные имена у владельца. Исключения для индексов, default values, enum/type declarations и readonly properties заданы в ESLint config. Test fixtures/expected values остаются inline, остальные lint правила продолжают действовать. Numeric policy получает предметное имя и единицы; Machine codes и protocol values задаются строковыми enum у владельца. no-restricted-syntax запрещает inline-коды и значения известных protocol families в production TS; no-unsafe-enum-comparison проверяет сравнения enum. Внешние открытые строки проверяются на boundary без cast и без потери unknown fallback. Tests сохраняют независимые literal wire assertions; обычный текст и env keys не считаются кодами. Новые словари требуют проверки владельца и расширения selectors.

Logging

События, levels, correlation и safe fields — по Observability.

Проверка

npm run typecheck:app, npm run lint, targeted Jest по изменённому use case. ESLint проверяет прямые imports: domain не зависит от transport/persistence, controllers делегируют application, use cases не импортируют Prisma. Чистые protocol inputs разрешены; existing CRUD/transaction services сохраняют DB owner. Транзитивные зависимости, DI composition и смысл ownership проверяются review: backend не имеет полного FSD/DI graph checker. Tests/build не доказывают owner.