Files
backend/.ai/architecture.md
T
ksv741 2d14081d27
Backend security / security (push) Successful in 8s
Backend security / security (pull_request) Canceled after 0s
Backend verify / verify (push) Canceled after 9s
Backend verify / verify (pull_request) Canceled after 4s
Conventional commits / commits (pull_request) Successful in 31s
refactor: enforce enum machine codes
Use owner enums for error and T-Bank protocol values with ESLint regression checks.

Preserve open wire fallbacks and keep protocol imports independent of DI and Prisma.

Verified npm ci, verify, 226 unit tests, 262 isolated e2e tests and unchanged OpenAPI.
2026-09-28 13:18:06 +03:00

22 KiB
Raw 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. Самостоятельные use cases получают данные через concrete repositories; существующие связные CRUD/transaction services сохраняют прямые queries по 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; StructuredLogger добавляет request context и ограничивает поля событий. 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. Существующие связные CRUD/transaction services могут сохранять прямые queries по Data; use case получает данные через repository и сам Prisma queries не выполняет. 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. Смысл boundaries проверяется review: backend не имеет отдельного автоматического FSD/DI architecture checker. Existing tests и build не доказывают правильный owner.