275 lines
22 KiB
Markdown
275 lines
22 KiB
Markdown
# 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](../package.json); source code
|
||
определяет фактическую реализацию.
|
||
|
||
```text
|
||
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](conventions/data.md).
|
||
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 доказывают разные части поведения.
|
||
Локальные источники выбираются через [Источники](../AGENTS.md#источники).
|
||
|
||
## 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,
|
||
группируй файлы по ответственности:
|
||
|
||
```text
|
||
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](conventions/testing.md).
|
||
|
||
Без зафиксированной необходимости не вводи `*.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](conventions/data.md); use case получает данные через repository и сам
|
||
Prisma queries не выполняет. Entity/interface на каждую таблицу не требуется.
|
||
DTO остаются транспортными runtime classes; существующее использование DTO
|
||
на application boundary допустимо по [HTTP API](conventions/http-api.md).
|
||
|
||
Сохраняй существующий error contract: HTTP exceptions остаются на application/
|
||
transport boundary. Чистому расчёту не нужен новый framework ошибок.
|
||
Mapping и stable codes — по [Errors](conventions/error-handling.md).
|
||
Точные ограничения — [HTTP](conventions/http-api.md), [Data](conventions/data.md),
|
||
[Security](conventions/security-auth.md), [Integrations](conventions/integrations.md).
|
||
|
||
## Сценарии и размер сервисов
|
||
|
||
Разделяй независимые операции, а не строки. Пример: `BankCardsService` владеет
|
||
картами, `BankOperationsService` — журналом/переводами, `GetBankSummaryUseCase` —
|
||
сводкой через repository и чистый расчёт. Не разрывай денежную transaction,
|
||
общий cache/limiter или pending promises при extraction.
|
||
|
||
Расширение abstractions — по [change scope](conventions/engineering.md#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](conventions/engineering.md#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](conventions/observability.md).
|
||
|
||
## Проверка
|
||
|
||
`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.
|