Перейти к содержанию

Шаблон проекта бэкенда (octopus-api)

Документ описывает структуру, стек, архитектурные подходы и особенности бэкенда на базе octopus-api (NestJS на Fastify, Prisma, несколько БД).


0. Общая архитектура сервиса

Request context (контекст запроса)

В проекте нет Koa и отдельного «cancellable»-контекста. Контекст запроса реализован через Node.js AsyncLocalStorage (async_hooks): один контекст на запрос, доступный в любой точке обработки без явной передачи (по смыслу близко к ctx в Koa, но без передачи по цепочке вызовов).

Класс RequestContext (src/common/request-context.ts):

  • Хранит в AsyncLocalStorage данные текущего запроса: requestId, logger (child-логгер с requestId/traceId), опционально traceId.
  • API: get() — получить контекст; set(context) — установить (вызывается из Fastify); getLogger(), getRequestId(), getTraceId() — удобные геттеры; setAppLogger() — глобальный логгер приложения (задаётся в main.ts).

Жизненный цикл (настраивается в init-fastify.ts):

  1. onRequest (вход запроса): для несистемных путей генерируется/берётся requestId, из заголовков извлекается traceId (x-trace-id или traceparent), создаётся child-логгер, вызывается RequestContext.set({ requestId, logger, traceId }), пишется лог входящего запроса.
  2. В течение обработки любой код (контроллеры, сервисы, фильтры) может вызвать RequestContext.get() или RequestContext.getLogger() и получить контекст/логгер этого запроса — без проброса аргументов.
  3. onResponse (выход ответа): логируется завершение запроса и время ответа (при > 5 с — пометка slow), затем RequestContext.set(undefined) — контекст очищается.

Для системных путей (/metrics, /healthz, /readyz, /docs, /status) контекст не устанавливается и запрос/ответ не логируются.

Итог: один request-scoped контекст на запрос на базе AsyncLocalStorage; логгер и идентификаторы запроса доступны по всему стеку вызовов без явной передачи. Это основа сквозного логирования и трассировки в сервисе.


1. Структура проекта

Корень

Файл / папка Назначение
package.json Зависимости, скрипты, конфиг Jest
nest-cli.json Nest CLI: sourceRoot: "src", ассеты generated/**/*
tsconfig.json TypeScript: ES2022, CommonJS, strictNullChecks, outDir: ./dist
eslint.config.mjs ESLint 9 (flat config) + Prettier + TypeScript
prisma/ Схемы БД и сиды

Точки входа

Файл Назначение
src/main.ts Точка входа: установка логгера в RequestContext, создание и запуск Application
src/application.ts Создание Nest-приложения на Fastify: CORS, версионирование, ValidationPipe, глобальные фильтры/интерцепторы, Swagger, graceful shutdown

Директория src/

Директория Назначение
auth/ Аутентификация: JWT, Local (login/password), TWork (JWKS), guards, strategies, middleware, DTO
common/ Общее: ошибки (errors.ts), AppErrorFilter, константы, RequestContext, декоратор @Public()
config/ Конфигурация: ConfigModule, AppConfigService, configuration.ts (registerAs)
health/ Health: HealthController (healthz, readyz), HealthCheckService
metrics/ Метрики Prometheus: MetricsService, MetricsInterceptor, MetricsController
prisma/ Основная БД: PrismaModule, PrismaService (PostgreSQL)
users/, roles/, groups/, permissions/ Пользователи, роли, группы, права: CRUD, DTO, сервисы
oauth/ OAuth 2.0: authorize, token (в т.ч. password grant), OAuthService
decision/ Бизнес-домен: партнёры, тарифы, связи, холдинги, подписки, заявки; отдельные Prisma-клиенты
entity-history/ История изменений сущностей
partner-extension/ Расширения партнёров (схема octopus_admin)
feature-toggle/ Feature flags (конфиг из env)
logger.ts Экземпляр ServerLogger (@coretech-nodejs/nestjs-logger)
init-fastify.ts Настройка Fastify: requestId, traceId, логирование onRequest/onResponse
init-swagger.ts Инициализация Swagger по AppConfigService

