# Backend architecture ## Module-first organization Backend — domain-oriented modular monolith с лёгким разделением по ответственности. `src/modules` группирует код по бизнес-возможности: `auth`, `banking/bank-cards`, области `investments`. В инвестициях отдельные Nest modules владеют `brokerage-accounts`, `portfolio`, `operations`, `income`, `upcoming-events`, `instruments`, `tbank-connection`. `instruments/stocks` и `instruments/bonds` — slices внутри одного модуля. ### 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 не требуются для этой организации. В текущем коде слои используют `auth`, `bank-cards`, `portfolio`, `operations` и slices `instruments/stocks`, `instruments/bonds`. Общие application helpers инструментов находятся в `instruments/application`; `instruments.module.ts` компонует оба slice. `stock-position.ts` и `instrument-position.ts` формируют результат из portfolio DTO; `instrument-market.ts` обрабатывает внешние данные и ошибки Nest. Это application code, а не независимая domain model. `brokerage-accounts`, `income`, `upcoming-events`, `tbank-connection` остаются компактными плоскими modules. Наличие tests само по себе не требует слоёв. ## Поиск и создание файла При работе над 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](backend-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` — отдельный публичный вход для protobuf conversion: импорт арифметики не загружает DI/Prisma. Внутри модуля используй прямые локальные импорты. Tests могут импортировать внутренний owner для проверки поведения и mocks. Новый export требует текущего потребителя; controller и persistence остаются приватными. Не добавляй `index.ts` на каждом уровне. Каждый 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](backend-data.md); use case получает данные через repository и сам Prisma queries не выполняет. Entity/interface на каждую таблицу не требуется. DTO остаются транспортными runtime classes; существующее использование DTO на application boundary допустимо по [HTTP API](backend-http-api.md). Сохраняй существующий error contract: HTTP exceptions остаются на application/ transport boundary. Чистому расчёту не нужен новый framework ошибок. Точные ограничения — [HTTP](backend-http-api.md), [Data](backend-data.md), [Security](security-auth.md), [Integrations](backend-integrations.md). ## Сценарии и размер сервисов Разделяй независимые операции, а не строки. `BankCardsService` отвечает за карты, `BankOperationsService` — журнал/переводы/возвраты, `ExpenseCategoriesService` — категории, `GetBankSummaryUseCase` — сводку. `BankCardsRepository` сохраняет общий transaction retry и owner lookup; денежные транзакции не разрываются. Чистые расчёты банковских карт находятся в `domain` этого модуля. У инструментов `StockCatalogService` / `BondCatalogService` хранят идентичность листинга и общий кэш slice; market service — котировки/свечи, income service — выплаты/личные позиции. `GetStockCompanyUseCase` владеет сведениями об эмитенте. Ключи, TTL и pending promises разделяют прежний экземпляр кэша; extract не должен менять generation. Portfolio и operations сохраняют связную координацию снимков/страниц и кэшей. Расчёты портфеля находятся в `portfolio/domain`; преобразование брокерских позиций — в `tbank-portfolio.mapper.ts`. `bond-money.mapper.ts` преобразует ответ клиента и не является value object. Выбирай обычную функцию для чистого вычисления; repository contract — только для реальной границы. CQRS/buses/events, BaseRepository/BaseService, DI token для каждого класса, mapper/presenter для каждого DTO и новые dependencies не нужны для структурного переноса. ## 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 Finite business vocabulary must be modeled explicitly; incidental implementation literals stay inline. Это относится к error codes, statuses, operation/account/ instrument/transaction types, permissions, roles, feature states и бизнес-режимам. Перед новым определением найди existing owner/type/value. В branching и comparisons используй символическое значение владельца; не повторяй raw domain literals. Храни словарь у domain/module/entity с соблюдением границ слоёв, без глобальной свалки `constants.ts`. Сохраняй принятый pattern; для нового project-owned словаря без подходящего аналога используй `as const` с derived union: ```ts const BankErrorCode = { InvalidDate: "BANK_INVALID_DATE", ImmutableOperation: "BANK_IMMUTABLE_OPERATION", } as const; type BankErrorCode = (typeof BankErrorCode)[keyof typeof BankErrorCode]; // Вместо code === "BANK_INVALID_DATE": if (code === BankErrorCode.InvalidDate) { /* existing handling */ } ``` Значимые numeric policies (timeout, retry, page size, threshold, coefficient) называй по смыслу и единицам: `REQUEST_TIMEOUT_MS`, `CACHE_TTL_SECONDS`, `MAX_RETRY_COUNT`, `PAGE_SIZE`. Оставляй inline UI labels в принятом месте, log messages, индексы, `index + 1`, `Math.max(value, 0)`, `page ?? 1`, `percentage * 100` и очевидные локальные детали без semantic benefit. При неясном owner/смысле оставь значение и укажи ограничение в review. Не мигрируй корректные existing enum/union styles массово. Не добавляй ради правила factories, registries, schemas, loaders, abstraction layers, dependencies, custom lint/AST rules, generation/validation scripts или тесты существования констант. Semantic review проверяет ownership, единое написание и неизменность wire values; обычные behavior tests проверяют поведение. OpenAPI enum metadata и runtime validation должны читать тот же vocabulary, что application/domain. Например, `BankOperationKind` принадлежит bank-cards/domain, а DTO получает допустимые значения через `Object.values(BankOperationKind)`. Frontend переиспользует generated OpenAPI types/values по возможностям генератора, а не определяет независимый backend-owned словарь. Внешние/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 Единственный logger — nestjs-pino; application events используют существующий [StructuredLogger](../../src/common/logging/structured-logger.service.ts) и request context. Выбирай стабильное имя события и разрешённые scalar fields. Wrapper отбрасывает неизвестные поля, но это не разрешение помещать secret в `message`/`stack`: review sensitive logging обязателен. Не добавляй второй logger или сырой request/body/error payload. Источник rationale — [ADR 008](../../../docs/adr/008-pino-backend-logging.md); при secrets/user data читай также Security. ## Проверка `npm run typecheck:app`, `npm run lint`, targeted Jest по изменённому use case. Смысл boundaries проверяется review: backend не имеет отдельного автоматического FSD/DI architecture checker. Existing tests и build не доказывают правильный owner.