- 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
5.4 KiB
5.4 KiB
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-оптимизация в PortfolioService —
fetchShareBatch/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
- Shared envelope DTO — вынести
ApiResponseMetaи один genericEnvelopeDto<T>вcommon/dto/, убрать дублирование. Унифицировать формат ответа screener'а под общий envelope. - Разделить MoexClientService — выделить
MoexSecuritiesClient,MoexMarketDataClient,MoexCandlesClient— каждый со своим набором методов. - Убрать inconsistency shares vs bonds —
getShare()должен возвращатьApiEnvelopePayloadкак иgetBond().
🟡 Medium
- Domain exception hierarchy — создать
BaseDomainException→MoexApiException,SecurityNotFoundException,PortfolioAccessDeniedException,TBankApiException. Добавить соответствующие фильтры вHttpExceptionFilter. - Screener — server-side пагинация — кешировать полный результат screener'а отдельным TTL.
- Health check прокачка — добавить проверки Prisma (
db.ping()), MOEX (/health), T-Bank gRPC connectivity. - Prisma JSON → typed JSON — использовать строки с
JSON.parseв геттерах или перейти на отдельные таблицы. - Отвязать
securities/от прямого импортаCacheModule— раз он@Global(), убрать лишний импорт.
🟢 Low / Nice to have
- RequestLoggingMiddleware — перевести на
configure()вAppModuleдля единообразия. - Refactor
tbank/— выделитьbroker-analyticsиbroker-syncв отдельные модули, если будут расти. - Swagger schema object для обёртки — глобально настроить OpenAPI для автоматической обёртки
{ data, meta }. - Circuit breaker — вынести в декоратор — обобщить в
@CircuitBreaker()декоратор.