Перейти к содержимому

Правила написания кода

Обязательный документ для агентов и разработчиков, которые пишут код в этом репозитории. Проектные и архитектурные решения сюда не входят — они в architecture/ и docs/plans/. Что и как документировать после реализации — в documentation-rules.md.

Соотношение с repository-guide.md: тот документ описывает, как устроен репозиторий, и читается для понимания. Этот — предписывает, как писать код, и обязателен к исполнению. При расхождении приоритет за правилами.

Правила выведены из фактического кода. Если правило и код расходятся — правило приоритетнее, кроме случаев, перечисленных в разделе «Известные расхождения».


  • Всё в kebab-case: admin-project.service.ts, header-fingerprint.decorator.ts.
  • Роль файла задаётся суффиксом, а не папкой:
СуффиксЧто внутри
*.module.tsNestJS-модуль
*-core.module.tsмодуль без контроллеров (только провайдеры на переиспользование)
*.controller.tsHTTP-контроллер
*.gateway.tsWebSocket-шлюз
*.service.tsбизнес-логика
*.repository.tsдоступ к данным
*.strategy.tsPassport-стратегия
*.guard.tsguard
*.decorator.tsпараметр-декоратор
*.filter.tsexception filter
*.helper.tsчистые функции-хелперы
*.dto.tsDTO, лежит в подпапке dto/data-access исторически dtos/)
*.type.ts / *.interface.tsтипы и интерфейсы
*.constants.tsконстанты модуля
*.env.ts, *.validation.ts, *.config.tsчасти конфиг-модуля
*.spec.tsunit-тест, лежит рядом с тестируемым файлом
  • Имя файла = доменный префикс + роль: admin-project.repository.ts, customer-auth.constants.ts.
  • Папки внутри библиотеки — во множественном числе по роли: dto/, guards/, helpers/, constants/, types/, validators/, services/, filters/, decorators/.

1.2. Регистрация библиотеки — четыре имени должны совпадать

Заголовок раздела «1.2. Регистрация библиотеки — четыре имени должны совпадать»

Это то место, где чаще всего ломают конвенцию. При создании библиотеки одновременно появляются четыре идентификатора, и все они выводятся из пути, а не придумываются:

ГдеЗначениеПример для libs/apis/providers/user-api/features/profile
project.jsonnameпуть без libs/apis/, через дефисuser-api-feature-profile
jest.config.tsdisplayNameто же самое, что nameuser-api-feature-profile
tsconfig.base.json → алиаспуть как есть, через слэши@crewsforge-back/apis/providers/user-api/features/profile
jest.config.tscoverageDirectorycoverage/ + полный путь библиотеки../../../../../../coverage/libs/apis/providers/user-api/features/profile

Схемы имён Nx-проектов по типу библиотеки:

Тип библиотекиСхема name / displayNameПример
фича сервиса<service>-feature-<feature>auth-api-feature-customer-auth
data-access сервиса<service>-data-accessproject-api-data-access
конфигconfig-<scope>-<name>config-shared-s3, config-auth-api-jwt-customer
клиент инфраструктуры<name>-clientredis-client, mailer-client
общая библиотекаapi-sharedapi-shared
утилитаapi-util-<name>api-util-prisma-client
доменное ядроapi-core-<name>api-core-money

Алиас всегда строится как @crewsforge-back/apis/<то же, что путь после libs/apis/>. Плоские алиасы вида @crewsforge-back/redis-client — легаси, новые так не заводятся.

Теги в project.json обязательны и состоят ровно из двух: scope:<service>|shared|utils|config и type:feature|data-access|util|config.

PascalCase, имя = <Домен><Уточнение><Роль>, где <Роль> дословно повторяет суффикс файла:

admin-project.service.ts → AdminProjectService
admin-project.repository.ts → AdminProjectRepository
admin-project.controller.ts → AdminProjectController
admin-project.module.ts → AdminProjectModule
customer-core.module.ts → CustomerCoreModule
admin-access-token.guard.ts → AdminAccessTokenGuard
admin-access-token.strategy.ts→ AdminAccessTokenStrategy
s3-config.service.ts → S3ConfigService
agent-chat.gateway.ts → AgentChatGateway

Домен в имени класса не сокращается и повторяется полностью, даже если он уже есть в пути библиотеки: внутри features/admin-project класс называется AdminProjectService, а не ProjectService.

<Домен><Действие|Сущность>[Response]Dto. Роль определяется хвостом:

ХвостНазначениеПример
...Dtoтело входящего запросаAdminLoginDto, BlockUserDto, LeaveRequestDto
...QueryDtoquery-параметрыAdminProjectQueryDto, AgentHistoryQueryDto
...CreateDto / ...UpdateDtoсоздание / обновлениеCustomerCreateDto, AdminProjectUpdateDto
...ResponseDtoтело ответаAdminProjectResponseDto, AdminMeResponseDto
  • Поля DTO — camelCase, обязательные помечаются !, необязательные — ?.
  • Общие поля id/createdAt/updatedAt наследуются от BaseDto из data-access сервиса.
  • Вложенный DTO объявляется отдельным файлом и подключается через @Type(() => XxxDto) + @ValidateNested().

Схема: <глагол><One|Many|All>[By<Поле>][OrFail].

ИмяСмысл
findAll(query)список с фильтрами и пагинацией
findOneById(id)найти или null
findOneByIdOrFail(id)найти или NotFoundException
findOneByUserIdOrFail(userId)поиск по другому полю — поле в имени
updateOneById(id, dto)обновление
deleteOneByIdOrFail(id)удаление
softDeleteOneById(id, adminId)мягкое удаление
saveOne(dto)создание/сохранение

Суффикс OrFail означает и обязывает бросать исключение. Метод без OrFail возвращает null и не бросает. Доменные действия именуются глаголом предметной области: signIn, signUp, signOut, refreshToken, blockUser, sendVerificationEmailCode.

Повторяют словарь Prisma: findMany, findUnique, findFirst, create, update, delete, deleteMany, count, upsert. Составные операции склеиваются через And: findManyAndCount, findManyAndPaginate. Для конкретной под-сущности имя дополняется ею: findUniqueAvatar, updateSkills, deleteLanguages.

  • Локальные переменные, поля, методы, параметры — camelCase.
  • Зависимость в конструкторе называется camelCase-версией своего класса и объявляется private readonly:
    constructor(
    private readonly adminProjectRepository: AdminProjectRepository,
    private readonly mapperService: MapperService,
    ) {}
    Единственное закреплённое исключение — PrismaClientService, его принято звать prisma.
  • Логгер: private readonly logger = new Logger(XxxService.name) — имя класса берётся через .name, не строкой.
  • Булевы поля и переменные начинаются с is/has: isBlocked, isEmailVerified, isFirstPage.
  • Массивы — во множественном числе (sourceObjects, variables), одиночные сущности — в единственном.
  • Сокращения не используются: adminProjectService, а не aps или svc.
  • Модульные константы — UPPER_SNAKE_CASE, живут в *.constants.ts: DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, ADMIN_ROUTE_PREFIX.
  • DI-токены — Symbol с именем, совпадающим с именем константы: export const REDIS_CLIENT = Symbol('REDIS_CLIENT');
  • Ключи Redis и TTL к ним — парой, с суффиксами _KEY и _TTL:
    export const CUSTOMER_VERIFICATION_CODE_KEY = 'CUSTOMER_VERIFICATION_CODE';
    export const CUSTOMER_VERIFICATION_CODE_TTL = 900; // 15 min
    TTL — в секундах, с комментарием в человеческих единицах.
  • Имена Passport-стратегий и cookie — <ROLE>_<ACCESS|REFRESH>_TOKEN_<STRATEGY|COOKIE>_NAME в libs/apis/shared/src/lib/constants/token.constants.ts. Строковые значения не хардкодятся в стратегиях и guard’ах — только через эти константы.
  • Литеральные наборы фиксируются через as const.
  • Пути — kebab-case, ресурсы во множественном числе: customers/auth/reset-password, employees/me/profiles, projects.
  • Вложенность отражает владение ресурсом: customers/me/projects.
  • Параметры пути — camelCase и осмысленные: :projectId, :customerId; :id допустим, когда ресурс в пути один.
  • Действие над ресурсом — отдельный сегмент-глагол: :id/block, :id/unblock, :id/restore, :projectId/status.
  • @ApiTags совпадает с путём контроллера: @Controller('customers/me/projects')@ApiTags('customers/me/projects').
  • Ведущий слэш в путях методов не ставится: @Get('me'), а не @Get('/me').

