21 KiB
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 проходи владельцев в порядке:
- Найди business module.
- Прочитай его
*.module.ts(для slice — composition родительского module). - Проверь
presentation/: controller и transport contract. - Проследи вызовы в
application/: сценарии и services. - Проверь используемые правила в
domain/. - Проверь
infrastructure/: repository и технические реализации. - По обнаруженным зависимостям переходи в
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, даже если реализованы обычными функциями.
Перед созданием файла определи:
- Какой business module владеет изменением? Сначала owner, затем common при подтверждённой общей потребности.
- Какова ответственность: HTTP/DTO/guard → presentation; сценарий/orchestration → application; чистое business rule/calculation → domain; persistence или module-specific technical implementation → infrastructure; внешний протокол → integrations.
- Какой approved suffix из таблицы соответствует этой ответственности?
- Какой слой владеет файлом? В компактном плоском module сохрани размещение рядом с owner; в слоистом используй соответствующий слой.
- Достаточно ли 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 — отдельный
публичный вход для protobuf conversion: импорт арифметики не загружает 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; отключённый lint no-explicit-any не делает
any нормой domain/application model. 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-версии. Исключения для очевидных чисел и их bigint форм,
индексов, default values, enum/type declarations и readonly properties заданы
в ESLint config. Test fixtures/expected values остаются inline, остальные lint
правила продолжают действовать. Numeric policy получает предметное имя и единицы;
semantic magic strings проверяются conventions/review без нового инструмента.
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.