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