Файлы конфиг-библиотеки <name>:

<name>.env.ts → interface <Name>EnvironmentVariables, get<Name>Environment()
<name>-config.validation.ts → export const validationSchema (Joi)
<name>.config.ts → export const <name>Config = registerAs('<name>', ...)
<name>-config.service.ts → class <Name>ConfigService с геттерами
<name>-config.module.ts → class <Name>ConfigModule
  • Имя namespace в registerAs — короткое имя конфига в нижнем регистре ('s3', 'app'), ключи внутри namespace — camelCase (s3.accessKeyId).
  • Переменные окружения — UPPER_SNAKE_CASE с префиксом домена: S3_BUCKET, SMTP_HOST, JWT_ADMIN_ACCESS_SECRET_KEY, RABBITMQ_REQUEST_QUEUE.
  • Геттер сервиса называется по смыслу без префикса домена: S3ConfigService.bucket, а не s3Bucket.
  • Модель — PascalCase в единственном числе: User, EmployeeProfile, WalriderThread.
  • Поля — camelCase, физическая колонка — @map("snake_case").
  • Таблица — @@map("множественное_число_snake_case"): @@map("users"), @@map("employee_profiles").
  • Enum — PascalCase, значения — UPPER_SNAKE_CASE: ProjectStatus.IN_PROGRESS.
  • Связующие таблицы — <A>On<B> (UserOnRole) или <Owner><Child> (EmployeeProfileSkill).
  • Стандартные поля: id (uuid, @default(dbgenerated("gen_random_uuid()"))), createdAt, updatedAt, deletedAt для soft delete.
  • Папка миграции — <timestamp>_<краткое_описание_snake_case>: 20260321000000_add_admin_service.