Директория prisma/

Файл Назначение
schema.prisma Основная БД (схема octopus_admin): User, Role, Permission, Group, EntityChangeLog, PartnerExtension
schema-decision.prisma БД решений (схема partner_settings): Partner, тарифы, настройки; клиент в src/generated/decision-prisma
schema-decision-applications.prisma БД заявок (схема bnpl_octopus_decision); клиент в src/generated/decision-applications-prisma
seed.ts Сид: права, роли (admin и др.), группы, пользователи (bcrypt)

Системные пути (без префикса /api)

  • /metrics — Prometheus
  • /healthz — liveness
  • /readyz — readiness
  • /docs — Swagger UI
  • /status — статус

2. Стек технологий

Категория Технология
Фреймворк NestJS 11 + платформа Fastify 5
Язык TypeScript 5.x, целевой ES2022
БД PostgreSQL (несколько баз/схем)
ORM Prisma 6 (три схемы → три клиента)
Валидация class-validator + class-transformer, глобальный ValidationPipe
Документация API @nestjs/swagger (Swagger UI на /docs)
Аутентификация Passport (JWT, Local), @nestjs/jwt, опционально TWork (JWKS, jwks-rsa)
Логирование @coretech-nodejs/nestjs-logger (ServerLogger), RequestContext (requestId, traceId)
Метрики prom-client, эндпоинт /metrics
Graceful shutdown @coretech-nodejs/server-graceful-shutdown
Тесты Jest (ts-jest), testRegex: .*\.spec\.ts$, rootDir: src
Линтинг/форматирование ESLint 9 + typescript-eslint, Prettier (singleQuote, trailingComma)
Движок Node >=20.19

2.1. Зависимости по назначению (логгер, метрики, guards и т.д.)

Пакеты и где используются

Назначение NPM-пакет Где используется
Логирование @coretech-nodejs/nestjs-logger src/logger.ts (ServerLogger), RequestContext, NestFactory, init-fastify (onRequest/onResponse), graceful shutdown
Метрики prom-client MetricsService (Counter, Histogram, Registry), эндпоинт /metrics
Graceful shutdown @coretech-nodejs/server-graceful-shutdown application.ts: preStop → readiness false, затем app.close(), таймауты из конфига
Валидация class-validator, class-transformer Глобальный ValidationPipe, DTO, exceptionFactory → InvalidDataError
Конфигурация @nestjs/config ConfigModule, configuration.ts (registerAs), AppConfigService
Аутентификация @nestjs/jwt, @nestjs/passport, passport, passport-jwt, passport-local, jwks-rsa JwtModule, JwtStrategy, LocalStrategy, JwtAuthGuard, JwtOrTworkAuthGuard (TWork/JWKS), AuthService (sign/verify)
Хеширование паролей bcryptjs AuthService / seed (хеш паролей)
Документация API @nestjs/swagger init-swagger, декораторы @ApiProperty, Swagger UI /docs
HTTP-адаптер fastify, @nestjs/platform-fastify init-fastify, NestFactory.create с FastifyAdapter
БД @prisma/client PrismaService, DecisionPrismaService, ApplicationsPrismaService
Утилиты reflect-metadata, rxjs NestJS (декораторы), интерцепторы/очереди

Guards (стражи)

Guard Регистрация Назначение
JwtOrTworkAuthGuard Глобальный (APP_GUARD) Первый уровень: @Public() → пропуск. Иначе: Bearer + TWork (issuer) → проверка JWKS и TworkUserService.getOrCreateAndSync; иначе JwtAuthGuard. При ошибке — 401.
JwtAuthGuard Используется внутри JwtOrTworkAuthGuard Passport JWT strategy: проверка Bearer-токена через JwtStrategy.
LocalAuthGuard На методе POST /auth/login Passport Local strategy: login + password, выдаёт JWT.
PermissionsGuard На контроллерах/методах через @UseGuards(PermissionsGuard) Проверка прав: роль admin — пропуск; иначе проверка кодов из @RequirePermissions / @RequireAnyPermissions против user.permissions.
RolesGuard В модуле auth (экспорт не глобальный) Проверка ролей пользователя при необходимости.

