19 KiB
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, группируй файлы по ответственности:
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 проходи владельцев в порядке:
- Найди 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 на каждом уровне.
Каждый 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 ошибок. Точные ограничения — HTTP, Data, Security, Integrations.
Сценарии и размер сервисов
Разделяй независимые операции, а не строки. 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.
Logging
Единственный logger — nestjs-pino; application events используют существующий
StructuredLogger и
request context. Выбирай стабильное имя события и разрешённые scalar fields.
Wrapper отбрасывает неизвестные поля, но это не разрешение помещать secret
в message/stack: review sensitive logging обязателен.
Не добавляй второй logger или сырой request/body/error payload.
Источник rationale — ADR 008;
при 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.