moex-vibe/docs/research/2026-06-25-backend-audit.md
Sergey Krylov ccb1082535 refactor(backend): unify envelope DTOs and fix shares/bonds inconsistency
- Replace 4 duplicate meta DTOs (AuthResponseMetaDto, PortfolioResponseMetaDto,
  BrokerResponseMetaDto, ScreenerResponseMetaDto) with shared ApiResponseMeta
- Wrap shares getShare() in ApiEnvelopePayload (was raw object, unlike bonds)
- Remove unnecessary CacheModule import from securities module
- Update portfolio controller nullDataEnvelopeSchema to use shared ApiResponseMeta
- All 116 tests pass
2026-06-25 20:20:59 +03:00

5.4 KiB
Raw Blame History

Backend Architecture Audit 2026-06-25

Что хорошо

  • Чистая модульная структура — каждый домен в своём каталоге, NestJS-модули, DI
  • Global-модули (PrismaModule, CacheModule, MoexClientModule) — не дублируются импорты
  • CacheService.getOrFetch() — универсальный примитив кеширования, config-driven TTL
  • MOEX rate limiter + circuit breaker — p-queue + ручной CB, защищают от внешнего API
  • TBank mappers layer — proto wire type → domain type → DTO, правильная изоляция
  • Batch-оптимизация в PortfolioServicefetchShareBatch/fetchBondBatch/fetchDividendsBatch собирает всё одним MOEX-запросом
  • Спецификации и тесты — 25 spec-файлов, есть TDD-подход

Ключевые архитектурные проблемы

# Проблема Где Описание
1 Дублирование Envelope DTO Все модули Каждый модуль переопределяет свой *ResponseMetaDto / *EnvelopeDto. 6+ копий одной структуры
2 Inconsistent caching shares/shares.service.ts:getShare() (raw) vs bonds/bonds.service.ts:getBond() (envelope) Разное поведение одинаковых по смыслу методов
3 Screener — in-memory filtering securities/screener.service.ts При пустом массиве getShareMarketDataBatch([]) тянет все бумаги с MOEX и фильтрует в памяти. Не масштабируется
4 MoexClientService — God Service 11 публичных методов Один сервис делает всё: search, shares, bonds, candles, history, dividends. Нарушает SRP
5 Отсутствие доменных исключений Весь код Нет иерархии исключений (SecurityNotFoundException, PortfolioAccessDeniedException, MoexApiException) — только generic NotFoundException / ForbiddenException
6 Health check — заглушка health/ Не проверяет БД, MOEX, T-Bank. Только { status: 'ok', timestamp, uptime }
7 Prisma JSON как String targets, tags, payment, price JSON хранится как String без валидации на уровне БД. Нет типизированных JSON-полей
8 tbank/ — перегруженный модуль 22 файла, 8 сервисов Один модуль содержит gRPC клиент, мапперы, CRUD, аналитику, синхронизацию. Можно разбить
9 Auth глобальные гарды JwtAuthGuard + RolesGuard как APP_GUARD Неявная защита всех эндпоинтов. Приходится использовать @Public() для открытых
10 RequestLoggingMiddleware Подключён через .use(), а не через configure() Работает, но не идёт через DI и не является частью модуля

Рекомендации

🔴 Critical

  1. Shared envelope DTO — вынести ApiResponseMeta и один generic EnvelopeDto<T> в common/dto/, убрать дублирование. Унифицировать формат ответа screener'а под общий envelope.
  2. Разделить MoexClientService — выделить MoexSecuritiesClient, MoexMarketDataClient, MoexCandlesClient — каждый со своим набором методов.
  3. Убрать inconsistency shares vs bondsgetShare() должен возвращать ApiEnvelopePayload как и getBond().

🟡 Medium

  1. Domain exception hierarchy — создать BaseDomainExceptionMoexApiException, SecurityNotFoundException, PortfolioAccessDeniedException, TBankApiException. Добавить соответствующие фильтры в HttpExceptionFilter.
  2. Screener — server-side пагинация — кешировать полный результат screener'а отдельным TTL.
  3. Health check прокачка — добавить проверки Prisma (db.ping()), MOEX (/health), T-Bank gRPC connectivity.
  4. Prisma JSON → typed JSON — использовать строки с JSON.parse в геттерах или перейти на отдельные таблицы.
  5. Отвязать securities/ от прямого импорта CacheModule — раз он @Global(), убрать лишний импорт.

🟢 Low / Nice to have

  1. RequestLoggingMiddleware — перевести на configure() в AppModule для единообразия.
  2. Refactor tbank/ — выделить broker-analytics и broker-sync в отдельные модули, если будут расти.
  3. Swagger schema object для обёртки — глобально настроить OpenAPI для автоматической обёртки { data, meta }.
  4. Circuit breaker — вынести в декоратор — обобщить в @CircuitBreaker() декоратор.