Фильтры исключений (Exception Filters)

Фильтр Регистрация Назначение
AppErrorFilter Глобальный (useGlobalFilters) HttpException → status + getResponse(). AppError → statusCode из getHttpStatusFromErrorCode + toJSON(). Остальное → 500, «Internal error». После ответа — MetricsService.sendMetrics(). Логирование warn/error для AppError (не internal).

Интерцепторы (Interceptors)

Интерцептор Регистрация Назначение
MetricsInterceptor Глобальный (useGlobalInterceptors) После успешного ответа отправляет метрики: routerPath/url, method, elapsedTime, statusCode (через MetricsService.sendMetrics).

Middleware

Middleware Маршруты Назначение
UserFromHeaderMiddleware api/* (все методы) Читает заголовок x-user-roles (список через запятую) и выставляет request.user.roles. Для тестирования без JWT.

Pipes

Pipe Регистрация Назначение
ValidationPipe Глобальный transform, whitelist, stopAtFirstError; validationError: target/value false; exceptionFactory → InvalidDataError.
useContainer(AppModule) Для class-validator Резолв зависимостей в DTO (DI в валидаторах).

3. Архитектурные подходы

Слои

  • Controllers — приём запросов, вызов сервисов, декораторы Swagger и прав (@RequirePermissions, @RequireAnyPermissions).
  • Services — бизнес-логика, работа с Prisma (основной и Decision-клиентами).
  • Отдельного слоя «repositories» нет — сервисы используют PrismaService / DecisionPrismaService / ApplicationsPrismaService напрямую.

Валидация

  • Глобальный ValidationPipe: transform: true, whitelist: true, stopAtFirstError: true, validationError: { target: false, value: false }.
  • Ошибки валидации приводятся к InvalidDataError (exceptionFactory).
  • useContainer(AppModule) для class-validator (резолв зависимостей в DTO).
  • DTO с декораторами class-validator (@IsString, @IsUUID, @MinLength, @IsEmail и т.д.) и опционально @ApiProperty для Swagger.

Ошибки

  • common/errors.ts:
  • AppErrorCode: invalid, notFound, internal, locked, rateLimit, access, auth.
  • AppError — базовый класс с errorCode и toJSON().
  • InvalidDataError — для ошибок валидации (расширяет AppError, добавляет массив errors).
  • getHttpStatusFromErrorCode() — маппинг кода в HTTP-статус (400, 401, 403, 404, 429, 500).
  • AppErrorFilter (глобальный): для HttpException — статус и тело ответа; для AppError — JSON с statusCode и toJSON(); для остальных — 500 и «Internal error». По всем ответам вызывается MetricsService.sendMetrics().

Логирование

  • Логгер задаётся в RequestContext в main.ts и в init-fastify (onRequest): child logger с requestId и при наличии traceId (заголовки x-trace-id или traceparent).
  • В onResponse — лог завершения запроса и время ответа; при времени > 5 с — пометка slow.
  • Для системных путей (metrics, healthz, readyz, docs, status) логирование запроса/ответа не выполняется.

Конфигурация

  • @nestjs/config: загрузка через registerAs в config/configuration.ts: app, database, jwt, oauth, decision, featureToggle, twork.
  • AppConfigService — типизированные геттеры для порта, хоста, CORS, JWT, OAuth-клиентов, TWork (issuer, jwksUri, enabled, groupMapping), URL БД и т.д.
  • Переменные окружения: LISTEN_PORT/PORT, DATABASE_URL, JWT_SECRET, JWT_EXPIRE, TWORK_*, DECISION_DATABASE_URL, DECISION_APPLICATIONS_DATABASE_URL, OAUTH_*, CORS_*, SLEEP_BEFORE_SHUTDOWN_MS, FORCE_SHUTDOWN_TIMEOUT_MS и др.

4. API

Префикс и версионирование

  • Глобальный префикс /api, кроме системных путей.
  • Версионирование по URI: defaultVersion: '1', префикс v → маршруты вида /api/v1/.... Health и metrics — VERSION_NEUTRAL.

Роуты по модулям

Модуль Примеры маршрутов Примечание
auth POST /api/v1/auth/login, GET /api/v1/auth/me login — Public, me — JWT
users CRUD /api/v1/users список с skip/take, login, isActive
roles, groups, permissions CRUD по своим ресурсам
oauth GET /api/oauth/authorize, POST /api/oauth/token Public
decision партнёры, тарифы, связи, холдинги, подписки, заявки у партнёров — :point_id/history
health GET /healthz, GET /readyz без префикса api
metrics GET /metrics Prometheus

Middleware

  • UserFromHeaderMiddleware — для всех api/* выставляет request.user.roles из заголовка x-user-roles (список через запятую). Используется для тестирования без JWT.

Аутентификация и авторизация

  • Глобальный guard: JwtOrTworkAuthGuard (APP_GUARD).
  • Сначала проверка @Public() — если есть, доступ разрешён.
  • Иначе: при Bearer-токене и включённом TWork с совпадением issuer — проверка через JWKS и подстановка пользователя через TworkUserService.getOrCreateAndSync; иначе — JwtAuthGuard (Passport JWT). При ошибке — 401.
  • Локальная аутентификация: POST /api/v1/auth/login с LocalAuthGuard (login/password), выдаётся JWT.
  • Авторизация по правам: на контроллерах/методах — @UseGuards(PermissionsGuard) и @RequirePermissions('...') или @RequireAnyPermissions('...'). Роль admin обходит проверку; иначе проверяются коды прав из user.permissions (эффективные права с учётом ролей и групп).
  • Публичные маршруты помечаются декоратором @Public() (auth/login, oauth authorize/token, health, metrics, docs).

5. Скрипты (package.json)

Скрипт Назначение
build prisma generate для всех трёх схем + nest build
prestart:dev prisma generate для dev
start nest start
start:dev nest start --watch
start:debug nest start --debug --watch
start:prod node dist/main
lint ESLint с автофиксом
check tsc --noEmit + lint + test
test, test:watch, test:cov Jest
db:seed Prisma seed
db:users скрипт list-users

6. Деплой и окружения

  • Запуск в production: node dist/main (после nest build).
  • Конфигурация только через переменные окружения (нет .env в репозитории).
  • Graceful shutdown: по сигналу сначала readiness = false (preStop), затем закрытие приложения; таймауты — sleepBeforeShutdownMs, forceShutdownTimeoutMs.
  • Readiness для Kubernetes: пока сервис не готов — readyz возвращает 503.

7. Зависимости (сводка)

Полное описание по назначению (логгер, метрики, guards, фильтры, интерцепторы, middleware) — в разделе 2.1.

Runtime (dependencies):

Группа Пакеты
NestJS @nestjs/common, @nestjs/core, @nestjs/config, @nestjs/jwt, @nestjs/passport, @nestjs/platform-fastify, @nestjs/swagger
HTTP fastify, @fastify/static
БД @prisma/client
Аутентификация passport, passport-jwt, passport-local, jwks-rsa
Валидация / трансформация class-validator, class-transformer
Логирование @coretech-nodejs/nestjs-logger
Метрики prom-client
Завершение работы @coretech-nodejs/server-graceful-shutdown
Прочее bcryptjs, reflect-metadata, rxjs

Dev: @nestjs/cli, @nestjs/testing, prisma, typescript, jest, ts-jest, eslint, prettier, typescript-eslint и типы (@types/...).
Node: >=20.19.


8. Особенности

  • Три независимых Prisma-клиента (основная БД, decision, decision-applications) и соответствующие сервисы подключения.
  • Swagger: Bearer JWT, описание входа через login или OAuth, UI на /docs.

Итог: octopus-api — NestJS 11 на Fastify с Prisma (три БД), JWT + опционально TWork, RBAC через роли/права/группы, единая обработка ошибок, контекстное логирование и метрики Prometheus, готовность к деплою в Kubernetes с health/readiness и graceful shutdown.