describe('AdminContactService', () => { // имя тестируемого класса
describe('leaveRequest', () => { // имя метода
it('sends the lead to the support inbox with reply-to set to the applicant', ...)

describe — имя класса, вложенный describe — имя метода, it — утверждение на английском в третьем лице («что делает»), а не «should …».

action в логе строится как <nx-имя библиотеки>/<Класс>/<метод> — см. раздел 10.3.

  • Ветки: feat/<kebab-тема>, fix/<kebab-тема>.
  • Коммиты — Conventional Commits, на английском, в нижнем регистре, со скоупом: feat(admin-api): add public landing contact endpoints. Типы: feat, fix, docs, refactor, chore, migration.
  • Документы планов: docs/plans/YYYY-MM-DD-<kebab-тема>-design.md и docs/plans/YYYY-MM-DD-<kebab-тема>.md.

  • Бизнес-логика — только в libs/. В apps/<service> правим лишь main.ts (глобальные пайпы, фильтры, Swagger) и app/app.module.ts (подключение модулей).
  • Новая фича сервиса → libs/apis/providers/<service>/features/<feature>/.
  • DTO, константы, типы, разделяемые несколькими фичами сервиса → libs/apis/providers/<service>/data-access/. Контроллеров и сервисов в data-access быть не должно.
  • Guard, декоратор, хелпер, валидатор, нужный больше чем одному сервису → libs/apis/shared/.
  • Обёртка над внешней системой → libs/apis/utils/<name>-client/.
  • Чтение окружения → libs/apis/configs/<scope>/<name>/.
Controller → Service → Repository → PrismaClientService / Redis / Rabbit / S3 / Mailer
  • Controller: маршрутизация, guard’ы, Swagger-декораторы, извлечение данных из запроса, вызов сервиса. Ни бизнес-правил, ни обращений к Prisma.
  • Service: бизнес-правила, оркестрация, транзакции, бросание HTTP-исключений.
  • Repository: единственное место, где инжектится PrismaClientService. Возвращает Prisma-сущности, не DTO.
  • Repository можно не заводить для тривиальной фичи, но зависимости всегда направлены сверху вниз.
  • *-core.module.ts создаётся, когда логику фичи нужно переиспользовать в другом приложении без HTTP-слоя; *.module.ts импортирует core и добавляет контроллеры.
  • Между проектами — только алиасы @crewsforge-back/.... Относительные пути — внутри одного проекта.
  • Публичный API библиотеки — src/index.ts. Импортировать внутренние файлы чужой библиотеки нельзя; нужно наружу — добавь реэкспорт в index.ts.
  • Границы проверяет @nx/enforce-module-boundaries по тегам. Действующие ограничения: type:data-access → только data-access / scope:shared / scope:utils; type:config → ни от кого не зависит; scope:admin-apiadmin-api, shared, utils, user-api, project-api.
  • Циклические импорты между библиотеками ломают старт приложения (чинили в 4e3a131). После добавления зависимости проверяй npx nx graph.
  • Каждое поле DTO — @Expose() + @ApiProperty() + валидаторы class-validator. Без @Expose() поле молча пропадает (strategy: 'excludeAll') — это уже ловили в проде (e5bc72c).
  • Числовые query-параметры — с @Type(() => Number), строковые — с @Trim() из shared.
  • Enum-поля объявляются через enum из @prisma/client и @IsEnum(...), в @ApiProperty({ enum }).
  • Сущность → DTO только через MapperService из @crewsforge-back/apis/shared: он сериализует Prisma-типы (Decimal, Date) перед plainToInstance. Ручной plainToInstance не использовать.
  • Пагинация — getPaginate / toPaginateResponse из shared/helpers, ответ в форме { data, meta }. Размеры страниц — из констант DEFAULT_PAGE_SIZE / MAX_PAGE_SIZE.
  • Пользовательский ввод, попадающий в HTML (письма), экранировать; на это есть тест в admin-contact.

Обязательный набор декораторов:

@ApiTags('admin-projects')
@ApiBearerAuth()
@UseGuards(AdminAccessTokenGuard)
@Controller('projects')
export class AdminProjectController {
@ApiOperation({ summary: 'Update a project' })
@ApiResponse({ status: 200 })
@Patch(':id')
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AdminProjectUpdateDto) {
return this.adminProjectService.updateOneById(id, dto);
}
}
  • Guard’ы и параметр-декораторы (AdminAccessTokenGuard, @GetTokenPayload(), @Token(), @HeaderFingerprint(), @HeaderSessionId()) берутся из shared, свои не изобретаются.
  • Публичный эндпоинт без guard’а сопровождается комментарием, почему авторизации нет.
  • Идентификаторы в пути — через ParseUUIDPipe.
  • Ответ без тела — @HttpCode(HttpStatus.NO_CONTENT) и возвращаемый тип Promise<void>.
  • Ошибки — штатные исключения Nest (NotFoundException, BadRequestException, ForbiddenException), самописных классов ошибок нет.
  • process.env в бизнес-коде запрещён — только типизированный XxxConfigService. Единственное легальное место чтения process.env — файл *.env.ts конфиг-библиотеки.
  • Новая переменная окружения требует одновременно: строку в Joi-схеме, геттер в config-сервисе, заглушку в jest.setup-env.ts (иначе падают все тесты), строку в docs/04-shared-and-utils/configs.md и docs/06-operations/environment.md, правку соответствующего dokploy/docker-compose.*.yml.
  • .env в git не коммитится; реальные значения задаются в Dokploy.
  • Схема одна: apps/core-api/prisma/schema.prisma. Команды — всегда с явным --schema:
    Окно терминала
    npx prisma generate --schema=./apps/core-api/prisma/schema.prisma
    npx prisma migrate dev --schema=./apps/core-api/prisma/schema.prisma
  • Изменение схемы и файл миграции попадают в один коммит. Забытая миграция уже ломала прод (3734cbc).
  • Soft delete: в репозиториях фильтровать deletedAt: null, физически строки не удалять.
  • Транзакции — декларативно, через @Transactional() (@nestjs-cls/transactional).
  • Фильтр по строке — { contains: value, mode: 'insensitive' }.
  • include-объекты, повторяющиеся в репозитории, выносятся в модульную константу (const projectInclude = {...}).
  • Prettier: singleQuote: true, printWidth: 110, 2 пробела, финальный перевод строки. Перед сдачей — npx prettier --write <изменённые файлы>.
  • TypeScript strict: strict, noImplicitOverride, noImplicitReturns, noFallthroughCasesInSwitch, noPropertyAccessFromIndexSignature. any не добавлять.
  • Комментарии — редкие и про «почему», а не «что»; язык комментариев русский, как в окружающем коде. Код, идентификаторы, тексты исключений и Swagger-описания — английские.

Логирования в коде почти нет: на весь монорепозиторий 31 вызов логгера, из них 17 — строковые, плюс 21 обращение к console.*. Инфраструктура (nestjs-pino, библиотека libs/apis/utils/logger) ещё не подключена — это Этап 4 плана observability.

Правила ниже — целевая конвенция, и писать по ним нужно уже сейчас: nestjs-pino подменяет логгер Nest, поэтому обычные new Logger(...) начнут писать JSON без единой правки в местах вызова. Код, написанный по этим правилам сегодня, при подключении pino заработает как надо; код, написанный строками, придётся переписывать.

Лог — всегда объект, никогда не строка:

private readonly logger = new Logger(AdminContactService.name);
this.logger.log({
action: 'admin-contact/AdminContactService/leaveRequest',
msg: 'lead accepted',
data: { fieldOfActivity },
});
this.logger.error({
action: 'admin-contact/AdminContactService/leaveRequest',
msg: 'failed to send lead email',
err,
});
ПолеОбяз.ТипНазначение
actionдаstringгде произошло: <библиотека>/<Класс>/<метод> (10.3)
msgдаstringчто произошло: стабильная фраза без значений (10.4)
dataнетobjectконтекст, только скаляры (10.5)
errдля warn/error/fatalErrorобъект ошибки отдельным полем (10.6)
stageнетstringэтап длинной многошаговой операции
durationMsнетnumberдлительность операции
доменные idнетstringuserId, projectId, threadIdверхним уровнем, по ним чаще всего ищут

Добавляются автоматически, руками не писать: time, level, pid, hostname, service (имя сервиса) и requestId (из ClsModule, сквозная трассировка запроса).

<nx-имя библиотеки>/<Класс>/<метод>
action: 'admin-contact/AdminContactService/leaveRequest'
action: 'auth-api-feature-customer-auth/CustomerAuthService/signIn'
action: 'rabbit-client/RabbitClientService/connect'
  • Разделитель — слэш, не дефис. Имена библиотек и так kebab-case (admin-contact, auth-api-feature-customer-auth), поэтому строка вида admin-contact-AdminContactService-leaveRequest не разбирается ни глазом, ни split('-'). Слэш даёт ровно три однозначных сегмента.
  • Первый сегмент — Nx-имя библиотеки (раздел 1.2), а не путь и не имя NestJS-модуля.
  • Второй и третий — имя класса и метода дословно, как в коде.
  • Пишется строковым литералом. Не собирать через this.constructor.name и не выносить в переменную: значение должно быть видно в месте вызова и переживать минификацию сборки.
  • В приватном хелпере указывается сам хелпер, а не вызвавший его публичный метод.
  • Английский, нижний регистр, короткая фраза.
  • Без интерполяции значений. Строка должна быть стабильной, иначе одинаковые события не группируются в Grafana и по ним нельзя построить счётчик.
  • Описывает исход, а не намерение: lead email sent, login rejected: domain not allowed.
// плохо — значения запечены в текст, фильтровать можно только регуляркой
this.logger.log(`Client ${client.id} connected to project ${projectId}`);
// хорошо
this.logger.log({
action: 'ai-agent-service-feature-agent-chat/AgentChatGateway/handleConnection',
msg: 'client connected',
projectId,
data: { clientId: client.id },
});
  • Допустимы string, number, boolean, null и короткие массивы скаляров с ограниченной длиной (например allowedDomains).
  • Запрещено класть Prisma-сущности, DTO, тела запросов и ответов, буферы, массивы объектов. Это раздувает Loki и затягивает в логи то, что туда не собирались класть, включая секреты.
  • Вместо коллекции — её размер: data: { projectsCount: projects.length }.
  • Идентификаторы, по которым будут искать, выносятся верхним уровнем, а не прячутся в data; в data остаётся второстепенный контекст.
  • Передаётся отдельным полем err и только целым объектом ошибки: err сериализуется вместе со стеком.
  • Не err: error.message, не склейка ошибки в msg.
  • Только на уровнях warn, error, fatal.
УровеньКогда
fatalпроцесс не может продолжать работу
errorнепредвиденный сбой, требующий вмешательства; всегда с err
warnожидаемый отказ по бизнес-правилу (домен не разрешён, код истёк, пользователь заблокирован) или деградация с ретраем
infoзначимое изменение состояния домена и жизненный цикл сервиса
debugшаги внутри операции, нужные при разборе; в проде выключены
traceне используем

this.logger.log(...) в Nest — это уровень info, this.logger.verbose(...)debug.

Ожидаемая ошибка валидации или авторизации — это warn или вообще ничего, но никогда не error. Уровень error зарезервирован за тем, на что реально нужно реагировать: иначе алерты по ошибкам бесполезны.

Логировать обязательно:

  • изменение состояния домена — создание, блокировка, смена статуса, удаление (info);
  • отказ по бизнес-правилу или авторизации, непосредственно перед throw (warn);
  • перехваченный сбой обращения к внешней системе — БД, S3, SMTP, RabbitMQ, Redis (error с err);
  • жизненный цикл соединений и приложения: connected, disconnected, consumer started (info);
  • начало и конец длинной фоновой операции (info, на конце — durationMs).

Не логировать:

  • успешные чтения и вообще GET-запросы;
  • одно и то же событие дважды на разных слоях: логирует тот, кто ошибку обрабатывает, а не тот, кто её пробрасывает;
  • внутри цикла по элементам — агрегировать в одну запись со счётчиком;
  • то, что и так пишет HTTP-слой: метод, путь, статус, длительность;
  • секреты и персональные данные (10.9).

Логирование живёт в сервисах и инфраструктурных клиентах. Контроллеры и репозитории не логируют.

Никогда не попадают в лог: пароли и их хеши, JWT и refresh-токены, коды подтверждения, заголовки Authorization / Cookie / Set-Cookie, содержимое писем, тела запросов целиком, платёжные данные.

Email в логе — только маскированный (pro***@crewsforge.com); где возможно, идентифицировать пользователя через userId, а не через email. Логи живут 30 дней и доступны всем, у кого есть вход в Grafana.

redact на стороне pino — вторая линия обороны, а не разрешение логировать секреты и надеяться на него.

  • Один вызов логгера = одно событие = одна строка JSON.
  • console.* в рантайм-коде запрещён. Допустим только в apps/core-api/prisma/seeds/** и apps/*-e2e/src/support/**.
  • Логгер — поле класса (private readonly logger = new Logger(XxxService.name)), а не создаётся на каждый вызов.
  • Тесты не проверяют тексты логов и не завязывают на них assert’ы.
  • JSON-поля ≠ лейблы Loki. Лейблы — только service, level, env. requestId, userId, email остаются полями: превращение их в лейблы взрывает Loki кардинальностью ровно так же, как Prometheus.

При правке файла, где уже есть логирование, попутно привести его к конвенции:

  • 17 строковых вызовов → структурные (ai-agent-service, rabbit-client, employee.service.ts);
  • 13 структурных вызовов в admin-auth используют поле message с кодами вида admin.register.initiated и логируют сырой email → перевести на action + msg, email убрать или маскировать;
  • console.* в рантайм-библиотеках (mailer-client, redis-client, email-sender, admin-contact) → логгер.

Отдельной задачей это не гонится — но новый код пишется сразу правильно.

Окно терминала
npx prettier --write <файлы>
npx nx lint <project>
npx nx test <project>
npx nx build <app>
# либо разом:
npx nx affected -t lint,test,build

CI в репозитории нет — проверки только локальные, поэтому пропускать их нельзя. Заявлять «готово» можно после реального запуска команд и просмотра вывода, а не по факту правки файлов. Коммитить и пушить — только по явной просьбе.

  1. DTO в data-access сервиса (@Expose() + @ApiProperty() + валидаторы).
  2. Папка libs/apis/providers/<service>/features/<feature>/ с module / controller / service / (repository), плюс project.json, tsconfig.json, tsconfig.lib.json, tsconfig.spec.json, jest.config.ts, README.md.
  3. Все четыре имени библиотеки согласованы (раздел 1.2), теги проставлены.
  4. Публичный API — через src/index.ts.
  5. Алиас в tsconfig.base.json по схеме @crewsforge-back/apis/....
  6. Модуль подключён в apps/<service>/src/app/app.module.ts.
  7. Guard’ы и маппер — из shared, конфиги — из configs, клиенты — из utils.
  8. Миграция Prisma (если менялась схема) и полный обвяз новой env-переменной (раздел 7).
  9. Логирование по разделу 10: структурные вызовы, action + msg, без секретов и console.*.
  10. Тесты *.spec.ts; lint / test / build зелёные.
  11. Документация обновлена по documentation-rules.md: README.md библиотеки, при необходимости её docs/, и страница сервиса в docs/03-services/.
  • Имена библиотек. Новые admin-* библиотеки зарегистрированы как admin-contact, admin-project, admin-user, admin-auth, jwt-admin и особенно data-access — в обход схемы из раздела 1.2. Правильные имена были бы admin-api-feature-contact, admin-api-data-access, config-auth-api-jwt-admin. Ориентироваться на схему, а не на эти имена.
  • Теги. Проставлены лишь у пяти библиотек из тридцати шести, поэтому правила границ Nx сейчас работают частично. У новых библиотек теги обязательны.
  • Алиасы. В tsconfig.base.json соседствуют две схемы; каноническая — @crewsforge-back/apis/....
  • ValidationPipe. Настроен по-разному в разных сервисах; эталон — apps/admin-api/src/main.ts (whitelist, forbidNonWhitelisted, transform).
  • @Controller. Часть контроллеров задаёт версию явно ({ path, version: '1' }), часть — нет. Для новых кода достаточно строкового пути: версионирование включено глобально.
  • Папка DTO. В старых data-access встречается dtos/, в новом коде — dto/.